- 之前写过一个基于
mapbox-gl-draw
的maobox-gl测量工具。 - 在使用到一般项目中没啥问题,可在一些加载底图耗时较长的项目中,就有了问题:图层层级低或者工具加载慢的问题。
- 所以就更新一个原生的测量工具。
一、依赖库与版本
mapbox-gl(2.6.1)
和@turf/turf(6.5.0)
二、测量距离源码文件
import mapboxgl from "mapbox-gl"
import * as turf from "@turf/turf"
const initData = {
clickMeasurePointsFunction: null,
mapClickFunction: null,
mapMousemoveFunction: null,
mapDblClickFunction: null,
map: null,
isMeasure: true,
jsonPoint: { type: "FeatureCollection", features: [] },
jsonLine: { type: "FeatureCollection", features: [] },
points: [],
}
let before = null
let after = null
export function measureLineLength(mapObject) {
const currentMapContainerId = mapObject._container.getAttribute("id")
currentMapContainerId.indexOf("-after") !== -1
? (after = JSON.parse(JSON.stringify(initData)))
: (before = JSON.parse(JSON.stringify(initData)))
const resultData = getBeforeOrAfterDataByMapContainerIdHandler(mapObject)
resultData.isMeasure = true
resultData.map = mapObject
resultData.map.doubleClickZoom.disable()
resultData.map.getCanvas().style.cursor = "default"
clearMeasureLine(mapObject)
resultData.jsonPoint = { type: "FeatureCollection", features: [] }
resultData.jsonLine = { type: "FeatureCollection", features: [] }
resultData.points = []
const { mouseLabel, ele } = createMeasurLineLabelMarkerHandler(mapObject)
createMeasureLinePLLHandler(resultData.jsonPoint, resultData.jsonLine, mapObject)
let timer = null
function mapClickHandler(_e) {
timer = setTimeout(() => {
clearTimeout(timer)
if (resultData.isMeasure) {
const coords = [_e.lngLat.lng, _e.lngLat.lat]
addMeasureLineRes(resultData.points, resultData.jsonPoint, coords, mapObject)
addMeasureLinePoint(resultData.jsonPoint, resultData.jsonLine, coords, mapObject)
resultData.points.push(coords)
}
}, 100)
}
resultData.mapClickFunction = mapClickHandler
resultData.map.off("click", mapClickHandler)
resultData.map.on("click", mapClickHandler)
function mousemoveHandler(_e) {
if (resultData.isMeasure) {
const coords = [_e.lngLat.lng, _e.lngLat.lat]
if (resultData.jsonPoint.features.length > 0) {
const prev = resultData.jsonPoint.features[resultData.jsonPoint.features.length - 1]
const json = {
type: "Feature",
geometry: {
type: "LineString",
coordinates: [prev.geometry.coordinates, coords],
},
}
resultData.map.getSource("measure-line-move").setData(json)
let resultText = "起点"
if (resultData.points.length !== 0) {
const prev = resultData.jsonPoint.features[resultData.jsonPoint.features.length - 1]
const prevCoordinates = prev ? prev.geometry.coordinates : ""
const resultAngle = prevCoordinates
? getAngleHandle(prevCoordinates[0], prevCoordinates[1], coords[0], coords[1]).toFixed(1) + "°"
: ""
resultText = `<span>${getMeasureLineLength(
resultData.points,
coords
)} / ${resultAngle}</span><span>单击确定地点,双击结束</span>`
}
ele.innerHTML = resultText
} else {
ele.style.display = "block"
ele.innerHTML = "点击地图开始测量"
}
mouseLabel.setLngLat(coords)
}
}
resultData.mapMousemoveFunction = mousemoveHandler
resultData.map.off("mousemove", mousemoveHandler)
resultData.map.on("mousemove", mousemoveHandler)
function dblclickHandler(_e) {
if (timer) {
clearTimeout(timer)
}
if (resultData.isMeasure) {
const coords = [_e.lngLat.lng, _e.lngLat.lat]
addMeasureLinePoint(resultData.jsonPoint, resultData.jsonLine, coords, mapObject)
resultData.isMeasure = false
resultData.map.getCanvas().style.cursor = ""
mouseLabel.remove()
resultData.map.getSource("measure-line-move").setData({ type: "FeatureCollection", features: [] })
resultData.map.on("mouseover", "measure-line-points", (event) => {
if (resultData.isMeasure) {
return
}
resultData.map.getCanvas().style.cursor = "pointer"
const tipsCoords = [event.lngLat.lng, event.lngLat.lat]
const tipsEle = document.createElement("div")
tipsEle.setAttribute("class", "measure-line-result delete-tips")
tipsEle.innerHTML = "单击可删除此点"
const tipsOption = {
element: tipsEle,
anchor: "left",
offset: [10, 20],
}
new mapboxgl.Marker(tipsOption).setLngLat(tipsCoords).addTo(resultData.map)
})
resultData.map.on("mouseout", "measure-line-points", () => {
if (resultData.isMeasure) {
resultData.map.getCanvas().style.cursor = "default"
} else {
resultData.map.getCanvas().style.cursor = ""
}
const deleteTips = resultData.map._container.querySelector(".delete-tips")
if (deleteTips) {
deleteTips.remove()
}
})
function clickMeasurePointsHandler(event) {
if (resultData.jsonPoint.features.length > 2) {
const features = resultData.map.queryRenderedFeatures(event.point)
if (features.length > 0) {
const measureResult = resultData.map._container.querySelectorAll(".measure-line-result")
if (measureResult && measureResult.length > 0) {
Array.from(measureResult).forEach((m) => {
m.remove()
})
}
const { index } = features[0].properties
resultData.jsonPoint.features = resultData.jsonPoint.features.filter(
(feature) => feature.properties.index !== index
)
resultData.jsonLine.features = []
const featuresArr = [...resultData.jsonPoint.features]
const resultPoints = []
featuresArr.forEach((feature, index) => {
const nextIndex = index + 1
if (featuresArr[nextIndex]) {
const current = featuresArr[index]
const next = featuresArr[nextIndex]
resultData.jsonLine.features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: [current.geometry.coordinates, next.geometry.coordinates],
},
})
}
resultPoints.push(feature.geometry.coordinates)
const ele = document.createElement("div")
ele.style.color = "#e73f32"
ele.style.fontSize = "16px"
ele.style.fontWeight = "700"
ele.setAttribute("class", "measure-line-result")
if (index === 0) {
ele.innerHTML = "起点"
} else {
const prevIndex = index - 1
const prevCoordinates = featuresArr[prevIndex].geometry.coordinates
const currentCoordinates = feature.geometry.coordinates
const resultAngle = prevCoordinates
? getAngleHandle(
prevCoordinates[0],
prevCoordinates[1],
currentCoordinates[0],
currentCoordinates[1]
).toFixed(1) + "°"
: ""
ele.innerHTML = `${getMetersHandler(resultPoints, currentCoordinates)} / ${resultAngle}`
}
if (nextIndex === featuresArr.length) {
createCloseMarkerHandler(clickMeasurePointsHandler, feature.geometry.coordinates, mapObject)
}
const left = window.document.documentElement.clientWidth > 7000 ? 20 : 8
const option = {
element: ele,
anchor: "left",
offset: [left, 0],
}
new mapboxgl.Marker(option).setLngLat(feature.geometry.coordinates).addTo(resultData.map)
})
resultData.map.getSource("measure-line-points").setData(resultData.jsonPoint)
resultData.map.getSource("measure-line").setData(resultData.jsonLine)
resultData.map.getSource("measure-line-move").setData({ type: "FeatureCollection", features: [] })
}
} else {
closeMeasureLine(mapObject)
}
}
resultData.clickMeasurePointsFunction = clickMeasurePointsHandler
resultData.map.off("click", "measure-line-points", clickMeasurePointsHandler)
resultData.map.on("click", "measure-line-points", clickMeasurePointsHandler)
createCloseMarkerHandler(clickMeasurePointsHandler, coords, mapObject)
}
}
resultData.mapDblClickFunction = dblclickHandler
resultData.map.off("dblclick", dblclickHandler)
resultData.map.on("dblclick", dblclickHandler)
}
function clearMeasureLine(mapObject) {
const resultData = getBeforeOrAfterDataByMapContainerIdHandler(mapObject)
const measureResult = resultData.map._container.querySelectorAll(".measure-line-result")
if (measureResult && measureResult.length > 0) {
Array.from(measureResult).forEach((m) => {
m.remove()
})
}
const source = resultData.map.getSource("measure-line-points")
const json = {
type: "FeatureCollection",
features: [],
}
if (source) {
resultData.map.getSource("measure-line-points").setData(json)
resultData.map.getSource("measure-line-move").setData(json)
resultData.map.getSource("measure-line").setData(json)
}
}
function createMeasurLineLabelMarkerHandler(mapObject) {
const resultData = getBeforeOrAfterDataByMapContainerIdHandler(mapObject)
const ele = document.createElement("div")
ele.style.color = "#e73f32"
ele.style.fontSize = "16px"
ele.style.fontWeight = "700"
ele.style.display = "none"
ele.setAttribute("class", "measure-line-result")
const windowW = document.documentElement.clientWidth
const top = windowW > 7000 ? 120 : 44
const left = window.document.documentElement.clientWidth > 7000 ? 20 : 8
const option = {
element: ele,
anchor: "left",
offset: [left, top],
}
const mouseLabel = new mapboxgl.Marker(option).setLngLat([0, 0]).addTo(resultData.map)
return {
mouseLabel,
ele,
}
}
function createMeasureLinePLLHandler(jsonPoint, jsonLine, mapObject) {
const resultData = getBeforeOrAfterDataByMapContainerIdHandler(mapObject)
const source = resultData.map.getSource("measure-line-points")
if (source) {
resultData.map.getSource("measure-line-points").setData(jsonPoint)
resultData.map.getSource("measure-line-move").setData(jsonLine)
resultData.map.getSource("measure-line").setData(jsonLine)
} else {
resultData.map.addSource("measure-line-points", {
type: "geojson",
data: jsonPoint,
})
resultData.map.addSource("measure-line", {
type: "geojson",
data: jsonLine,
})
resultData.map.addSource("measure-line-move", {
type: "geojson",
data: jsonLine,
})
const windowW = document.documentElement.clientWidth
const LW = windowW > 7000 ? 6 : 2
const RW = windowW > 7000 ? 10.5 : 3.5
const CW = windowW > 7000 ? 7.5 : 2.5
resultData.map.addLayer({
id: "measure-line-move",
type: "line",
source: "measure-line-move",
paint: {
"line-color": "#ffdf40",
"line-width": LW,
"line-opacity": 1,
"line-dasharray": [0.5, 1],
},
})
resultData.map.addLayer({
id: "measure-line",
type: "line",
source: "measure-line",
paint: {
"line-color": "#ffdf40",
"line-width": LW,
"line-opacity": 1,
"line-dasharray": [0.5, 1],
},
})
resultData.map.addLayer({
id: "measure-line-points",
type: "circle",
source: "measure-line-points",
paint: {
"circle-color": "#ffffff",
"circle-radius": RW,
"circle-stroke-width": CW,
"circle-stroke-color": "#ffdf40",
},
})
}
moveLayerHandler(resultData.map)
}
function moveLayerHandler(mapObject) {
const { map } = getBeforeOrAfterDataByMapContainerIdHandler(mapObject)
const { layers } = map.getStyle()
const length = layers.length
const lastLayerId = map.getStyle().layers[length - 1].id
if (lastLayerId !== "measure-line-points") {
map.moveLayer("measure-line-points", map.getStyle().layers[length - 1].id)
map.moveLayer("measure-line", "measure-line-points")
map.moveLayer("measure-line-move", "measure-line")
}
}
function addMeasureLinePoint(jsonPoint, jsonLine, coords, mapObject) {
const { map } = getBeforeOrAfterDataByMapContainerIdHandler(mapObject)
if (jsonPoint.features.length > 0) {
const prev = jsonPoint.features[jsonPoint.features.length - 1]
jsonLine.features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: [prev.geometry.coordinates, coords],
},
})
map.getSource("measure-line").setData(jsonLine)
}
jsonPoint.features.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: coords,
},
properties: {
index: jsonPoint.features.length,
},
})
const data = [...jsonPoint.features]
const result = []
data.forEach((feature, index) => {
if (data[index - 1]) {
if (
data[index - 1].geometry.coordinates[0] !== feature.geometry.coordinates[0] ||
data[index - 1].geometry.coordinates[1] !== feature.geometry.coordinates[1]
) {
result.push(feature)
}
} else {
result.push(feature)
}
})
jsonPoint.features = [...result]
map.getSource("measure-line-points").setData(jsonPoint)
}
function getMeasureLineLength(points, coords) {
const _points = points.concat([coords])
const line = turf.lineString(_points)
let len = turf.length(line)
if (len < 1) {
len = Math.round(len * 1000) + "米"
} else {
len = len.toFixed(2) + "千米"
}
return len
}
function addMeasureLineRes(points, jsonPoint, coords, mapObject) {
const { map } = getBeforeOrAfterDataByMapContainerIdHandler(mapObject)
const ele = document.createElement("div")
ele.style.color = "#e73f32"
ele.style.fontSize = "16px"
ele.style.fontWeight = "700"
ele.setAttribute("class", "measure-line-result")
const left = window.document.documentElement.clientWidth > 7000 ? 20 : 8
const option = {
element: ele,
anchor: "left",
offset: [left, 0],
}
let resultText = "起点"
if (points.length !== 0) {
resultText = `${getMeasureLineLength(points, coords)}`
}
ele.innerHTML = resultText
new mapboxgl.Marker(option).setLngLat(coords).addTo(map)
}
function getMetersHandler(resultPoints, coords) {
if (resultPoints.length > 1) {
const _points = [...resultPoints]
const line = turf.lineString(_points)
let len = turf.length(line)
if (len < 1) {
len = Math.round(len * 1000) + "米"
} else {
len = len.toFixed(2) + "公里"
}
return len
}
}
function getAngleHandle(lng1, lat1, lng2, lat2) {
const a = ((90 - lat2) * Math.PI) / 180
const b = ((90 - lat1) * Math.PI) / 180
const AOC_BOC = ((lng2 - lng1) * Math.PI) / 180
const cosc = Math.cos(a) * Math.cos(b) + Math.sin(a) * Math.sin(b) * Math.cos(AOC_BOC)
const sinc = Math.sqrt(1 - cosc * cosc)
const sinA = (Math.sin(a) * Math.sin(AOC_BOC)) / sinc
const A = (Math.asin(sinA) * 180) / Math.PI
let res = 0
if (lng2 > lng1 && lat2 > lat1) {
res = A
} else if (lng2 > lng1 && lat2 < lat1) {
res = 180 - A
} else if (lng2 < lng1 && lat2 < lat1) {
res = 180 - A
} else if (lng2 < lng1 && lat2 > lat1) {
res = 360 + A
} else if (lng2 > lng1 && lat2 === lat1) {
res = 90
} else if (lng2 < lng1 && lat2 === lat1) {
res = 270
} else if (lng2 === lng1 && lat2 > lat1) {
res = 0
} else if (lng2 === lng1 && lat2 < lat1) {
res = 180
}
return res
}
function createCloseMarkerHandler(clickMeasurePointsHandler, coords, mapObject) {
const resultData = getBeforeOrAfterDataByMapContainerIdHandler(mapObject)
const ele = document.createElement("div")
ele.setAttribute("class", "measure-line-result close")
const left = window.document.documentElement.clientWidth > 7000 ? 20 : 8
const top = window.document.documentElement.clientWidth > 7000 ? -50 : -11
const option = {
element: ele,
anchor: "bottom-left",
offset: [left, top],
}
ele.innerHTML = "×"
new mapboxgl.Marker(option).setLngLat(coords).addTo(resultData.map)
ele.onclick = function (__e) {
__e.stopPropagation()
closeMeasureLine(resultData.map)
}
}
export function closeMeasureLine(mapObject) {
if (!mapObject) return
const resultData = getBeforeOrAfterDataByMapContainerIdHandler(mapObject)
if (!resultData) return
resultData.map.doubleClickZoom.enable()
clearMeasureLine(resultData.map)
resultData.map.off("click", resultData.mapClickFunction)
resultData.map.off("mousemove", resultData.mapMousemoveFunction)
resultData.map.off("dblclick", resultData.mapDblClickFunction)
resultData.map.off("click", "measure-line-points", resultData.clickMeasurePointsFunction)
resultData.isMeasure = false
resultData.map.getCanvas().style.cursor = ""
resultData.map.getSource("measure-line-move").setData({ type: "FeatureCollection", features: [] })
resultData.jsonPoint = { type: "FeatureCollection", features: [] }
resultData.jsonLine = { type: "FeatureCollection", features: [] }
resultData.points = []
}
function getBeforeOrAfterDataByMapContainerIdHandler(mapObject) {
if (!mapObject) return
const currentMapContainerId = mapObject._container.getAttribute("id")
return currentMapContainerId.indexOf("-after") !== -1 ? after : before
}
三、测量面积源码文件
import mapboxgl from "mapbox-gl"
import * as turf from "@turf/turf"
import { Message } from "element-ui"
import { map as mapboxMap } from "./map_fun"
let isMeasure = true
let points = []
let jsonPoint = {
type: "FeatureCollection",
features: [],
}
let jsonLine = {
type: "FeatureCollection",
features: [],
}
let jsonFirstLine = {
type: "FeatureCollection",
features: [],
}
let measureAreaEle = null
let tooltip = null
export function measureArea(map) {
isMeasure = true
map.doubleClickZoom.disable()
map.getCanvas().style.cursor = "default"
measureAreaEle = document.createElement("div")
measureAreaEle.style.color = "#e73f32"
measureAreaEle.style.fontSize = "16px"
measureAreaEle.style.fontWeight = "700"
measureAreaEle.setAttribute("class", "measure-area-result")
const option = {
element: measureAreaEle,
anchor: "left",
offset: [8, 0],
}
tooltip = new mapboxgl.Marker(option).setLngLat([0, 0]).addTo(map)
const source = map.getSource("measure-area-points")
if (source) {
map.getSource("measure-area-points").setData(jsonPoint)
map.getSource("measure-area-line").setData(jsonLine)
map.getSource("measure-area-first-line").setData(jsonFirstLine)
} else {
map.addSource("measure-area-points", {
type: "geojson",
data: jsonPoint,
})
map.addSource("measure-area-line", {
type: "geojson",
data: jsonLine,
})
map.addSource("measure-area-first-line", {
type: "geojson",
data: jsonLine,
})
map.addLayer({
id: "measure-area-line",
type: "fill",
source: "measure-area-line",
paint: {
"fill-color": "#ffdf40",
"fill-opacity": 0.1,
},
})
map.addLayer({
id: "line-area-stroke",
type: "line",
source: "measure-area-line",
paint: {
"line-color": "#ffdf40",
"line-width": 2,
"line-opacity": 1,
"line-dasharray": [0.5, 1],
},
})
map.addLayer({
id: "measure-area-first-line",
type: "line",
source: "measure-area-first-line",
paint: {
"line-color": "#ffdf40",
"line-width": 2,
"line-opacity": 1,
"line-dasharray": [0.5, 1],
},
})
map.addLayer({
id: "measure-area-points",
type: "circle",
source: "measure-area-points",
paint: {
"circle-color": "#ffffff",
"circle-radius": 3,
"circle-stroke-width": 2,
"circle-stroke-color": "#ffdf40",
},
})
}
map.on("click", drawPointHandler)
map.on("dblclick", drawResultHandler)
map.on("mousemove", drawMoveHandler)
}
function getArea(coords) {
let pts = points.concat([coords])
pts = pts.concat([points[0]])
const polygon = turf.polygon([pts])
let area = turf.area(polygon)
if (area < 1000) {
area = Math.round(area) + "m²" + ", 双击结束绘制"
} else {
area = (area / 1000000).toFixed(4) + "km²" + ", 双击结束绘制"
}
return area
}
function drawPointHandler(e) {
if (isMeasure) {
const coords = [e.lngLat.lng, e.lngLat.lat]
const map = e.target
points.push(coords)
jsonPoint.features.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: coords,
},
})
map.getSource("measure-area-points").setData(jsonPoint)
}
}
function drawResultHandler(e) {
if (isMeasure) {
if (points.length === 3) {
Message.error("测量时至少需要三个点,请才重新测量")
closeMeasureArea(mapboxMap)
measureArea(mapboxMap)
}
const kinks = turf.kinks(turf.polygon([[...points, points[0]]]))
if (kinks.features.length > 1) {
Message.error("测量时不能相交,请重新测量")
closeMeasureArea(mapboxMap)
measureArea(mapboxMap)
} else {
const coords = [e.lngLat.lng, e.lngLat.lat]
points.push(coords)
const features = turf.featureCollection(
points.map((item) => {
return turf.point(item)
})
)
const center = turf.center(features)
isMeasure = false
measureAreaEle.innerHTML = getArea(coords).split(",")[0]
tooltip.setLngLat(center.geometry.coordinates)
}
}
}
function drawMoveHandler(e) {
if (isMeasure) {
const coords = [e.lngLat.lng, e.lngLat.lat]
const map = e.target
const len = jsonPoint.features.length
if (len === 0) {
measureAreaEle.innerHTML = "点击地图开始测量"
} else {
let pts = points.concat([coords])
pts = pts.concat([points[0]])
const jsonFill = {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [pts],
},
}
const jsonLine = {
type: "Feature",
geometry: {
type: "LineString",
coordinates: [...pts],
},
}
map.getSource("measure-area-line").setData(jsonFill)
map.getSource("measure-area-first-line").setData(jsonLine)
if (len === 1) {
measureAreaEle.innerHTML = "点击地图继续绘制"
} else {
measureAreaEle.innerHTML = getArea(coords)
}
}
tooltip.setLngLat(coords)
}
}
function clearMeasure(map) {
const measureResult = map._container.querySelectorAll(".measure-area-result")
if (measureResult && measureResult.length > 0) {
Array.from(measureResult).forEach((m) => {
m.remove()
})
}
const json = {
type: "FeatureCollection",
features: [],
}
const sourceArea = map.getSource("measure-area-points")
if (sourceArea) {
map.getSource("measure-area-points").setData(json)
map.getSource("measure-area-line").setData(json)
map.getSource("measure-area-first-line").setData(json)
}
isMeasure = true
points = []
jsonPoint = {
type: "FeatureCollection",
features: [],
}
jsonLine = {
type: "FeatureCollection",
features: [],
}
measureAreaEle = null
tooltip = null
map.off("click", drawPointHandler)
map.off("dblclick", drawResultHandler)
map.off("mousemove", drawMoveHandler)
}
export function closeMeasureArea(map) {
if (!map) return
map.doubleClickZoom.enable()
clearMeasure(map)
}
四、使用方式
export const MEASURE_LINE = () => {
MEASURE_CLEAR()
measureLineLength(map)
}
export const MEASURE_AREA = () => {
MEASURE_CLEAR()
measureArea(map)
}
export const MEASURE_CLEAR = () => {
closeMeasureLine(map)
closeMeasureArea(map)
}