import * as ol from 'ol' import * as style from 'ol/style' import * as layer from 'ol/layer' import * as source from 'ol/source' import * as geom from 'ol/geom' import * as extent from 'ol/extent' import * as proj from 'ol/proj' import * as interaction from 'ol/interaction' import * as format from "ol/format"; import Modify from "ol/interaction/Modify" import Draw, {createBox} from "ol/interaction/Draw" import * as sphere from "ol/sphere"; import {unByKey} from "ol/Observable"; import {formatPosition} from "@/utils/easyMap"; import {isValue} from "@/utils/util"; import {fromCircle} from "ol/geom/Polygon"; import {ElMessage} from "element-plus"; const globalLineDash = [ [0, 0], //实线 [15, 15], //长虚线 [5, 5] //虚线 ] const layerFlag = ['layerName', 'drawViewsLayer'] const drawFlag = ['interactionName', 'drawInteraction'] const modifyFlag = ['interactionName', 'modifyInteraction'] const baseDrawConfig = { // 样式字段 text: null, // 要素上显示的文字,默认无文字 pointIcon: null, // Point的图标,默认圆形 pointScale: 1, // Point的缩放,默认1 pointOffset: [0, 0], // Point的偏移量,默认[0, 0] lineColor: '#2860F1', // LineString的线段颜色,默认蓝色 lineWidth: 1, // LineString的线段宽度,默认1 lineType: 0, // LineString的线段类型索引,默认0,实线,取globalLineDash数组索引 lineDash: null, // LineString的线段类型,默认null,优先级比lineType高 polyColor: 'rgba(20, 129, 241, 0.3)', // Polygon的填充色,默认蓝色 polyBorderColor: '#2860F1', // Polygon的边框颜色,默认蓝色 polyBorderWidth: 1, // Polygon的边框宽度,默认1 polyBorderType: 0, // Polygon的边框类型索引,默认0,实线,取globalLineDash数组索引 polyBorderDash: null, // Polygon的边框类型,默认null,优先级比polyBorderType高 // 业务字段 show: false, // 标绘样式选项是否显示 featureType: 'Point', // 标绘的要素类型 isPoint: false, // 是否可以标绘点 isLineString: false, // 是否可以标绘线 isPolygon: false, // 是否可以标绘面 showPosition: false, // 是否显示经纬度输入框 refreshStyleFunc: () => {}, // 刷新标绘样式方法 } export const getBaseDrawConfig = () => { return JSON.parse(JSON.stringify(baseDrawConfig)) } /** * * @param map * @param arr 要标绘的数组,每个对象的数据在要素中保存的属性为 'val' * @param arr > wkt:wkt格式坐标 * @param arr > styles:要素的样式,优先级最高 * @param arr > text:styles为空的时候,要素上显示的文字,默认无文字 * @param arr > textOffsetY:styles为空的时候,要素上显示的文字Y轴偏移量,默认Point-30,其他0 * @param arr > pointIcon:styles为空的时候,Point的图标,默认圆形 * @param arr > pointScale:styles为空的时候,Point的缩放,默认1 * @param arr > pointOffset:styles为空的时候,Point的偏移量,默认[0, 0] * @param arr > lineColor:styles为空的时候,LineString的线段颜色,默认蓝色 * @param arr > lineWidth:styles为空的时候,LineString的线段宽度,默认1 * @param arr > lineType:styles为空的时候,LineString的线段类型索引,默认0,实线,取globalLineDash数组索引 * @param arr > lineDash:styles为空的时候,LineString的线段类型,默认null,优先级比lineType高 * @param arr > polyColor:styles为空的时候,Polygon的填充色,默认蓝色 * @param arr > polyBorderColor:styles为空的时候,Polygon的边框颜色,默认蓝色 * @param arr > polyBorderWidth:styles为空的时候,Polygon的边框宽度,默认1 * @param arr > polyBorderType:styles为空的时候,Polygon的边框类型索引,默认0,实线,取globalLineDash数组索引 * @param arr > polyBorderDash:styles为空的时候,Polygon的边框类型,默认null,优先级比polyBorderType高 */ export const drawViews = (map, arr, isAuto = true) => { let _source const realLayer = map.getLayers().getArray().filter(v => v.get(layerFlag[0]) === layerFlag[1]) if (realLayer[0]) { _source = realLayer[0].getSource() } else { _source = new source.Vector(); //图层数据源 const _vector = new layer.Vector({ zIndex: 9999, source: _source, }); _vector.set(layerFlag[0], layerFlag[1]) map.addLayer(_vector); } _source.clear() // 清空标绘 const autoPosition: any = [] const features = arr.filter(v => { try { new format.WKT().readFeature(v.wkt) return true } catch (e) { console.error('错误坐标:' + v.wkt) return false } }).map(v => { const feat: any = new format.WKT().readFeature(v.wkt) const type = feat.getGeometry().getType() switch (type) { case 'Point': autoPosition.push(feat.getGeometry().getCoordinates()) break case 'LineString': autoPosition.push(...feat.getGeometry().getCoordinates()) break case 'Polygon': autoPosition.push(...feat.getGeometry().getCoordinates()[0]) break } for (const [kVal, vVal] of Object.entries(v)) { // @ts-ignore if (vVal === null || vVal === undefined || (typeof vVal === 'string' && vVal.trim() === '') || (typeof vVal === 'object' && vVal?.length === 0)) { delete v[kVal] } } const { textOffsetY = (type === 'Point' ? -30 : 0), pointIcon, pointScale, pointOffset, lineColor, lineWidth, lineType, lineDash, polyColor, polyBorderColor, polyBorderWidth, polyBorderType, polyBorderDash } = Object.assign(getBaseDrawConfig(), v) let styles: any = [] if (v.styles) { styles = v.styles } else { if (type === 'Point') { if (pointIcon) { styles.push(new style.Style({ image: new style.Icon({ src: pointIcon, scale: pointScale, displacement: pointOffset }) })) } else { styles.push(new style.Style({ image: new style.Circle({ radius: 10, fill: new style.Fill({ color: '#e810dd', }), scale: pointScale, displacement: pointOffset }) })) } } else if (type === 'LineString') { styles.push(new style.Style({ stroke: new style.Stroke({ color: lineColor, width: lineWidth, lineDash: lineDash ?? globalLineDash[Number(lineType)] }) })) } else if (type === 'Polygon') { styles.push(new style.Style({ stroke: new style.Stroke({ color: polyBorderColor, width: polyBorderWidth, lineDash: polyBorderDash ?? globalLineDash[Number(polyBorderType)] }), fill: new style.Fill({ color: polyColor, }), })) } if (v.text) { styles.push(new style.Style({ text: new style.Text({ font: "16px bold 微软雅黑", text: v.text, fill: new style.Fill({ color: '#ffffff' }), stroke: new style.Stroke({ color: '#D26CDB', width: 2 }), offsetY: textOffsetY, }), })) } } feat.set('val', v) feat.setStyle(styles) return feat }) _source.addFeatures(features) if (isAuto) { getShapeView(map, autoPosition) } } let baseDrawTooltipElement; let baseDrawHelpTooltipElement; export const drawEdits = (map, obj, emitWkt, isAuto = true) => { return new Promise((resolve => { if (!isValue(obj.textOffsetY)) { obj.textOffsetY = (obj.featureType === 'Point' ? -30 : 0) } let commonStyle = (showText) => { if (obj.featureType === 'Point') { return new style.Style({ image: obj.pointIcon ? new style.Icon({ src: obj.pointIcon, scale: obj.pointScale, displacement: obj.pointOffset }) : new style.Circle({ radius: 10, fill: new style.Fill({ color: '#e810dd', }), scale: obj.pointScale, displacement: obj.pointOffset }), text: (showText && obj.text) ? new style.Text({ font: "16px bold 微软雅黑", text: obj.text, fill: new style.Fill({ color: '#ffffff' }), stroke: new style.Stroke({ color: '#D26CDB', width: 2 }), offsetY: obj.textOffsetY, }) : undefined, }) } else if (obj.featureType === 'LineString') { return new style.Style({ stroke: new style.Stroke({ color: obj.lineColor, width: obj.lineWidth, lineDash: obj.lineDash ?? globalLineDash[Number(obj.lineType)] }), text: (showText && obj.text) ? new style.Text({ font: "16px bold 微软雅黑", text: obj.text, fill: new style.Fill({ color: '#ffffff' }), stroke: new style.Stroke({ color: '#D26CDB', width: 2 }), offsetY: obj.textOffsetY, }) : undefined, }) } else if (obj.featureType === 'Polygon') { return new style.Style({ stroke: new style.Stroke({ color: obj.polyBorderColor, width: obj.polyBorderWidth, lineDash: obj.polyBorderDash ?? globalLineDash[Number(obj.polyBorderType)] }), fill: new style.Fill({ color: obj.polyColor, }), text: (showText && obj.text) ? new style.Text({ font: "16px bold 微软雅黑", text: obj.text, fill: new style.Fill({ color: '#ffffff' }), stroke: new style.Stroke({ color: '#D26CDB', width: 2 }), offsetY: obj.textOffsetY, }) : undefined, }) } } const getWkt = (feature) => { const gm = feature.getGeometry() let wkt = '' const gType = gm.getType() if (gType === 'Polygon') { wkt = formatPosition.cpnTwpn(gm.getCoordinates()) } else if (gType === 'LineString') { wkt = formatPosition.clTwl(gm.getCoordinates()) } else if (gType === 'Circle') { const circlePoly = fromCircle(gm, 128) feature.setGeometry(circlePoly) wkt = formatPosition.cpnTwpn(circlePoly.getCoordinates()) } else if (gType === 'Point') { wkt = formatPosition.cptTwpt(gm.getCoordinates()) } emitWkt(wkt) } const reset = () => { const oldLayer = map.getLayers().getArray().filter(v => v.get(layerFlag[0]) === layerFlag[1]) if (oldLayer) { map.removeLayer(oldLayer[0]) } const oldDraw = map.getInteractions().getArray().filter(v => v.get(drawFlag[0]) === drawFlag[1]) if (oldDraw) { map.removeInteraction(oldDraw[0]) } const oldModify = map.getInteractions().getArray().filter(v => v.get(modifyFlag[0]) === modifyFlag[1]) if (oldModify) { map.removeInteraction(oldModify[0]) } } if (!baseDrawTooltipElement) { reset() let _source const realLayer = map.getLayers().getArray().filter(v => v.get(layerFlag[0]) === layerFlag[1]) if (realLayer[0]) { _source = realLayer[0].getSource() } else { _source = new source.Vector(); //图层数据源 const _vector = new layer.Vector({ zIndex: 9999, source: _source, style: commonStyle(true), }); _vector.set(layerFlag[0], layerFlag[1]) map.addLayer(_vector); } if (obj.wkt) { try { const feat: any = new format.WKT().readFeature(obj.wkt) _source.addFeature(feat) const autoPosition: any = [] const type = feat.getGeometry().getType() switch (type) { case 'Point': autoPosition.push(feat.getGeometry().getCoordinates()) break case 'LineString': autoPosition.push(...feat.getGeometry().getCoordinates()) break case 'Polygon': autoPosition.push(...feat.getGeometry().getCoordinates()[0]) break } if (isAuto) { getShapeView(map, autoPosition) } } catch (e) { obj.wkt = '' ElMessage.warning('坐标格式错误,请重新标绘!') } } const modifyInteraction = new Modify({ source: _source, }); modifyInteraction.set(modifyFlag[0], modifyFlag[1]) map.addInteraction(modifyInteraction) modifyInteraction.on('modifyend', evt => { try { const feat = evt.features.item(0) getWkt(feat) } catch { } }) if (!obj.wkt) { let sketch; let helpTooltip; let measureTooltip; let continueMsg = '双击结束标绘'; const geodesicCheckbox = true;//测地学方式对象 const createMeasureTooltip = () => { const id = 'baseDrawTooltipElementId' if (baseDrawTooltipElement) { map.removeOverlay(map.getOverlayById(id)) baseDrawTooltipElement.parentNode.removeChild(baseDrawTooltipElement); } baseDrawTooltipElement = document.createElement('div'); baseDrawTooltipElement.className = 'tooltip tooltip-measure'; measureTooltip = new ol.Overlay({ id, element: baseDrawTooltipElement, offset: [0, -15], positioning: 'bottom-center' }); map.addOverlay(measureTooltip); } const createHelpTooltip = () => { const id = 'baseDrawHelpTooltipElementId' if (baseDrawHelpTooltipElement) { map.removeOverlay(map.getOverlayById(id)) baseDrawHelpTooltipElement.parentNode.removeChild(baseDrawHelpTooltipElement); } baseDrawHelpTooltipElement = document.createElement('div'); baseDrawHelpTooltipElement.className = 'tooltip hidden'; helpTooltip = new ol.Overlay({ id, element: baseDrawHelpTooltipElement, offset: [15, 0], positioning: 'center-left' }); map.addOverlay(helpTooltip); } const formatLength = (line) => { // 获取投影坐标系 const sourceProj = map.getView().getProjection(); // ol/sphere里有getLength()和getArea()用来测量距离和区域面积,默认的投影坐标系是EPSG:3857, 其中有个options的参数,可以设置投影坐标系 const length = sphere.getLength(line, {projection: sourceProj}); // const length = getLength(line); let output; if (length > 100) { const km = Math.round((length / 1000) * 100) / 100; output = `${km} 千米 <br>${parseFloat(String(km * 0.53995)).toFixed(2)} 海里`; } else { output = `${Math.round(length * 100) / 100} m`; } return output; }; //获取圆的面积 const getCircleArea = (circle, projection) => { const P = 3.14 const radius = getCircleRadius(circle, projection) return P * radius * radius } //获取圆的半径 const getCircleRadius = (circle, projection) => { return circle.getRadius() * projection.getMetersPerUnit() } const formatArea = (polygon, type= 'polygon') => { let area const sourceProj = map.getView().getProjection(); // 获取投影坐标系 if (type === 'polygon') { area = sphere.getArea(polygon, { projection: sourceProj, }); } else if (type === 'circle') { area = getCircleArea(polygon, sourceProj) } let output; if (area > 10000) { const km = Math.round((area / 1000000) * 100) / 100; output = `${km} 平方公里<br>${parseFloat(String(km * 0.38610)).toFixed( 2 )} 平方英里`; } else { output = `${Math.round(area * 100) / 100} ` + " m<sup>2</sup>"; } return output; }; const addInteraction = () => { const id = 'baseDrawName' let drawLastPoint = '' let drawLastPointTimer: any = null let drawCircleDBClickTimer: any = null const draw = new interaction.Draw({ stopClick: true, condition: (e) => { // 圆形单击即触发finishCondition,跳过 if (obj.featureType === 'Circle') { return true } const str = e.coordinate.join(',') let flag = true // 进行延时判断,避免只要鼠标不移动,单击后间隔很久也会视为双击, if (!drawLastPointTimer && str === drawLastPoint) { flag = false } else { if (drawLastPointTimer) { clearTimeout(drawLastPointTimer) } drawLastPoint = str } drawLastPointTimer = setTimeout(() => { drawLastPointTimer = null }, 1000) return flag }, finishCondition: (e) => { if (obj.featureType !== 'Circle') { return true } let flag = true // 圆形进行双击延时监听判断 if (!drawCircleDBClickTimer) { flag = false } if (drawCircleDBClickTimer) { clearTimeout(drawCircleDBClickTimer) } drawCircleDBClickTimer = setTimeout(() => { drawCircleDBClickTimer = null }, 1000) return flag }, source: _source,//测量绘制层数据源 type: obj.featureType, //几何图形类型 // geometryFunction: typeSelect === 'rectangle' ? createBox() : null, style: commonStyle(obj.featureType === 'Point' ? true : false), }); draw.set('showText', obj.featureType === 'Point' ? true : false) draw.set(drawFlag[0], drawFlag[1]) draw.set(id, id) createMeasureTooltip(); //创建测量工具提示框 createHelpTooltip(); //创建帮助提示框 map.addInteraction(draw); let listener; //绑定交互绘制工具开始绘制的事件 const drawstartHandle = (evt) => { sketch = evt.feature; //绘制的要素 let tooltipCoord = evt.coordinate;// 绘制的坐标 //绑定change事件,根据绘制几何类型得到测量长度值或面积值,并将其设置到测量工具提示框中显示 listener = sketch.getGeometry().on('change', function (evt) { const geom = evt.target let output; if (geom.getType() === 'LineString') { output = formatLength(geom);//长度值 tooltipCoord = geom.getLastCoordinate();//坐标 } else if (geom.getType() === 'Polygon') { output = formatArea(geom);//面积值 tooltipCoord = geom.getInteriorPoint().getCoordinates();//坐标 } else if (geom.getType() === 'Circle') { output = formatArea(geom, 'circle');//面积值 tooltipCoord = geom.getCenter() } baseDrawTooltipElement.innerHTML = output;//将测量值设置到测量工具提示框中显示 measureTooltip.setPosition(tooltipCoord);//设置测量工具提示框的显示位置 }); } draw.on('drawstart', drawstartHandle); //绑定交互绘制工具结束绘制的事件 const copy = (value) => { const str = document.createElement('input') str.setAttribute('value', value) document.body.appendChild(str) str.select() document.execCommand('copy') document.body.removeChild(str) } const drawendHandle = (evt) => { map.removeInteraction(map.getInteractions().getArray().filter(v => v.get(id) === id)[0]); // 标绘的时候不需要最终结果dom map.removeOverlay(map.getOverlayById('baseDrawHelpTooltipElementId')) baseDrawTooltipElement.parentNode.removeChild(baseDrawTooltipElement); sketch = null; //置空当前绘制的要素对象 baseDrawTooltipElement = null; //置空测量工具提示框对象 baseDrawHelpTooltipElement.parentNode.removeChild(baseDrawHelpTooltipElement); baseDrawHelpTooltipElement = null; //置空测量工具提示框对象 unByKey(listener); draw.un('drawstart', drawstartHandle); draw.un('drawend', drawendHandle); map.removeInteraction(map.getInteractions().getArray().filter(v => v.get(id) === id)[0]); map.un('pointermove', pointerMoveHandler) const baseDrawFeature = evt.feature getWkt(baseDrawFeature) } draw.on('drawend', drawendHandle); } addInteraction(); //调用加载绘制交互控件方法,添加绘图进行测量 const pointerMoveHandler = (evt) => { if (evt.dragging) { return; } let helpMsg = '单击开始标绘';//当前默认提示信息 //判断绘制几何类型设置相应的帮助提示信息 if (sketch) { const geom = sketch.getGeometry() helpMsg = continueMsg; // if (geom.getType() === 'Polygon') { // helpMsg = continueMsg; //绘制多边形时提示相应内容 // } else if (geom.getType() === 'LineString') { // helpMsg = continueMsg; //绘制线时提示相应内容 // } } if (baseDrawHelpTooltipElement) { baseDrawHelpTooltipElement.innerHTML = helpMsg; //将提示信息设置到对话框中显示 helpTooltip.setPosition(evt.coordinate);//设置帮助提示框的位置 baseDrawHelpTooltipElement.classList.remove('hidden');//移除帮助提示框的隐藏样式进行显示 } }; map.on('pointermove', pointerMoveHandler); //地图容器绑定鼠标移动事件,动态显示帮助提示框内容 //地图绑定鼠标移出事件,鼠标移出时为帮助提示框设置隐藏样式 try { map.getViewport().on('mouseout', () => { baseDrawHelpTooltipElement.addClass('hidden'); }); } catch (e) { } } } resolve(() => { const oldLayer = map.getLayers().getArray().filter(v => v.get(layerFlag[0]) === layerFlag[1]) if (oldLayer) { oldLayer[0].setStyle(commonStyle(true)) } // const oldDraw = map.getInteractions().getArray().filter(v => v.get(drawFlag[0]) === drawFlag[1]) // if (oldDraw) { // oldDraw[0].setStyle(commonStyle(oldDraw[0].get('showText'))) // } }) })) } export const drawExit = (map) => { if (baseDrawTooltipElement) { baseDrawTooltipElement.parentNode?.removeChild?.(baseDrawTooltipElement); baseDrawTooltipElement = null } if (baseDrawHelpTooltipElement) { baseDrawHelpTooltipElement.parentNode?.removeChild?.(baseDrawHelpTooltipElement); baseDrawHelpTooltipElement = null } const oldLayer = map.getLayers().getArray().filter(v => v.get(layerFlag[0]) === layerFlag[1]) if (oldLayer) { map.removeLayer(oldLayer[0]) } const oldDraw = map.getInteractions().getArray().filter(v => v.get(drawFlag[0]) === drawFlag[1]) if (oldDraw) { map.removeInteraction(oldDraw[0]) } const oldModify = map.getInteractions().getArray().filter(v => v.get(modifyFlag[0]) === modifyFlag[1]) if (oldModify) { map.removeInteraction(oldModify[0]) } } const getShapeView = (map, position, L = 600) => { const center = extent.getCenter(extent.boundingExtent(position)) let x = 0 let y = 0 position.forEach(v => { if (Math.abs(v[0] - center[0]) > x) { x = Math.abs(v[0] - center[0]) } if (Math.abs(v[1] - center[1]) > y) { y = Math.abs(v[1] - center[1]) } }) const resolution = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) / (L / document.body.clientWidth * document.body.clientHeight) if (map) { if (position.length > 1) { map.getView().animate({ center, resolution }) } else { map.getView().animate({ center, zoom: 12 }) } } return { center, resolution } }