OpenLayersMap.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. <template>
  2. <div id="map" ref="map">
  3. <div class="toolbox">
  4. <button @click="loadMarker">加载标记</button>
  5. <button
  6. :style="{ backgroundColor: isMarking ? '#ff4d4f' : '', color: isMarking ? '#fff' : '' }"
  7. @click="markPlace"
  8. >
  9. {{ isMarking ? '结束标记' : '标记地点' }}
  10. </button>
  11. <button
  12. :style="{ backgroundColor: isDrawingCircle ? '#409EFF' : '', color: isDrawingCircle ? '#fff' : '' }"
  13. @click="drawCircle"
  14. >
  15. {{ isDrawingCircle ? '结束画圆' : '绘制圆形' }}
  16. </button>
  17. <button @click="measure('distance')">测距离</button>
  18. <button @click="measure('area')">测面积</button>
  19. <button @click="clear">清除</button>
  20. </div>
  21. <div ref="radiusOverlayLabel" class="ol-radius-marker" style="display: none;">
  22. {{ liveRadiusText }}
  23. </div>
  24. </div>
  25. </template>
  26. <script>
  27. import 'ol/ol.css'
  28. import Map from 'ol/Map'
  29. import View from 'ol/View'
  30. import { Vector as VectorSource, OSM, XYZ } from 'ol/source'
  31. import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
  32. // Draw 绘制功能几何的交互
  33. import { Draw, Modify } from 'ol/interaction'
  34. import Overlay from 'ol/Overlay'
  35. // 线条几何形状
  36. import { LineString } from 'ol/geom'
  37. // 具有几何和其他属性属性属性的地理特征的矢量对象,类似于像 GeoJSON 这样的矢量文件格式中的特征
  38. import Feature from 'ol/Feature'
  39. import { unByKey } from 'ol/Observable'
  40. // 获取几何形状的球形长度和面积
  41. import { getLength, getArea } from 'ol/sphere'
  42. import Style from 'ol/style/Style'
  43. import Stroke from 'ol/style/Stroke'
  44. import Fill from 'ol/style/Fill'
  45. import Circle from 'ol/style/Circle'
  46. import { fromLonLat, toLonLat } from 'ol/proj'
  47. import Icon from 'ol/style/Icon'
  48. import Point from 'ol/geom/Point'
  49. import Text from 'ol/style/Text'
  50. import { getMapMarks } from '@/api/map'
  51. export default {
  52. name: 'OpenLayersMap',
  53. data() {
  54. return {
  55. center: [104.068071, 30.576432],
  56. zoom: 12, // 比例尺 10 公里
  57. map: null,
  58. vectorLayer: null,
  59. vectorSource: null,
  60. // 标记地点
  61. isMarking: false,
  62. clickListenerKey: null,
  63. // --- 新增:绘制圆形相关变量 ---
  64. isDrawingCircle: false, // 是否处于画圆状态
  65. liveRadiusText: '', // 实时半径文字
  66. radiusOverlay: null, // 存放半径文本的 Overlay 实例
  67. draw: null,
  68. select: null,
  69. modify: null,
  70. measureType: 'distance',
  71. tipDiv: null,
  72. pointermoveEvent: null, // 地图pointermove事件
  73. sketchFeature: null, // 绘制的要素
  74. geometryListener: null, // 要素几何change事件
  75. measureResult: '0', // 测量结果
  76. markerSource: null // 新增:专门存放后台加载的标记点
  77. }
  78. },
  79. mounted() {
  80. this.markerSource = new VectorSource({ wrapX: false })
  81. const markerLayer = new VectorLayer({
  82. source: this.markerSource,
  83. name: '后台标记图层'
  84. })
  85. // 底图使用 OpenStreetMap, 对应 view
  86. var baseLayer = new TileLayer({
  87. source: new OSM()
  88. })
  89. // 矢量图层源
  90. this.vectorSource = new VectorSource({
  91. wrapX: false
  92. })
  93. // 矢量图层
  94. this.vectorLayer = new VectorLayer({
  95. source: this.vectorSource
  96. })
  97. var url = 'https://webrd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}'
  98. // 切换为高德地图的电子地图底图
  99. var amapLayer = new TileLayer({
  100. source: new XYZ({
  101. // 高德官方切片地址(t1-t4可选,lang=zh_cn代表中文,style=7代表标准地图)
  102. url: url,
  103. crossOrigin: 'anonymous',
  104. maxZoom: 18
  105. }),
  106. name: '高德底图'
  107. })
  108. /* this.map = new Map({
  109. layers: [baseLayer, this.vectorLayer, markerLayer],
  110. target: 'map',
  111. view: new View({
  112. projection: 'EPSG:4326',
  113. center: [104.06531800244139, 30.65852484539117],
  114. zoom: 10
  115. })
  116. })*/
  117. this.map = new Map({
  118. layers: [amapLayer, this.vectorLayer, markerLayer], // 替换掉 baseLayer
  119. target: 'map',
  120. view: new View({
  121. projection: 'EPSG:3857', // ⚠️注意:高德切片是墨卡托(3857)投影,建议View也改为3857
  122. center: fromLonLat(this.center), // 用 fromLonLat 转换中心点
  123. zoom: 12,
  124. maxZoom: 18
  125. })
  126. })
  127. },
  128. created() {
  129. document.title = 'OpenLayersMap'
  130. },
  131. methods: {
  132. drawCircle() {
  133. this.isDrawingCircle = !this.isDrawingCircle
  134. // 清理上一次的组件
  135. if (this.draw) this.map.removeInteraction(this.draw)
  136. if (this.modify) this.map.removeInteraction(this.modify)
  137. if (this.radiusOverlay) this.map.removeOverlay(this.radiusOverlay)
  138. if (this.isDrawingCircle) {
  139. this.map.getTargetElement().style.cursor = 'crosshair'
  140. // 1. 创建半径气泡标签 DOM
  141. const labelEl = document.createElement('div')
  142. labelEl.className = 'ol-radius-marker'
  143. this.radiusOverlay = new Overlay({
  144. element: labelEl,
  145. offset: [15, -15],
  146. positioning: 'bottom-left',
  147. stopEvent: false
  148. })
  149. this.map.addOverlay(this.radiusOverlay)
  150. // 2. 初始化绘制器
  151. this.draw = new Draw({
  152. source: this.vectorSource,
  153. type: 'Circle'
  154. })
  155. let centerFeature = null
  156. // 3. 监听开始绘制
  157. this.draw.on('drawstart', (evt) => {
  158. const geometry = evt.feature.getGeometry()
  159. // 绘制圆心红点
  160. const initialCenter = geometry.getCenter()
  161. centerFeature = new Feature({ geometry: new Point(initialCenter) })
  162. centerFeature.setStyle(new Style({
  163. image: new Circle({
  164. radius: 5,
  165. fill: new Fill({ color: '#ff4d4f' }),
  166. stroke: new Stroke({ color: '#ffffff', width: 2 })
  167. })
  168. }))
  169. this.vectorSource.addFeature(centerFeature)
  170. // 绑定高频拖拽计算半径事件
  171. const bindGeometryChange = (geom) => {
  172. geom.on('change', () => {
  173. const radiusInMeters = geom.getRadius()
  174. const text = radiusInMeters < 1000 ? `${radiusInMeters.toFixed(1)}m` : `${(radiusInMeters / 1000).toFixed(2)}km`
  175. labelEl.innerText = text
  176. // 标签死死跟着圆周上的鼠标/控制点边缘
  177. const lastCoordinate = geom.getLastCoordinate()
  178. if (this.radiusOverlay) {
  179. this.radiusOverlay.setPosition(lastCoordinate)
  180. }
  181. // 同步纠正圆心物理位置
  182. if (centerFeature) centerFeature.getGeometry().setCoordinates(geom.getCenter())
  183. })
  184. }
  185. bindGeometryChange(geometry)
  186. // 将这个绑定函数挂载到 feature 上,方便后期二次编辑修改时复用
  187. evt.feature.set('changeBinder', bindGeometryChange)
  188. })
  189. // 4. 核心新增:监听绘制结束,立刻无缝移交控制权给 Modify
  190. this.draw.on('drawend', (evt) => {
  191. // 移除画圆交互(不让继续画新圆了),但保留图形和标签
  192. this.map.removeInteraction(this.draw)
  193. this.map.getTargetElement().style.cursor = 'default'
  194. const finishedFeature = evt.feature
  195. // 5. 激活修改交互器(传入指定的图层源)
  196. this.modify = new Modify({
  197. source: this.vectorSource
  198. })
  199. this.map.addInteraction(this.modify)
  200. // 6. 当用户再次用鼠标按住圆周边缘控制点拖拽调整时
  201. this.modify.on('modifystart', (mEvt) => {
  202. // 重新绑定几何变化监听,让半径气泡文字在拖拽修改时再度跟着动起来!
  203. mEvt.features.forEach((f) => {
  204. const geom = f.getGeometry()
  205. const binder = f.get('changeBinder')
  206. if (binder) binder(geom)
  207. })
  208. })
  209. this.modify.on('modifyend', () => {
  210. console.log('圆半径二次调整结束')
  211. })
  212. })
  213. this.map.addInteraction(this.draw)
  214. } else {
  215. // 退出状态
  216. this.map.getTargetElement().style.cursor = ''
  217. }
  218. },
  219. initFeatureClick() {
  220. this.map.on('singleclick', (evt) => {
  221. // 核心:如果是地点标记状态,优先允许用户打点,不触发图标点击事件
  222. if (this.isMarking) return
  223. // 1. 获取点击处的屏幕像素坐标
  224. const pixel = this.map.getEventPixel(evt.originalEvent)
  225. // 2. 探测当前像素点下是否存在 Feature 要素
  226. const feature = this.map.forEachFeatureAtPixel(pixel, (clickedFeature) => {
  227. return clickedFeature // 如果有,直接返回这个要素
  228. })
  229. // 3. 如果抓到了要素,说明用户点中了某个图标
  230. if (feature) {
  231. // 4. 健壮性检查:通过我们在创建要素时注入的 customType 属性,判定它是不是我们要的标记点
  232. const customType = feature.get('customType')
  233. if (customType === 'api-marker') {
  234. const name = feature.get('title')
  235. const id = feature.get('id')
  236. this.$message.info(`你点击了后台标记点:${name} (ID: ${id})`)
  237. } else if (feature.getGeometry() instanceof Point && !customType) {
  238. // === 处理手动通过 markPlace 打的橙色小圆点点击 ===
  239. // 因为在 addMarker 里你没有加 customType,但它的几何类型是 Point
  240. this.$message.info('你点击了手动标记的圆点位置')
  241. // 示例:点击手动标记点时,可以把它从数据源中删掉
  242. // this.vectorSource.removeFeature(feature)
  243. }
  244. }
  245. })
  246. },
  247. // 加载标记点主方法
  248. async loadMarker() {
  249. this.initFeatureClick()
  250. // 1. 先清除之前加载过的历史标记
  251. if (this.markerSource) {
  252. this.markerSource.clear()
  253. }
  254. getMapMarks().then(resp => {
  255. if (resp.code === 0) {
  256. for (const item of resp.data) {
  257. const numLng1 = item.lng
  258. const numLat1 = item.lat
  259. // 创建要素几何(使用转换后的坐标,如果是高德底图,记得在外面套一层 fromLonLat)
  260. const finalCoord = fromLonLat([numLng1, numLat1]) // 如果底图是高德请解开这行,下面传入 finalCoord
  261. const feature = new Feature({
  262. geometry: new Point(finalCoord),
  263. customType: 'api-marker',
  264. ...item
  265. })
  266. var text = item.title + '\n[,' + numLng1.toFixed(4) + ' ' + numLat1.toFixed(4) + ']'
  267. // 设置样式
  268. feature.setStyle(
  269. new Style({
  270. // 1. 图标样式
  271. image: new Icon({
  272. src: 'https://cdn-icons-png.flaticon.com/512/684/684908.png',
  273. crossOrigin: 'anonymous',
  274. anchor: [0.5, 1], // 锚点在底部尖角,图标整体向上伸展约 36 像素
  275. scale: 0.07
  276. }),
  277. // 2. 文字样式
  278. text: new Text({
  279. text: text,
  280. font: '12px sans-serif',
  281. fill: new Fill({ color: '#3a55c2' }),
  282. stroke: new Stroke({ color: '#ffffff', width: 3 }),
  283. offsetY: -42, // 调大负数!直接把文字推到图标头顶上方(36px 图标高度 + 6px 呼吸间距)
  284. textBaseline: 'bottom', // 保持底部对齐,确保多行文本也能整齐向上排列
  285. placement: 'point'
  286. })
  287. })
  288. )
  289. // 添加到数据源
  290. this.markerSource.addFeature(feature)
  291. if (this.markerSource && this.markerSource.getFeatures().length > 0) {
  292. const extent = this.markerSource.getExtent()
  293. // 安全校验
  294. if (extent && isFinite(extent[0]) && isFinite(extent[1])) {
  295. this.map.getView().fit(extent, {
  296. padding: [50, 50, 50, 50],
  297. maxZoom: 14,
  298. duration: 800
  299. })
  300. }
  301. }
  302. }
  303. }
  304. }).catch(error => {
  305. console.error('加载标记点失败:', error.message)
  306. })
  307. },
  308. // 在地图上标记经纬度
  309. addMarker(coordinate) {
  310. // 经度(保留6位小数)
  311. const lng = coordinate[0].toFixed(6)
  312. // 纬度(保留6位小数)
  313. const lat = coordinate[1].toFixed(6)
  314. /* const lng = Number(lngStr)
  315. const lat = Number(latStr)*/
  316. // 1. 创建一个点几何
  317. const pointGeom = new Point([lng, lat])
  318. // 2. 创建一个要素(Feature),并将几何图形赋予它
  319. const markerFeature = new Feature({
  320. geometry: pointGeom
  321. })
  322. const lonLat = toLonLat(coordinate)
  323. const lng1 = lonLat[0].toFixed(6)
  324. const lat1 = lonLat[1].toFixed(6)
  325. // 格式化需要显示的文字(保留6位小数)
  326. const textLabel = `[${lng1}, ${lat1}]`
  327. // 3. 设置标记的样式(这里画一个实心圆,你也可以用 Icon 贴图)
  328. markerFeature.setStyle(
  329. new Style({
  330. text: new Text({
  331. text: textLabel, // 要显示的文字内容
  332. font: '12px sans-serif', // 字体大小和家族
  333. fill: new Fill({ color: '#333333' }), // 文字颜色
  334. stroke: new Stroke({ color: '#ffffff', width: 3 }), // 文字白边背景(防瞎,更清晰)
  335. offsetY: -12, // 改为负数,将文字向上推 12 像素
  336. textBaseline: 'bottom', // 文本基线设为底部,确保文字的屁股对齐偏移位置,不会压到圆点
  337. placement: 'point' // 渲染类型
  338. }),
  339. image: new Circle({
  340. radius: 4, // 圆半径
  341. fill: new Fill({ color: '#ff7300' }), // 填充红色
  342. stroke: new Stroke({ color: '#ffffff', width: 2 }) // 白色外边框
  343. })
  344. })
  345. )
  346. // 4. 将这个要素添加到你现有的矢量图层源中
  347. // 注意:这里建议先清除之前的标记,或者你可以专门建一个标记图层
  348. this.vectorSource.addFeature(markerFeature)
  349. // 5. 动画平移让地图中心对准这个标记点
  350. /* this.map.getView().animate({
  351. center: [lng, lat],
  352. duration: 800, // 动画持续时间(毫秒)
  353. zoom: 14 // 顺便放大地图级别
  354. })*/
  355. },
  356. creatDraw(type) {
  357. let maxPoints = null
  358. if (this.measureType === 'angle') maxPoints = 3
  359. else maxPoints = null
  360. // 矢量图层源
  361. const vectorSource = new VectorSource({
  362. wrapX: false
  363. })
  364. // 矢量图层
  365. this.vectorLayer = new VectorLayer({
  366. source: vectorSource,
  367. style: new Style({
  368. fill: new Fill({
  369. color: 'rgba(252, 86, 49, 0.1)'
  370. }),
  371. stroke: new Stroke({
  372. color: '#fc5531',
  373. width: 3
  374. }),
  375. image: new Circle({
  376. radius: 0,
  377. fill: new Fill({
  378. color: '#fc5531'
  379. })
  380. })
  381. }),
  382. name: '测量图层'
  383. })
  384. this.map.addLayer(this.vectorLayer)
  385. this.draw = new Draw({
  386. source: vectorSource,
  387. type: type,
  388. maxPoints: maxPoints,
  389. style: new Style({
  390. fill: new Fill({
  391. color: 'rgba(252, 86, 49, 0.1)'
  392. }),
  393. stroke: new Stroke({
  394. color: '#fc5531',
  395. lineDash: [10, 10],
  396. width: 3
  397. }),
  398. image: new Circle({
  399. radius: 0,
  400. fill: new Fill({
  401. color: '#fc5531'
  402. })
  403. })
  404. }),
  405. // 绘制时点击处理事件
  406. condition: (evt) => {
  407. // 测距时添加点标注
  408. if (this.measureResult !== '0' && !this.map.getOverlayById(this.measureResult) && this.measureType === 'distance') {
  409. this.creatMark(
  410. null,
  411. this.measureResult,
  412. this.measureResult
  413. ).setPosition(evt.coordinate)
  414. }
  415. return true
  416. }
  417. })
  418. this.map.addInteraction(this.draw)
  419. /**
  420. * 绘制开始事件
  421. */
  422. this.draw.on('drawstart', (e) => {
  423. this.sketchFeature = e.feature
  424. const proj = this.map.getView().getProjection()
  425. //* *****距离测量开始时*****//
  426. if (this.measureType === 'distance') {
  427. this.creatMark(null, '起点', 'start').setPosition(
  428. this.map.getCoordinateFromPixel(e.target.downPx_)
  429. )
  430. this.tipDiv.innerHTML = '总长:0 m</br>单击确定地点,双击结束'
  431. this.geometryListener = this.sketchFeature
  432. .getGeometry()
  433. .on('change', (evt) => {
  434. this.measureResult = this.distanceFormat(
  435. getLength(evt.target, { projection: proj, radius: 6378137 })
  436. )
  437. this.tipDiv.innerHTML =
  438. '总长:' + this.measureResult + '</br>单击确定地点,双击结束'
  439. })
  440. } else if (this.measureType === 'area') {
  441. //* *****面积测量开始时*****//
  442. this.tipDiv.innerHTML = '面积:0 m<sup>2</sup></br>继续单击确定地点'
  443. this.geometryListener = this.sketchFeature
  444. .getGeometry()
  445. .on('change', (evt) => {
  446. if (evt.target.getCoordinates()[0].length < 4) {
  447. this.tipDiv.innerHTML =
  448. '面积:0m<sup>2</sup></br>继续单击确定地点'
  449. } else {
  450. this.measureResult = this.formatArea(
  451. getArea(evt.target, { projection: proj, radius: 6378137 })
  452. )
  453. this.tipDiv.innerHTML =
  454. '面积:' + this.measureResult + '</br>单击确定地点,双击结束'
  455. }
  456. })
  457. } else if (this.measureType === 'angle') {
  458. //* *****角度测量开始时*****//
  459. this.tipDiv.innerHTML = '继续单击确定顶点'
  460. this.geometryListener = this.sketchFeature
  461. .getGeometry()
  462. .on('change', (evt) => {
  463. if (evt.target.getCoordinates().length < 3) { this.tipDiv.innerHTML = '继续单击确定顶点' } else {
  464. this.measureResult = this.formatAngle(evt.target)
  465. this.tipDiv.innerHTML =
  466. '角度:' + this.measureResult + '</br>继续单击结束'
  467. }
  468. })
  469. }
  470. })
  471. /**
  472. * 绘制开始事件
  473. */
  474. this.draw.on('drawend', (e) => {
  475. const closeBtn = document.createElement('span')
  476. closeBtn.innerHTML = '×'
  477. closeBtn.title = '清除测量'
  478. closeBtn.style =
  479. 'width: 10px;height:10px;line-height: 12px;text-align: center;border-radius: 5px;display: inline-block;padding: 2px;color: rgb(255, 68, 0);border: 2px solid rgb(255, 68, 0);background-color: rgb(255, 255, 255);font-weight: 600;position: absolute;top: -25px;right: -2px;cursor: pointer;'
  480. closeBtn.addEventListener('click', () => {
  481. this.clearMeasure()
  482. })
  483. //* *****距离测量结束时*****//
  484. if (this.measureType === 'distance') {
  485. this.creatMark(closeBtn, null, 'close1').setPosition(
  486. e.feature.getGeometry().getLastCoordinate()
  487. )
  488. this.creatMark(
  489. null,
  490. '总长:' + this.measureResult + '',
  491. 'length'
  492. ).setPosition(e.feature.getGeometry().getLastCoordinate())
  493. this.map.removeOverlay(this.map.getOverlayById(this.measureResult))
  494. } else if (this.measureType === 'area') {
  495. //* *****面积测量结束时*****//
  496. this.creatMark(closeBtn, null, 'close2').setPosition(
  497. e.feature.getGeometry().getInteriorPoint().getCoordinates()
  498. )
  499. this.creatMark(
  500. null,
  501. '总面积:' + this.measureResult + '',
  502. 'area'
  503. ).setPosition(
  504. e.feature.getGeometry().getInteriorPoint().getCoordinates()
  505. )
  506. } else if (this.measureType === 'angle') {
  507. //* *****角度测量结束时*****//
  508. this.creatMark(closeBtn, null, 'close3').setPosition(
  509. e.feature.getGeometry().getCoordinates()[1]
  510. )
  511. this.creatMark(
  512. null,
  513. '角度:' + this.measureResult + '',
  514. 'angle'
  515. ).setPosition(e.feature.getGeometry().getCoordinates()[1])
  516. }
  517. // 停止测量
  518. this.stopMeasure()
  519. })
  520. },
  521. markPlace() {
  522. // 切换状态
  523. this.isMarking = !this.isMarking
  524. if (this.isMarking) {
  525. // 1. 改变鼠标指针为十字准星
  526. this.map.getTargetElement().style.cursor = 'crosshair'
  527. // 2. 定义点击时的执行函数
  528. const handleMapClick = (evt) => {
  529. // 获取点击位置的原始墨卡托坐标(高德地图) [米, 米]
  530. // evt.coordinate 是一个数组:[lng, lat]
  531. const coordinate = evt.coordinate
  532. this.addMarker(coordinate)
  533. }
  534. // 3. 绑定持久点击事件,并把返回的 key 存起来以便后续解绑
  535. this.clickListenerKey = this.map.on('singleclick', handleMapClick)
  536. } else {
  537. // ================= 退出标记地点状态 =================
  538. // 1. 恢复默认鼠标指针
  539. this.map.getTargetElement().style.cursor = ''
  540. // 2. 核心:解除绑定的点击事件,让地图恢复正常
  541. if (this.clickListenerKey) {
  542. // OpenLayers 6+ 中解绑事件的方式:使用 ol/Observable 的 unByKey 或者直接传入 key
  543. // 如果你的版本支持, 可以直接使用下面的标准方法:
  544. this.map.un('singleclick', this.clickListenerKey.listener)
  545. this.clickListenerKey = null
  546. }
  547. }
  548. },
  549. /**
  550. * 测量
  551. */
  552. measure(type) {
  553. if (this.draw !== null) return false // 防止在绘制过程再创建测量
  554. this.measureType = type
  555. if (this.vectorLayer !== null) this.clearMeasure()
  556. this.tipDiv = document.createElement('div')
  557. this.tipDiv.innerHTML = '单击确定起点'
  558. this.tipDiv.className = 'tipDiv'
  559. this.tipDiv.style = 'width:auto;height:auto;padding:4px;border:1px solid #fc5531;font-size:12px;background-color:#fff;position:relative;top:60%;left:60%;font-weight:600;'
  560. const overlay = new Overlay({
  561. element: this.tipDiv,
  562. autoPan: false,
  563. positioning: 'bottom-center',
  564. id: 'tipLay',
  565. stopEvent: false // 停止事件传播到地图
  566. })
  567. this.map.addOverlay(overlay)
  568. this.pointermoveEvent = this.map.on('pointermove', (evt) => {
  569. overlay.setPosition(evt.coordinate)
  570. })
  571. if (this.measureType === 'distance' || this.measureType === 'angle') {
  572. this.creatDraw('LineString')
  573. } else if (this.measureType === 'area') {
  574. this.creatDraw('Polygon')
  575. }
  576. },
  577. /**
  578. * 创建标记
  579. */
  580. creatMark(markDom, txt, idstr) {
  581. if (markDom === null) {
  582. markDom = document.createElement('div')
  583. markDom.innerHTML = txt
  584. markDom.style =
  585. 'width:auto;height:auto;padding:4px;border:1px solid #fc5531;font-size:12px;background-color:#fff;position:relative;top:60%;left:60%;font-weight:600;'
  586. }
  587. const overlay = new Overlay({
  588. element: markDom,
  589. autoPan: false,
  590. positioning: 'bottom-center',
  591. id: idstr,
  592. stopEvent: false
  593. })
  594. this.map.addOverlay(overlay)
  595. return overlay
  596. },
  597. /**
  598. * 格式化距离结果输出
  599. */
  600. distanceFormat(length) {
  601. let output
  602. if (length > 100) {
  603. output = Math.round((length / 1000) * 100) / 100 + ' ' + 'km' // 换算成km单位
  604. } else {
  605. output = Math.round(length * 100) / 100 + ' ' + 'm' // m为单位
  606. }
  607. return output // 返回线的长度
  608. },
  609. /**
  610. * 格式化面积输出
  611. */
  612. formatArea(area) {
  613. let output
  614. if (area > 10000) {
  615. output =
  616. Math.round((area / 1000000) * 100) / 100 + ' ' + 'km<sup>2</sup>' // 换算成km单位
  617. } else {
  618. output = Math.round(area * 100) / 100 + ' ' + 'm<sup>2</sup>' // m为单位
  619. }
  620. return output // 返回多边形的面积
  621. },
  622. /**
  623. * 计算角度输出
  624. */
  625. formatAngle(line) {
  626. var coordinates = line.getCoordinates()
  627. var angle = '0°'
  628. if (coordinates.length === 3) {
  629. const disa = getLength(
  630. new Feature({
  631. geometry: new LineString([coordinates[0], coordinates[1]])
  632. }).getGeometry(),
  633. {
  634. radius: 6378137,
  635. projection: this.map.getView().getProjection()
  636. }
  637. )
  638. const disb = getLength(
  639. new Feature({
  640. geometry: new LineString([coordinates[1], coordinates[2]])
  641. }).getGeometry(),
  642. {
  643. radius: 6378137,
  644. projection: this.map.getView().getProjection()
  645. }
  646. )
  647. const disc = getLength(
  648. new Feature({
  649. geometry: new LineString([coordinates[0], coordinates[2]])
  650. }).getGeometry(),
  651. {
  652. radius: 6378137,
  653. projection: this.map.getView().getProjection()
  654. }
  655. )
  656. var cos = (disa * disa + disb * disb - disc * disc) / (2 * disa * disb) // 计算cos值
  657. angle = (Math.acos(cos) * 180) / Math.PI // 角度值
  658. angle = angle.toFixed(2) // 结果保留两位小数
  659. }
  660. if (isNaN(angle)) return '0°'
  661. else return angle + '°' // 返回角度
  662. },
  663. /**
  664. * 停止测量
  665. */
  666. stopMeasure() {
  667. this.tipDiv = null
  668. this.map.removeInteraction(this.draw) // 移除绘制组件
  669. this.draw = null
  670. this.map.removeOverlay(this.map.getOverlayById('tipLay')) // 移除动态提示框
  671. },
  672. /**
  673. * 清除测量
  674. */
  675. clearMeasure() {
  676. this.vectorLayer.getSource().clear()
  677. this.map.getOverlays().clear()
  678. // 移除监听事件
  679. unByKey(this.pointermoveEvent) // 清除鼠标在地图的pointermove事件
  680. unByKey(this.geometryListener) // 清除绘制图像change事件
  681. this.pointermoveEvent = null
  682. this.geometryListener = null
  683. this.measureResult = '0'
  684. },
  685. // 坐标
  686. single() {
  687. this.map.on('singleclick', function(e) {
  688. console.log(e.coordinate)
  689. })
  690. },
  691. // 清除
  692. clear() {
  693. // 清空测量矢量图层
  694. if (this.vectorLayer) this.vectorLayer.getSource().clear()
  695. // 清空后台加载的标记图层
  696. if (this.markerSource) this.markerSource.clear()
  697. // 清空所有弹窗 Overlay
  698. this.map.getOverlays().clear()
  699. }
  700. }
  701. }
  702. </script>
  703. <style scoped>
  704. #map {
  705. position: absolute;
  706. top: 0;
  707. left: 0;
  708. width: 100%;
  709. height: 100%;
  710. overflow: hidden;
  711. }
  712. /* 右上角工具箱容器 */
  713. .toolbox {
  714. position: absolute;
  715. z-index: 999;
  716. top: 20px; /* 距离顶部 20px */
  717. right: 20px; /* 距离右侧 20px */
  718. background: rgba(255, 255, 255, 0.9); /* 半透明白底背景 */
  719. padding: 8px;
  720. border-radius: 6px; /* 圆角 */
  721. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.15); /* 地图控件常用的阴影 */
  722. display: grid;
  723. grid-template-columns: repeat(2, 75px); /* 固定为两列,每列宽 75px */
  724. gap: 6px; /* 按钮之间的间距 */
  725. }
  726. /* 工具箱内的按钮通用样式 */
  727. .toolbox button {
  728. height: 28px;
  729. border: 1px solid #dcdfe6;
  730. background-color: #fff;
  731. color: #606266;
  732. font-size: 12px;
  733. border-radius: 4px;
  734. cursor: pointer;
  735. transition: all 0.2s;
  736. padding: 0 4px;
  737. }
  738. /* 鼠标悬浮和点击效果 */
  739. .toolbox button:hover {
  740. color: #fc5531;
  741. border-color: #fc5531;
  742. background-color: rgba(252, 86, 49, 0.05);
  743. }
  744. /* 实时跟随鼠标的半径标签气泡样式 */
  745. .ol-radius-marker {
  746. background: rgba(64, 158, 255, 0.95); /* 漂亮的科技蓝 */
  747. color: #ffffff;
  748. padding: 4px 8px;
  749. border-radius: 4px;
  750. font-size: 11px;
  751. font-weight: bold;
  752. white-space: nowrap;
  753. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  754. border: 1px solid #ffffff;
  755. position: relative;
  756. pointer-events: none; /* 极其重要:确保 DOM 穿透,否则会挡住鼠标导致无法继续绘制 */
  757. }
  758. /* 气泡下方的微型小尖角 */
  759. .ol-radius-marker::after {
  760. content: "";
  761. position: absolute;
  762. bottom: -4px;
  763. left: 10px;
  764. border-width: 4px 4px 0;
  765. border-style: solid;
  766. border-color: rgba(64, 158, 255, 0.95) transparent;
  767. }
  768. </style>