|
|
@@ -1,17 +1,27 @@
|
|
|
<template>
|
|
|
- <!-- 绘制、编辑、输出GeoJson数据 -->
|
|
|
<div id="map" ref="map">
|
|
|
- <div class="btns">
|
|
|
- <button @click="drawFeature">绘制</button>
|
|
|
- <button @click="editorFeature" v-text="editorBtnText" />
|
|
|
- <button @click="outputJson">输出Json</button>
|
|
|
- <button @click="measure('distence')">距</button>
|
|
|
- <button @click="measure('area')">面</button>
|
|
|
- <button @click="measure('angle')">角</button>
|
|
|
- <button @click="fullscreen" v-html="textarr" />
|
|
|
- <button @click="single">坐标</button>
|
|
|
+ <div class="toolbox">
|
|
|
+ <button @click="loadMarker">加载标记</button>
|
|
|
+ <button
|
|
|
+ :style="{ backgroundColor: isMarking ? '#ff4d4f' : '', color: isMarking ? '#fff' : '' }"
|
|
|
+ @click="markPlace"
|
|
|
+ >
|
|
|
+ {{ isMarking ? '结束标记' : '标记地点' }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ :style="{ backgroundColor: isDrawingCircle ? '#409EFF' : '', color: isDrawingCircle ? '#fff' : '' }"
|
|
|
+ @click="drawCircle"
|
|
|
+ >
|
|
|
+ {{ isDrawingCircle ? '结束画圆' : '绘制圆形' }}
|
|
|
+ </button>
|
|
|
+ <button @click="measure('distance')">测距离</button>
|
|
|
+ <button @click="measure('area')">测面积</button>
|
|
|
<button @click="clear">清除</button>
|
|
|
</div>
|
|
|
+
|
|
|
+ <div ref="radiusOverlayLabel" class="ol-radius-marker" style="display: none;">
|
|
|
+ {{ liveRadiusText }}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -21,16 +31,13 @@ import Map from 'ol/Map'
|
|
|
import View from 'ol/View'
|
|
|
import { Vector as VectorSource, OSM, XYZ } from 'ol/source'
|
|
|
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
|
|
|
-import { GeoJSON } from 'ol/format'
|
|
|
-// 交互 Select在选择功能时触发。Modify用于修改功能几何的交互。Draw绘制功能几何的交互。
|
|
|
-import { Draw, Modify, Select } from 'ol/interaction'
|
|
|
-
|
|
|
+// Draw 绘制功能几何的交互
|
|
|
+import { Draw, Modify } from 'ol/interaction'
|
|
|
import Overlay from 'ol/Overlay'
|
|
|
-// 线条几何形状。
|
|
|
+// 线条几何形状
|
|
|
import { LineString } from 'ol/geom'
|
|
|
-// 具有几何和其他属性属性属性的地理特征的矢量对象,类似于像 GeoJSON 这样的矢量文件格式中的特征。
|
|
|
+// 具有几何和其他属性属性属性的地理特征的矢量对象,类似于像 GeoJSON 这样的矢量文件格式中的特征
|
|
|
import Feature from 'ol/Feature'
|
|
|
-// 使用返回的键或
|
|
|
import { unByKey } from 'ol/Observable'
|
|
|
// 获取几何形状的球形长度和面积
|
|
|
import { getLength, getArea } from 'ol/sphere'
|
|
|
@@ -38,51 +45,51 @@ import Style from 'ol/style/Style'
|
|
|
import Stroke from 'ol/style/Stroke'
|
|
|
import Fill from 'ol/style/Fill'
|
|
|
import Circle from 'ol/style/Circle'
|
|
|
-import { fromLonLat } from 'ol/proj'
|
|
|
-// import Icon from "ol/style/Icon";
|
|
|
-// import { transform } from "ol/proj";
|
|
|
-// import Point from "ol/geom/Point";
|
|
|
+import { fromLonLat, toLonLat } from 'ol/proj'
|
|
|
+import Icon from 'ol/style/Icon'
|
|
|
+import Point from 'ol/geom/Point'
|
|
|
+import Text from 'ol/style/Text'
|
|
|
+import { getMapMarks } from '@/api/map'
|
|
|
|
|
|
export default {
|
|
|
name: 'OpenLayersMap',
|
|
|
data() {
|
|
|
return {
|
|
|
+ center: [104.068071, 30.576432],
|
|
|
+ zoom: 12, // 比例尺 10 公里
|
|
|
map: null,
|
|
|
vectorLayer: null,
|
|
|
vectorSource: null,
|
|
|
+ // 标记地点
|
|
|
+ isMarking: false,
|
|
|
+ clickListenerKey: null,
|
|
|
+ // --- 新增:绘制圆形相关变量 ---
|
|
|
+ isDrawingCircle: false, // 是否处于画圆状态
|
|
|
+ liveRadiusText: '', // 实时半径文字
|
|
|
+ radiusOverlay: null, // 存放半径文本的 Overlay 实例
|
|
|
draw: null,
|
|
|
select: null,
|
|
|
modify: null,
|
|
|
- editorBtnText: '编辑',
|
|
|
- textarr: '全屏',
|
|
|
- measureType: 'diatence',
|
|
|
+ measureType: 'distance',
|
|
|
tipDiv: null,
|
|
|
pointermoveEvent: null, // 地图pointermove事件
|
|
|
sketchFeature: null, // 绘制的要素
|
|
|
geometryListener: null, // 要素几何change事件
|
|
|
- measureResult: '0' // 测量结果
|
|
|
+ measureResult: '0', // 测量结果
|
|
|
+ markerSource: null // 新增:专门存放后台加载的标记点
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
+ this.markerSource = new VectorSource({ wrapX: false })
|
|
|
+ const markerLayer = new VectorLayer({
|
|
|
+ source: this.markerSource,
|
|
|
+ name: '后台标记图层'
|
|
|
+ })
|
|
|
+
|
|
|
// 底图使用 OpenStreetMap, 对应 view
|
|
|
var baseLayer = new TileLayer({
|
|
|
source: new OSM()
|
|
|
})
|
|
|
- // 谷歌地球, 对应 view1
|
|
|
- var baseLayer1 = new TileLayer({
|
|
|
- source: new XYZ({
|
|
|
- url: 'https://www.google.com/maps/vt?lyrs=y&gl=cn&x={x}&y={y}&z={z}',
|
|
|
- crossOrigin: 'anonymous',
|
|
|
- wrapX: true
|
|
|
- })
|
|
|
- })
|
|
|
- // 谷歌地图, 对应 view2
|
|
|
- var baseLayer2 = new TileLayer({
|
|
|
- source: new XYZ({
|
|
|
- url: 'http://www.google.cn/maps/vt/pb=!1m4!1m3!1i{z}!2i{x}!3i{y}!2m3!1e0!2sm!3i345013117!3m8!2szh-CN!3scn!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0'
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
// 矢量图层源
|
|
|
this.vectorSource = new VectorSource({
|
|
|
wrapX: false
|
|
|
@@ -92,86 +99,296 @@ export default {
|
|
|
source: this.vectorSource
|
|
|
})
|
|
|
|
|
|
- var view = new View({
|
|
|
+ var url = 'https://webrd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}'
|
|
|
+ // 切换为高德地图的电子地图底图
|
|
|
+ var amapLayer = new TileLayer({
|
|
|
+ source: new XYZ({
|
|
|
+ // 高德官方切片地址(t1-t4可选,lang=zh_cn代表中文,style=7代表标准地图)
|
|
|
+ url: url,
|
|
|
+ crossOrigin: 'anonymous',
|
|
|
+ maxZoom: 18
|
|
|
+ }),
|
|
|
+ name: '高德底图'
|
|
|
+ })
|
|
|
+ /* this.map = new Map({
|
|
|
+ layers: [baseLayer, this.vectorLayer, markerLayer],
|
|
|
+ target: 'map',
|
|
|
+ view: new View({
|
|
|
projection: 'EPSG:4326',
|
|
|
center: [104.06531800244139, 30.65852484539117],
|
|
|
zoom: 10
|
|
|
})
|
|
|
- var view1 = new View({
|
|
|
- projection: 'EPSG:3857', // 坐标系EPSG:4326或EPSG:3857
|
|
|
- zoom: 0, // 打开页面时默认地图缩放级别
|
|
|
- center: fromLonLat([121.5, 25]) // 转到墨卡托坐标系
|
|
|
- })
|
|
|
- var view2 = new View({
|
|
|
- center: [104.06531800244139, 30.65852484539117],
|
|
|
- zoom: 12,
|
|
|
- maxZoom: 18,
|
|
|
- projection: 'EPSG:4326',
|
|
|
- constrainResolution: true, // 设置缩放级别为整数
|
|
|
- smoothResolutionConstraint: false // 关闭无级缩放地图
|
|
|
- })
|
|
|
+ })*/
|
|
|
|
|
|
this.map = new Map({
|
|
|
- layers: [baseLayer, this.vectorLayer],
|
|
|
+ layers: [amapLayer, this.vectorLayer, markerLayer], // 替换掉 baseLayer
|
|
|
target: 'map',
|
|
|
- view: view
|
|
|
+ view: new View({
|
|
|
+ projection: 'EPSG:3857', // ⚠️注意:高德切片是墨卡托(3857)投影,建议View也改为3857
|
|
|
+ center: fromLonLat(this.center), // 用 fromLonLat 转换中心点
|
|
|
+ zoom: 12,
|
|
|
+ maxZoom: 18
|
|
|
+ })
|
|
|
})
|
|
|
},
|
|
|
created() {
|
|
|
document.title = 'OpenLayersMap'
|
|
|
},
|
|
|
methods: {
|
|
|
- drawFeature() {
|
|
|
- this.draw = new Draw({
|
|
|
- source: this.vectorSource,
|
|
|
- // 用此实例绘制的几何形状类型。
|
|
|
- type: 'Polygon'
|
|
|
- })
|
|
|
- // addInteraction 将给定的交互添加到地图中
|
|
|
- this.map.addInteraction(this.draw)
|
|
|
- // 绘制完成
|
|
|
- this.draw.on('drawend', () => {
|
|
|
- // removeInteraction 从地图中删除给定的交互。
|
|
|
- this.map.removeInteraction(this.draw)
|
|
|
- this.draw = null
|
|
|
- })
|
|
|
- },
|
|
|
+ drawCircle() {
|
|
|
+ this.isDrawingCircle = !this.isDrawingCircle
|
|
|
+
|
|
|
+ // 清理上一次的组件
|
|
|
+ if (this.draw) this.map.removeInteraction(this.draw)
|
|
|
+ if (this.modify) this.map.removeInteraction(this.modify)
|
|
|
+ if (this.radiusOverlay) this.map.removeOverlay(this.radiusOverlay)
|
|
|
+
|
|
|
+ if (this.isDrawingCircle) {
|
|
|
+ this.map.getTargetElement().style.cursor = 'crosshair'
|
|
|
|
|
|
- editorFeature() {
|
|
|
- if (this.editorBtnText === '编辑') {
|
|
|
- this.editorBtnText = '完成编辑'
|
|
|
- // 要素选择组件
|
|
|
- this.select = new Select({
|
|
|
- // wrapX 将世界水平包裹在草图覆盖物上。
|
|
|
- wrapX: false
|
|
|
+ // 1. 创建半径气泡标签 DOM
|
|
|
+ const labelEl = document.createElement('div')
|
|
|
+ labelEl.className = 'ol-radius-marker'
|
|
|
+
|
|
|
+ this.radiusOverlay = new Overlay({
|
|
|
+ element: labelEl,
|
|
|
+ offset: [15, -15],
|
|
|
+ positioning: 'bottom-left',
|
|
|
+ stopEvent: false
|
|
|
+ })
|
|
|
+ this.map.addOverlay(this.radiusOverlay)
|
|
|
+
|
|
|
+ // 2. 初始化绘制器
|
|
|
+ this.draw = new Draw({
|
|
|
+ source: this.vectorSource,
|
|
|
+ type: 'Circle'
|
|
|
})
|
|
|
- // 要素编辑
|
|
|
- this.modify = new Modify({
|
|
|
- // getFeatures 随机获取源上当前功能的快照。返回的阵列是副本,功能是源中功能的引用。
|
|
|
- features: this.select.getFeatures()
|
|
|
+
|
|
|
+ let centerFeature = null
|
|
|
+
|
|
|
+ // 3. 监听开始绘制
|
|
|
+ this.draw.on('drawstart', (evt) => {
|
|
|
+ const geometry = evt.feature.getGeometry()
|
|
|
+
|
|
|
+ // 绘制圆心红点
|
|
|
+ const initialCenter = geometry.getCenter()
|
|
|
+ centerFeature = new Feature({ geometry: new Point(initialCenter) })
|
|
|
+ centerFeature.setStyle(new Style({
|
|
|
+ image: new Circle({
|
|
|
+ radius: 5,
|
|
|
+ fill: new Fill({ color: '#ff4d4f' }),
|
|
|
+ stroke: new Stroke({ color: '#ffffff', width: 2 })
|
|
|
+ })
|
|
|
+ }))
|
|
|
+ this.vectorSource.addFeature(centerFeature)
|
|
|
+
|
|
|
+ // 绑定高频拖拽计算半径事件
|
|
|
+ const bindGeometryChange = (geom) => {
|
|
|
+ geom.on('change', () => {
|
|
|
+ const radiusInMeters = geom.getRadius()
|
|
|
+ const text = radiusInMeters < 1000 ? `${radiusInMeters.toFixed(1)}m` : `${(radiusInMeters / 1000).toFixed(2)}km`
|
|
|
+ labelEl.innerText = text
|
|
|
+
|
|
|
+ // 标签死死跟着圆周上的鼠标/控制点边缘
|
|
|
+ const lastCoordinate = geom.getLastCoordinate()
|
|
|
+ if (this.radiusOverlay) {
|
|
|
+ this.radiusOverlay.setPosition(lastCoordinate)
|
|
|
+ }
|
|
|
+ // 同步纠正圆心物理位置
|
|
|
+ if (centerFeature) centerFeature.getGeometry().setCoordinates(geom.getCenter())
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ bindGeometryChange(geometry)
|
|
|
+ // 将这个绑定函数挂载到 feature 上,方便后期二次编辑修改时复用
|
|
|
+ evt.feature.set('changeBinder', bindGeometryChange)
|
|
|
})
|
|
|
|
|
|
- // 编辑完成后
|
|
|
- this.modify.on('modifyend', function(e) {
|
|
|
- console.log('编辑后的要素:', e.features)
|
|
|
+ // 4. 核心新增:监听绘制结束,立刻无缝移交控制权给 Modify
|
|
|
+ this.draw.on('drawend', (evt) => {
|
|
|
+ // 移除画圆交互(不让继续画新圆了),但保留图形和标签
|
|
|
+ this.map.removeInteraction(this.draw)
|
|
|
+ this.map.getTargetElement().style.cursor = 'default'
|
|
|
+
|
|
|
+ const finishedFeature = evt.feature
|
|
|
+
|
|
|
+ // 5. 激活修改交互器(传入指定的图层源)
|
|
|
+ this.modify = new Modify({
|
|
|
+ source: this.vectorSource
|
|
|
+ })
|
|
|
+ this.map.addInteraction(this.modify)
|
|
|
+
|
|
|
+ // 6. 当用户再次用鼠标按住圆周边缘控制点拖拽调整时
|
|
|
+ this.modify.on('modifystart', (mEvt) => {
|
|
|
+ // 重新绑定几何变化监听,让半径气泡文字在拖拽修改时再度跟着动起来!
|
|
|
+ mEvt.features.forEach((f) => {
|
|
|
+ const geom = f.getGeometry()
|
|
|
+ const binder = f.get('changeBinder')
|
|
|
+ if (binder) binder(geom)
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ this.modify.on('modifyend', () => {
|
|
|
+ console.log('圆半径二次调整结束')
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
- this.map.addInteraction(this.select)
|
|
|
- this.map.addInteraction(this.modify)
|
|
|
+ this.map.addInteraction(this.draw)
|
|
|
} else {
|
|
|
- this.editorBtnText = '编辑'
|
|
|
- this.map.removeInteraction(this.select)
|
|
|
- this.map.removeInteraction(this.modify)
|
|
|
- this.select = null
|
|
|
- this.modify = null
|
|
|
+ // 退出状态
|
|
|
+ this.map.getTargetElement().style.cursor = ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ initFeatureClick() {
|
|
|
+ this.map.on('singleclick', (evt) => {
|
|
|
+ // 核心:如果是地点标记状态,优先允许用户打点,不触发图标点击事件
|
|
|
+ if (this.isMarking) return
|
|
|
+
|
|
|
+ // 1. 获取点击处的屏幕像素坐标
|
|
|
+ const pixel = this.map.getEventPixel(evt.originalEvent)
|
|
|
+
|
|
|
+ // 2. 探测当前像素点下是否存在 Feature 要素
|
|
|
+ const feature = this.map.forEachFeatureAtPixel(pixel, (clickedFeature) => {
|
|
|
+ return clickedFeature // 如果有,直接返回这个要素
|
|
|
+ })
|
|
|
+
|
|
|
+ // 3. 如果抓到了要素,说明用户点中了某个图标
|
|
|
+ if (feature) {
|
|
|
+ // 4. 健壮性检查:通过我们在创建要素时注入的 customType 属性,判定它是不是我们要的标记点
|
|
|
+ const customType = feature.get('customType')
|
|
|
+ if (customType === 'api-marker') {
|
|
|
+ const name = feature.get('title')
|
|
|
+ const id = feature.get('id')
|
|
|
+ this.$message.info(`你点击了后台标记点:${name} (ID: ${id})`)
|
|
|
+ } else if (feature.getGeometry() instanceof Point && !customType) {
|
|
|
+ // === 处理手动通过 markPlace 打的橙色小圆点点击 ===
|
|
|
+ // 因为在 addMarker 里你没有加 customType,但它的几何类型是 Point
|
|
|
+ this.$message.info('你点击了手动标记的圆点位置')
|
|
|
+ // 示例:点击手动标记点时,可以把它从数据源中删掉
|
|
|
+ // this.vectorSource.removeFeature(feature)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 加载标记点主方法
|
|
|
+ async loadMarker() {
|
|
|
+ this.initFeatureClick()
|
|
|
+ // 1. 先清除之前加载过的历史标记
|
|
|
+ if (this.markerSource) {
|
|
|
+ this.markerSource.clear()
|
|
|
}
|
|
|
+
|
|
|
+ getMapMarks().then(resp => {
|
|
|
+ if (resp.code === 0) {
|
|
|
+ for (const item of resp.data) {
|
|
|
+ const numLng1 = item.position.lng
|
|
|
+ const numLat1 = item.position.lat
|
|
|
+ // 创建要素几何(使用转换后的坐标,如果是高德底图,记得在外面套一层 fromLonLat)
|
|
|
+ const finalCoord = fromLonLat([numLng1, numLat1]) // 如果底图是高德请解开这行,下面传入 finalCoord
|
|
|
+ const feature = new Feature({
|
|
|
+ geometry: new Point(finalCoord),
|
|
|
+ customType: 'api-marker',
|
|
|
+ ...item
|
|
|
+ })
|
|
|
+
|
|
|
+ var text = item.title + '\n[,' + numLng1.toFixed(4) + ' ' + numLat1.toFixed(4) + ']'
|
|
|
+ // 设置样式
|
|
|
+ feature.setStyle(
|
|
|
+ new Style({
|
|
|
+ // 1. 图标样式
|
|
|
+ image: new Icon({
|
|
|
+ src: 'https://cdn-icons-png.flaticon.com/512/684/684908.png',
|
|
|
+ crossOrigin: 'anonymous',
|
|
|
+ anchor: [0.5, 1], // 锚点在底部尖角,图标整体向上伸展约 36 像素
|
|
|
+ scale: 0.07
|
|
|
+ }),
|
|
|
+
|
|
|
+ // 2. 文字样式
|
|
|
+ text: new Text({
|
|
|
+ text: text,
|
|
|
+ font: '12px sans-serif',
|
|
|
+ fill: new Fill({ color: '#3a55c2' }),
|
|
|
+ stroke: new Stroke({ color: '#ffffff', width: 3 }),
|
|
|
+ offsetY: -42, // 调大负数!直接把文字推到图标头顶上方(36px 图标高度 + 6px 呼吸间距)
|
|
|
+ textBaseline: 'bottom', // 保持底部对齐,确保多行文本也能整齐向上排列
|
|
|
+ placement: 'point'
|
|
|
+ })
|
|
|
+ })
|
|
|
+ )
|
|
|
+
|
|
|
+ // 添加到数据源
|
|
|
+ this.markerSource.addFeature(feature)
|
|
|
+ if (this.markerSource && this.markerSource.getFeatures().length > 0) {
|
|
|
+ const extent = this.markerSource.getExtent()
|
|
|
+
|
|
|
+ // 安全校验
|
|
|
+ if (extent && isFinite(extent[0]) && isFinite(extent[1])) {
|
|
|
+ this.map.getView().fit(extent, {
|
|
|
+ padding: [50, 50, 50, 50],
|
|
|
+ maxZoom: 14,
|
|
|
+ duration: 800
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }).catch(error => {
|
|
|
+ console.error('加载标记点失败:', error.message)
|
|
|
+ })
|
|
|
},
|
|
|
+ // 在地图上标记经纬度
|
|
|
+ addMarker(coordinate) {
|
|
|
+ // 经度(保留6位小数)
|
|
|
+ const lng = coordinate[0].toFixed(6)
|
|
|
+ // 纬度(保留6位小数)
|
|
|
+ const lat = coordinate[1].toFixed(6)
|
|
|
+ /* const lng = Number(lngStr)
|
|
|
+ const lat = Number(latStr)*/
|
|
|
+
|
|
|
+ // 1. 创建一个点几何
|
|
|
+ const pointGeom = new Point([lng, lat])
|
|
|
|
|
|
- // 输出矢量图层要素为GeoJson数据
|
|
|
- outputJson() {
|
|
|
- const features = this.vectorSource.getFeatures()
|
|
|
- const jsonObj = new GeoJSON().writeFeatures(features)
|
|
|
- console.log('->GeoJson格式数据:', jsonObj)
|
|
|
+ // 2. 创建一个要素(Feature),并将几何图形赋予它
|
|
|
+ const markerFeature = new Feature({
|
|
|
+ geometry: pointGeom
|
|
|
+ })
|
|
|
+
|
|
|
+ const lonLat = toLonLat(coordinate)
|
|
|
+ const lng1 = lonLat[0].toFixed(6)
|
|
|
+ const lat1 = lonLat[1].toFixed(6)
|
|
|
+ // 格式化需要显示的文字(保留6位小数)
|
|
|
+ const textLabel = `[${lng1}, ${lat1}]`
|
|
|
+
|
|
|
+ // 3. 设置标记的样式(这里画一个实心圆,你也可以用 Icon 贴图)
|
|
|
+ markerFeature.setStyle(
|
|
|
+ new Style({
|
|
|
+ text: new Text({
|
|
|
+ text: textLabel, // 要显示的文字内容
|
|
|
+ font: '12px sans-serif', // 字体大小和家族
|
|
|
+ fill: new Fill({ color: '#333333' }), // 文字颜色
|
|
|
+ stroke: new Stroke({ color: '#ffffff', width: 3 }), // 文字白边背景(防瞎,更清晰)
|
|
|
+ offsetY: -12, // 改为负数,将文字向上推 12 像素
|
|
|
+ textBaseline: 'bottom', // 文本基线设为底部,确保文字的屁股对齐偏移位置,不会压到圆点
|
|
|
+ placement: 'point' // 渲染类型
|
|
|
+ }),
|
|
|
+ image: new Circle({
|
|
|
+ radius: 4, // 圆半径
|
|
|
+ fill: new Fill({ color: '#ff7300' }), // 填充红色
|
|
|
+ stroke: new Stroke({ color: '#ffffff', width: 2 }) // 白色外边框
|
|
|
+ })
|
|
|
+ })
|
|
|
+ )
|
|
|
+
|
|
|
+ // 4. 将这个要素添加到你现有的矢量图层源中
|
|
|
+ // 注意:这里建议先清除之前的标记,或者你可以专门建一个标记图层
|
|
|
+ this.vectorSource.addFeature(markerFeature)
|
|
|
+
|
|
|
+ // 5. 动画平移让地图中心对准这个标记点
|
|
|
+ /* this.map.getView().animate({
|
|
|
+ center: [lng, lat],
|
|
|
+ duration: 800, // 动画持续时间(毫秒)
|
|
|
+ zoom: 14 // 顺便放大地图级别
|
|
|
+ })*/
|
|
|
},
|
|
|
creatDraw(type) {
|
|
|
let maxPoints = null
|
|
|
@@ -226,11 +443,7 @@ export default {
|
|
|
// 绘制时点击处理事件
|
|
|
condition: (evt) => {
|
|
|
// 测距时添加点标注
|
|
|
- if (
|
|
|
- this.measureResult != '0' &&
|
|
|
- !this.map.getOverlayById(this.measureResult) &&
|
|
|
- this.measureType === 'distence'
|
|
|
- ) {
|
|
|
+ if (this.measureResult !== '0' && !this.map.getOverlayById(this.measureResult) && this.measureType === 'distance') {
|
|
|
this.creatMark(
|
|
|
null,
|
|
|
this.measureResult,
|
|
|
@@ -249,7 +462,7 @@ export default {
|
|
|
this.sketchFeature = e.feature
|
|
|
const proj = this.map.getView().getProjection()
|
|
|
//* *****距离测量开始时*****//
|
|
|
- if (this.measureType === 'distence') {
|
|
|
+ if (this.measureType === 'distance') {
|
|
|
this.creatMark(null, '起点', 'start').setPosition(
|
|
|
this.map.getCoordinateFromPixel(e.target.downPx_)
|
|
|
)
|
|
|
@@ -257,15 +470,14 @@ export default {
|
|
|
this.geometryListener = this.sketchFeature
|
|
|
.getGeometry()
|
|
|
.on('change', (evt) => {
|
|
|
- this.measureResult = this.distenceFormat(
|
|
|
+ this.measureResult = this.distanceFormat(
|
|
|
getLength(evt.target, { projection: proj, radius: 6378137 })
|
|
|
)
|
|
|
this.tipDiv.innerHTML =
|
|
|
'总长:' + this.measureResult + '</br>单击确定地点,双击结束'
|
|
|
})
|
|
|
- }
|
|
|
- //* *****面积测量开始时*****//
|
|
|
- else if (this.measureType === 'area') {
|
|
|
+ } else if (this.measureType === 'area') {
|
|
|
+ //* *****面积测量开始时*****//
|
|
|
this.tipDiv.innerHTML = '面积:0 m<sup>2</sup></br>继续单击确定地点'
|
|
|
this.geometryListener = this.sketchFeature
|
|
|
.getGeometry()
|
|
|
@@ -281,9 +493,8 @@ export default {
|
|
|
'面积:' + this.measureResult + '</br>单击确定地点,双击结束'
|
|
|
}
|
|
|
})
|
|
|
- }
|
|
|
- //* *****角度测量开始时*****//
|
|
|
- else if (this.measureType === 'angle') {
|
|
|
+ } else if (this.measureType === 'angle') {
|
|
|
+ //* *****角度测量开始时*****//
|
|
|
this.tipDiv.innerHTML = '继续单击确定顶点'
|
|
|
this.geometryListener = this.sketchFeature
|
|
|
.getGeometry()
|
|
|
@@ -310,7 +521,7 @@ export default {
|
|
|
this.clearMeasure()
|
|
|
})
|
|
|
//* *****距离测量结束时*****//
|
|
|
- if (this.measureType === 'distence') {
|
|
|
+ if (this.measureType === 'distance') {
|
|
|
this.creatMark(closeBtn, null, 'close1').setPosition(
|
|
|
e.feature.getGeometry().getLastCoordinate()
|
|
|
)
|
|
|
@@ -320,9 +531,8 @@ export default {
|
|
|
'length'
|
|
|
).setPosition(e.feature.getGeometry().getLastCoordinate())
|
|
|
this.map.removeOverlay(this.map.getOverlayById(this.measureResult))
|
|
|
- }
|
|
|
- //* *****面积测量结束时*****//
|
|
|
- else if (this.measureType === 'area') {
|
|
|
+ } else if (this.measureType === 'area') {
|
|
|
+ //* *****面积测量结束时*****//
|
|
|
this.creatMark(closeBtn, null, 'close2').setPosition(
|
|
|
e.feature.getGeometry().getInteriorPoint().getCoordinates()
|
|
|
)
|
|
|
@@ -333,9 +543,8 @@ export default {
|
|
|
).setPosition(
|
|
|
e.feature.getGeometry().getInteriorPoint().getCoordinates()
|
|
|
)
|
|
|
- }
|
|
|
- //* *****角度测量结束时*****//
|
|
|
- else if (this.measureType === 'angle') {
|
|
|
+ } else if (this.measureType === 'angle') {
|
|
|
+ //* *****角度测量结束时*****//
|
|
|
this.creatMark(closeBtn, null, 'close3').setPosition(
|
|
|
e.feature.getGeometry().getCoordinates()[1]
|
|
|
)
|
|
|
@@ -349,19 +558,48 @@ export default {
|
|
|
this.stopMeasure()
|
|
|
})
|
|
|
},
|
|
|
+ markPlace() {
|
|
|
+ // 切换状态
|
|
|
+ this.isMarking = !this.isMarking
|
|
|
+
|
|
|
+ if (this.isMarking) {
|
|
|
+ // 1. 改变鼠标指针为十字准星
|
|
|
+ this.map.getTargetElement().style.cursor = 'crosshair'
|
|
|
+ // 2. 定义点击时的执行函数
|
|
|
+ const handleMapClick = (evt) => {
|
|
|
+ // 获取点击位置的原始墨卡托坐标(高德地图) [米, 米]
|
|
|
+ // evt.coordinate 是一个数组:[lng, lat]
|
|
|
+ const coordinate = evt.coordinate
|
|
|
+ this.addMarker(coordinate)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 绑定持久点击事件,并把返回的 key 存起来以便后续解绑
|
|
|
+ this.clickListenerKey = this.map.on('singleclick', handleMapClick)
|
|
|
+ } else {
|
|
|
+ // ================= 退出标记地点状态 =================
|
|
|
+ // 1. 恢复默认鼠标指针
|
|
|
+ this.map.getTargetElement().style.cursor = ''
|
|
|
+
|
|
|
+ // 2. 核心:解除绑定的点击事件,让地图恢复正常
|
|
|
+ if (this.clickListenerKey) {
|
|
|
+ // OpenLayers 6+ 中解绑事件的方式:使用 ol/Observable 的 unByKey 或者直接传入 key
|
|
|
+ // 如果你的版本支持, 可以直接使用下面的标准方法:
|
|
|
+ this.map.un('singleclick', this.clickListenerKey.listener)
|
|
|
+ this.clickListenerKey = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
/**
|
|
|
* 测量
|
|
|
*/
|
|
|
measure(type) {
|
|
|
- if (this.draw != null) return false // 防止在绘制过程再创建测量
|
|
|
+ if (this.draw !== null) return false // 防止在绘制过程再创建测量
|
|
|
this.measureType = type
|
|
|
- if (this.vectorLayer != null) this.clearMeasure()
|
|
|
+ if (this.vectorLayer !== null) this.clearMeasure()
|
|
|
this.tipDiv = document.createElement('div')
|
|
|
this.tipDiv.innerHTML = '单击确定起点'
|
|
|
this.tipDiv.className = 'tipDiv'
|
|
|
- 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;'
|
|
|
-
|
|
|
+ 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;'
|
|
|
const overlay = new Overlay({
|
|
|
element: this.tipDiv,
|
|
|
autoPan: false,
|
|
|
@@ -374,7 +612,7 @@ export default {
|
|
|
this.pointermoveEvent = this.map.on('pointermove', (evt) => {
|
|
|
overlay.setPosition(evt.coordinate)
|
|
|
})
|
|
|
- if (this.measureType === 'distence' || this.measureType === 'angle') {
|
|
|
+ if (this.measureType === 'distance' || this.measureType === 'angle') {
|
|
|
this.creatDraw('LineString')
|
|
|
} else if (this.measureType === 'area') {
|
|
|
this.creatDraw('Polygon')
|
|
|
@@ -403,7 +641,7 @@ export default {
|
|
|
/**
|
|
|
* 格式化距离结果输出
|
|
|
*/
|
|
|
- distenceFormat(length) {
|
|
|
+ distanceFormat(length) {
|
|
|
let output
|
|
|
if (length > 100) {
|
|
|
output = Math.round((length / 1000) * 100) / 100 + ' ' + 'km' // 换算成km单位
|
|
|
@@ -490,54 +728,6 @@ export default {
|
|
|
this.geometryListener = null
|
|
|
this.measureResult = '0'
|
|
|
},
|
|
|
- // 全屏
|
|
|
- fullscreen() {
|
|
|
- if (this.textarr === '全屏') {
|
|
|
- this.textarr = '缩小'
|
|
|
- const rfs =
|
|
|
- this.$refs.map.requestFullScreen ||
|
|
|
- this.$refs.map.webkitRequestFullScreen ||
|
|
|
- this.$refs.map.mozRequestFullScreen ||
|
|
|
- this.$refs.map.msRequestFullScreen
|
|
|
- if (typeof rfs !== 'undefined' && rfs) {
|
|
|
- rfs.call(this.$refs.map)
|
|
|
- console.log('1全屏')
|
|
|
- } else if (typeof window.ActiveXObject !== 'undefined') {
|
|
|
- // for IE,这里其实就是模拟了按下键盘的F11,使浏览器全屏
|
|
|
- // eslint-disable-next-line no-undef
|
|
|
- const wscript = new ActiveXObject('WScript.Shell')
|
|
|
- console.log(wscript)
|
|
|
- if (wscript != null) {
|
|
|
- wscript.SendKeys('{F11}')
|
|
|
- console.log('3全屏')
|
|
|
- }
|
|
|
- console.log('2全屏')
|
|
|
- }
|
|
|
- } else {
|
|
|
- // el.webkitExitFullscreen()
|
|
|
- this.textarr = '全屏'
|
|
|
- const cfs =
|
|
|
- document.exitFullscreen ||
|
|
|
- document.msExitFullscreen ||
|
|
|
- document.mozCancelFullScreen ||
|
|
|
- document.webkitCancelFullScreen
|
|
|
- console.log(cfs, 'cfs')
|
|
|
- if (typeof cfs !== 'undefined' && cfs) {
|
|
|
- cfs.call(document)
|
|
|
- console.log('4全屏')
|
|
|
- } else if (typeof window.ActiveXObject !== 'undefined') {
|
|
|
- // for IE,这里和fullScreen相同,模拟按下F11键退出全屏
|
|
|
- // eslint-disable-next-line no-undef
|
|
|
- const wscript = new ActiveXObject('WScript.Shell')
|
|
|
- console.log('5全屏')
|
|
|
- if (wscript != null) {
|
|
|
- wscript.SendKeys('{F11}')
|
|
|
- console.log('6全屏')
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- // this.map.addControl(new FullScreen())
|
|
|
- },
|
|
|
// 坐标
|
|
|
single() {
|
|
|
this.map.on('singleclick', function(e) {
|
|
|
@@ -546,8 +736,11 @@ export default {
|
|
|
},
|
|
|
// 清除
|
|
|
clear() {
|
|
|
- // getSource 获取包装源的参考
|
|
|
- this.vectorLayer.getSource().clear()
|
|
|
+ // 清空测量矢量图层
|
|
|
+ if (this.vectorLayer) this.vectorLayer.getSource().clear()
|
|
|
+ // 清空后台加载的标记图层
|
|
|
+ if (this.markerSource) this.markerSource.clear()
|
|
|
+ // 清空所有弹窗 Overlay
|
|
|
this.map.getOverlays().clear()
|
|
|
}
|
|
|
}
|
|
|
@@ -556,22 +749,73 @@ export default {
|
|
|
|
|
|
<style scoped>
|
|
|
#map {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
width: 100%;
|
|
|
- height: 600px;
|
|
|
- position: relative;
|
|
|
+ height: 100%;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
-.btns {
|
|
|
+
|
|
|
+/* 右上角工具箱容器 */
|
|
|
+.toolbox {
|
|
|
position: absolute;
|
|
|
z-index: 999;
|
|
|
- left: 50%;
|
|
|
- top: 20px;
|
|
|
- transform: translate(-50%);
|
|
|
+ top: 20px; /* 距离顶部 20px */
|
|
|
+ right: 20px; /* 距离右侧 20px */
|
|
|
+ background: rgba(255, 255, 255, 0.9); /* 半透明白底背景 */
|
|
|
+ padding: 8px;
|
|
|
+ border-radius: 6px; /* 圆角 */
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.15); /* 地图控件常用的阴影 */
|
|
|
+
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, 75px); /* 固定为两列,每列宽 75px */
|
|
|
+ gap: 6px; /* 按钮之间的间距 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 工具箱内的按钮通用样式 */
|
|
|
+.toolbox button {
|
|
|
+ height: 28px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ background-color: #fff;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 12px;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ padding: 0 4px;
|
|
|
}
|
|
|
-.btns button {
|
|
|
- display: inline-block;
|
|
|
- text-decoration: none;
|
|
|
- text-align: center;
|
|
|
- height: 30px;
|
|
|
- width: 90px;
|
|
|
+
|
|
|
+/* 鼠标悬浮和点击效果 */
|
|
|
+.toolbox button:hover {
|
|
|
+ color: #fc5531;
|
|
|
+ border-color: #fc5531;
|
|
|
+ background-color: rgba(252, 86, 49, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+/* 实时跟随鼠标的半径标签气泡样式 */
|
|
|
+.ol-radius-marker {
|
|
|
+ background: rgba(64, 158, 255, 0.95); /* 漂亮的科技蓝 */
|
|
|
+ color: #ffffff;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: bold;
|
|
|
+ white-space: nowrap;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
|
+ border: 1px solid #ffffff;
|
|
|
+ position: relative;
|
|
|
+ pointer-events: none; /* 极其重要:确保 DOM 穿透,否则会挡住鼠标导致无法继续绘制 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 气泡下方的微型小尖角 */
|
|
|
+.ol-radius-marker::after {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ bottom: -4px;
|
|
|
+ left: 10px;
|
|
|
+ border-width: 4px 4px 0;
|
|
|
+ border-style: solid;
|
|
|
+ border-color: rgba(64, 158, 255, 0.95) transparent;
|
|
|
}
|
|
|
</style>
|