前言
开发GIS系统,我们都会提供基本的一些测量功能给用户使用,用于测量地图上的距离和面积,这里我们使用openlayers实现此功能。
实现
1. 功能描述
用户通过在地图上多次点击确定要测量的路线或区域,点击过程中会更新现有的距离和面积,并且距离会按段显示。双击后结束单次的测量。
2. 前置知识
要实现此功能我们需要了解openlayers的一些交互组件,像我们在地图上点击画路线和区域就需要draw interaction.要在地图上显示其距离和面积需要通过overlay 显示到地图上。
我们并没有直接通过geometry对象的getLength和getArea获取长度和面积,而是通过sphere获取。如果直接调用geometry的方法获取的长度是直接通过平面坐标计算,不会考虑地球的曲率,如果本身就是平面的需求可以直接调用
3.代码实现
/*
* 地图测量工具类
* */
import {Vector as VectorLayer} from "ol/layer"
import {Vector as VectorSource} from "ol/source"
import {Circle, Fill, Stroke, Style} from 'ol/style'
import {LineString, MultiPoint, Point, Polygon} from 'ol/geom'
import {Draw} from 'ol/interaction'
import Overlay from "ol/Overlay"
import {areaFormatterEnglish, distanceFormatterEnglish, randomString} from "@/utils/util"
import Vue from 'vue'
import * as olSphere from "ol/sphere.js"
class MeasureTool {
static LayerName = 'gis-map-measure-layer'
constructor(olMap,options) {
this.olMap = olMap
this.source = new VectorSource()
const style = MeasureTool.getStyle()
this.layer = new VectorLayer({
source:this.source,
style:style,
zIndex:100
})
this.olMap.addLayers({[MeasureTool.LayerName]:this.layer})
this.interaction = null
this.calcType = options.calcType ? options.calcType : 'sphere'
this.featureOverlayCaches = {}
}
static getStyle(drawing=false){
return [
new Style({
fill: new Fill({
color: 'rgba(60, 225, 223, 0.2)'
}),
stroke: new Stroke({
color: '#3acecc',
width: 2,
lineDash:drawing ? [3,3]:null
}),
image: new Circle({
radius: 4,
stroke: new Stroke({
color: '#4b4b4b',
width: 2
}),
fill: new Fill({
color: '#3acecc'
})
})
}),
new Style({
image: new Circle({
radius: 4,
stroke: new Stroke({
color: '#4b4b4b',
width: 2
}),
fill: new Fill({
color: '#3acecc'
})
}),
geometry: function (feature) {
let coordinates;
let geom = feature.getGeometry();
if (geom instanceof Polygon) {
coordinates = geom.getCoordinates()[0];
} else if (geom instanceof LineString) {
coordinates = geom.getCoordinates();
} else if (geom instanceof Point) {
return null;
}
return new MultiPoint(coordinates);
}
})
]
}
/**
* 测量距离
* */
measureDistance() {
this.startMeasure('LineString')
}
/**
* 测量面积
* */
measureArea() {
this.startMeasure('Polygon')
}
/**
* 开始测量,添加interaction
* @param {string} type 测量类型
* */
startMeasure(type) {
//显示图层
this.layer.setVisible(true)
this.resetMeasure()
this.interaction = new Draw({
source: this.source,
type: type,
style: MeasureTool.getStyle(true)
})
this.olMap.map.addInteraction(this.interaction)
this.interaction.on('drawstart',this.drawStartHandler.bind(this))
this.interaction.on('drawend',this.drawEndHandler.bind(this))
//创建tooltip element
}
/**
* 交互开始处理
* */
drawStartHandler(e){
const feature = e.feature
feature.setId(randomString(8))
this.featureOverlayCaches[feature.getId()] = []
feature.on('change',this.featureChangeHandler.bind(this))
}
/**
* 交互过程中测量变化处理
* */
featureChangeHandler(e){
const feature = e.target
const geom = feature.getGeometry()
//更新overlay
const overlays = this.featureOverlayCaches[feature.getId()]
if (geom instanceof LineString){
const points = geom.getCoordinates()
for(let i=0;i<points.length;i++){
let overlay = overlays[i]
if (overlay==null){
if (i === 0){
//起点
overlay = this.newLabelOverlay('start',points[i],feature)
this.olMap.map.addOverlay(overlay)
this.featureOverlayCaches[feature.getId()][i]=overlay
}else if (i===points.length-1 && i>0){
//添加总距离overlay
overlay = this.newLabelOverlay('end',points[i],feature,this.getLineStringLen(geom))
this.olMap.map.addOverlay(overlay)
this.featureOverlayCaches[feature.getId()][i]=overlay
if (points.length>2){
//更新上一个点的overlay 为截止上个点的长度
this.olMap.map.removeOverlay(overlays[i-1])
const segLine = new LineString(points.slice(0,i))
const segOverlay = this.newLabelOverlay('mid',points[i-1],feature,this.getLineStringLen(segLine))
this.olMap.map.addOverlay(segOverlay)
this.featureOverlayCaches[feature.getId()][i-1] = segOverlay
}
}
}else if (i===points.length-1){
//更新位置
overlay.setPosition(geom.getLastCoordinate())
//更新内容
overlay.setElement(this.buildDistanceOverlayTipElement('end',feature,this.getLineStringLen(geom)))
}
}
}else if (geom instanceof Polygon){
const coords = geom.getCoordinates()
console.log('polygon coords:',coords)
if (overlays.length ===0){
const overlay = this.newLabelOverlay('mid',coords[0].slice(-2,-1)[0],feature,this.getPolygonArea(geom))
overlays[0] = overlay
this.olMap.map.addOverlay(overlay)
}else{
overlays[0].setPosition(coords[0].slice(-2,-1)[0])
//更新内容
overlays[0].setElement(this.buildAreaOverlayTipElement('mid',feature,this.getPolygonArea(geom)))
}
}
}
getLineStringLen(geom){
if (this.calcType === 'sphere'){
return olSphere.getLength(geom,{projection:this.olMap.getProjection()})
}else{
return geom.getLength()
}
}
getPolygonArea(geom){
if (this.calcType === 'sphere'){
return olSphere.getArea(geom,{projection:this.olMap.getProjection()})
}else{
return geom.getArea()
}
}
buildDistanceOverlayTipElement(type,feature,val){
const containerStyle = {
padding:'3px 6px 5px',
display:'block',
backgroundColor:'#4a4a4a',
borderRadius:'4px',
boxShadow:'0 2px 4px 0 rgba(0,0,0,0.15)'
}
const that = this
const formatVal = distanceFormatterEnglish(val)
const obj = new Vue({
render(createElement) {
if (type === 'start'){
return (
<div style={containerStyle} className="ol-tooltip ol-tooltip-static">
<span>起点</span>
</div>
)
}else if (type === 'end'){
return (
<div className="ol-tooltip ol-tooltip-static" style="display:flex;align-items:center">
<div style={containerStyle}>
<span>总距离:</span>
<span style="color:#1890FF">{formatVal.val}</span>
<span style="margin-left:3px">{formatVal.unit}</span>
</div>
<div style={Object.assign({marginLeft:'6px'},containerStyle)} onClick={()=>{that.clearMeasureByKey(feature.getId())}}>
<a-icon type="close"/>
</div>
</div>
)
} else if (type === 'mid') {
return (
<div style={containerStyle} className="ol-tooltip ol-tooltip-static">
<span>{formatVal.val}</span>
<span style="margin-left:3px">{formatVal.unit}</span>
</div>
)
}
},
methods:{
clearMeasureByKey(key){
that.clearMeasureByKey(key)
}
}
}).$mount()
return obj.$el
}
buildAreaOverlayTipElement(type,feature,val){
const containerStyle = {
padding:'3px 6px 5px',
display:'block',
backgroundColor:'#4a4a4a',
borderRadius:'4px',
boxShadow:'0 2px 4px 0 rgba(0,0,0,0.15)'
}
const that = this
const formatVal = areaFormatterEnglish(val)
const obj = new Vue({
render(createElement) {
if (type === 'end'){
return (
<div className="ol-tooltip ol-tooltip-static" style="display:flex;align-items:center">
<div style={containerStyle}>
<span>总面积:</span>
<span style="color:#1890FF">{formatVal.val}</span>
<span style="margin-left:3px">{formatVal.unit}</span>
</div>
<div style={Object.assign({marginLeft:'6px'},containerStyle)} onClick={()=>{that.clearMeasureByKey(feature.getId())}}>
<a-icon type="close"/>
</div>
</div>
)
} else if (type === 'mid') {
return (
<div style={containerStyle} className="ol-tooltip ol-tooltip-static">
<div>
<span>总面积:</span>
<span style="color:#1890FF">{formatVal.val}</span>
<span style="margin-left:3px">{formatVal.unit}</span>
</div>
<div>
<span>单击添加点,双击结束</span>
</div>
</div>
)
}
}
}).$mount()
return obj.$el
}
/**
* 生成提示的 overlay
* */
newLabelOverlay(type, position, feature, val) {
const geom = feature.getGeometry()
let element = null
let yOffset = -36
if (geom instanceof LineString){
element = this.buildDistanceOverlayTipElement(type, feature, val)
}else{
element = this.buildAreaOverlayTipElement(type, feature, val)
yOffset = -58
}
const overlay = new Overlay({
element: element,
offset: [0, yOffset],
positioning: 'top-center'
})
overlay.setPosition(position)
return overlay
}
/**
* 交互完成处理
* */
drawEndHandler(e){
const feature = e.feature
const overlays = this.featureOverlayCaches[feature.getId()]
if (feature.getGeometry() instanceof Polygon){
if (overlays[0]){
overlays[0].setPosition(feature.getGeometry().getLastCoordinate())
overlays[0].setElement(this.buildAreaOverlayTipElement('end',feature,this.getPolygonArea(feature.getGeometry())))
overlays[0].setOffset([0,-36])
}
}
}
/**
* 重置测量 interaction
* */
resetMeasure() {
if (this.interaction){
this.olMap.map.removeInteraction(this.interaction)
this.interaction = null
}
}
/**
* 清空所有测量信息.但不清除交互
* */
clearMeasure() {
this.source.clear()
//清除overlay
for (let key in this.featureOverlayCaches){
const overlays = this.featureOverlayCaches[key]
for (let i=0;i<overlays.length;i++){
this.olMap.map.removeOverlay(overlays[i])
}
}
}
clearMeasureByKey(key){
this.source.removeFeature(this.source.getFeatureById(key))
const overlays = this.featureOverlayCaches[key]
for (let i=0;i<overlays.length;i++){
this.olMap.map.removeOverlay(overlays[i])
}
}
/**
* 停止测量,清除interaction.清空测量信息.隐藏图层
* */
stopMeasure(){
this.clearMeasure()
this.resetMeasure()
this.layer.setVisible(false)
}
destroy() {
//移除图层
this.olMap.removeLayer(MeasureTool.LayerName)
//移除交互
if (this.interactionId){
this.olMap.map.removeInteraction(this.interactionId)
}
//移除element
}
}
export default MeasureTool
效果
openlayers实现测量距离和面积
浙公网安备 33010602011771号