openlayers 测距离, 测面积

452 阅读3分钟

OpenLayers是一个开源的JavaScript库,用于在网页上展示地图和进行空间分析。使用OpenLayers进行测距,主要涉及到在地图上添加绘制线段的交互,并实时计算这些线段的长度。以下是一个基于OpenLayers进行测距的基本步骤和代码示例:

2024-07-08-18-06-38.gif

目录结构 image.png

OlMeasure/index.vue

<template>
  <NButtonGroup vertical size="tiny" class="OlMeasure">
    <slot></slot>
    <n-popover trigger="hover" placement="left">
      <template #trigger>
        <NButton :disabled="!current" type="error" @click="onClose">
          <RemixIcon icon="close-large-fill"></RemixIcon>
        </NButton>
      </template>
      <span>关闭</span>
    </n-popover>
  </NButtonGroup>
</template>

<script setup>
import {Style, Stroke, Fill, Circle} from "ol/style"
import VectorLayer from "ol/layer/Vector.js"
import VectorSource from "ol/source/Vector.js"

defineOptions({
  name: "OlMeasure"
})

let {map} = inject("openlayers")

// {'Point'}
// {'LineString'}
// {'LinearRing'}
// {'Polygon'}
// {'MultiPoint'}
// {'MultiLineString'}
// {'MultiPolygon'}
// {'GeometryCollection'}
// {'Circle'}

let style = new Style({
  fill: new Fill({
    color: "rgba(0, 0, 255, 0.2)"
  }),
  stroke: new Stroke({
    color: "yellow",
    lineDash: [10, 10],
    width: 2
  }),
  image: new Circle({
    radius: 5,
    stroke: new Stroke({
      color: "rgba(0, 0, 0, 0.7)"
    }),
    fill: new Fill({
      color: "rgba(255, 255, 255, 0.2)"
    })
  })
})

let source = new VectorSource()

let layer = new VectorLayer({
  source,
  style,
  zIndex: Infinity
})
map.addLayer(layer)

let current = ref(null)

watch(current, () => {
  source.clear()
})

const onClose = () => {
  current.value = null
}

provide("OlMeasure", {
  source,
  layer,
  current
})
</script>
<style lang="scss" scoped>
.OlMeasure {
  position: absolute;
  right: 10px;
  top: 20px;
  z-index: 1;
}
</style>

OlMeasure/config.js

import {getDistance} from "ol/sphere"
import numeral from "numeral"

export const getDistanceTotal = coordinates => {
  return coordinates.reduce((target, item, index) => {
    let next = coordinates[index + 1]
    if (!next) return target
    target += getDistance(item, next)
    return target
  }, 0)
}

export const formatDistance = value => {
  if (value > 10000) {
    return numeral(value / 1000).format("0.00") + "km"
  }
  return numeral(value).format("0.00") + "m"
}

测距离

OlMeasure/OlDistance/index.vue

<template>
  <n-popover trigger="hover" placement="left">
    <template #trigger>
      <NButton @click="onClick" :disabled="isShow" :type="isShow ? 'info' : 'primary'">
        <RemixIcon icon="ruler-fill"></RemixIcon>
      </NButton>
    </template>
    <span>测距</span>
  </n-popover>

  <div v-for="(item, index) in flagOverlayList" :key="index">
    <LineOverlay v-for="option in item.distance.filter((a, b) => b > 0)" v-bind="option" :index="index"></LineOverlay>
  </div>

  <OlOverlay :position="mousePosition" :offset="[0, -90]" v-if="isShow">
    <div class="mouse-overlay">
      <div v-if="currentFlag.length > 0">
        <p>总长: {{ formatDistance(totalLength) }}</p>
        <p>点击设置顶点,右键删除上一个点,双击结束测量</p>
      </div>
      <div v-else>
        <p>0米</p>
        <p>点击开始测量</p>
      </div>
    </div>
  </OlOverlay>
</template>

<script setup>
import {useEventListener} from "../../hooks"
import {last} from "lodash-es"
import {getDistanceTotal, formatDistance} from "../config"
import {getDistance} from "ol/sphere"
import LineOverlay from "./LineOverlay"
import {Draw} from "ol/interaction"

defineOptions({
  name: "OlDistance"
})

let {map} = inject("openlayers")
let {current, source} = inject("OlMeasure")

let isShow = computed(() => current.value === "distance")

let prevLength = ref(0)
let totalLength = ref(0)
let mousePosition = ref([])
let flagOverlayList = ref([])

let currentFlag = reactive([])

let draw = new Draw({
  source: source,
  type: "LineString"
})

provide("olDistance", {
  flagOverlayList
})

watch(current, value => {
  if (isShow.value) {
    map.addInteraction(draw)
  } else {
    map.removeInteraction(draw)
    flagOverlayList.value = []
    currentFlag = reactive([])
  }
})

draw.on("drawstart", ev => {
  flagOverlayList.value.push({
    distance: currentFlag,
    feature: ev.feature
  })
})
draw.on("drawend", ev => {
  let {feature} = ev
  let geometry = feature.getGeometry()
  let points = geometry.getCoordinates()

  let metre = getDistance(last(points), last(currentFlag).position)
  currentFlag.push({
    key: Math.random(),
    position: last(points),
    metre,
    total: currentFlag.reduce((a, b) => a + b.metre, metre),
    isEnd: true
  })

  currentFlag = reactive([])
})

useEventListener(map, "contextmenu", ev => {
  ev.preventDefault()

  draw.removeLastPoint()
  draw.removeLastPoint()
  currentFlag.pop()
})

useEventListener(map, "singleclick", ev => {
  if (!isShow.value) return

  currentFlag.push({
    key: Math.random(),
    position: ev.coordinate,
    metre: last(currentFlag) ? getDistance(ev.coordinate, last(currentFlag).position) : 0
  })
})
useEventListener(map, "pointermove", ev => {
  if (!isShow.value) return
  mousePosition.value = ev.coordinate

  if (currentFlag.length) {
    prevLength.value = getDistance(ev.coordinate, last(currentFlag).position)
    totalLength.value = getDistanceTotal([ev.coordinate, ...currentFlag.map(item => item.position)])
  }
})

const onClick = async () => {
  if (isShow.value) return

  current.value = "distance"
}
</script>
<style lang="scss">
.mouse-overlay {
  margin-left: -50%;
  margin-right: 50%;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 4px;
  color: lime;
  padding: 8px 18px 8px 12px;
  white-space: nowrap;
  z-index: 0;
  /* font-weight: bold; */
  position: relative;
  p {
    line-height: 150%;
  }
  &::before {
    border-top: 6px solid rgba(0, 0, 0, 0.5);
    border-right: 6px solid transparent;
    border-left: 6px solid transparent;
    content: "";
    position: absolute;
    bottom: -6px;
    margin-left: -7px;
    left: 50%;
  }
  &-record {
    margin-left: -50%;
    margin-right: 50%;
    background: rgba(0, 0, 0, 0.5);
    border-radius: 4px;
    color: #fff;
    padding: 8px 28px 8px 12px;
    white-space: nowrap;
    z-index: 0;
    /* font-weight: bold; */
    position: relative;
    font-size: 14px;
    &::before {
      border-top: 6px solid rgba(0, 0, 0, 0.5);
      border-right: 6px solid transparent;
      border-left: 6px solid transparent;
      content: "";
      position: absolute;
      bottom: -6px;
      margin-left: -7px;
      left: 50%;
    }
  }
  &-close {
    font-size: 20px;
    position: absolute;
    right: 0px;
    top: 0;
  }
}
</style>

OlMeasure/OlDistance/LineOverlay.vue

<template>
  <OlOverlay :position="position" :offset="[0, -50]">
    <div class="mouse-overlay-record" style="color: lime" v-if="isEnd">
      <b>总长:</b>
      <span>{{ formatDistance(total) }}</span>
      <RemixIcon
        @click="onClose(item, options, index)"
        class="mouse-overlay-close"
        icon="close-circle-fill"
      ></RemixIcon>
    </div>
    <div class="mouse-overlay-record" v-else>
      <b>本段长:</b>
      <span>{{ formatDistance(metre) }}</span>
    </div>
  </OlOverlay>
</template>

<script setup>
import {last} from "lodash-es"
import {formatDistance} from "../config"

let props = defineProps({
  position: Array,
  isEnd: Boolean,
  total: Number,
  metre: Number,
  index: Number
})

let {flagOverlayList} = inject("olDistance")
let {source} = inject("OlMeasure")
const onClose = () => {
  let remove = flagOverlayList.value.splice(props.index, 1)
  source.removeFeature(last(remove).feature)
}
</script>
<style lang="scss" scoped>
.mouse-overlay-record {
  margin-left: -50%;
  margin-right: 50%;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 4px;
  color: #fff;
  padding: 8px 20px 8px 12px;
  white-space: nowrap;
  z-index: 0;
  position: relative;
  font-size: 14px;
  .mouse-overlay-close {
    cursor: pointer;
  }
}
</style>

测面积

OlMeasure/OlArea/index.vue

<template>
  <n-popover trigger="hover" placement="left">
    <template #trigger>
      <NButton @click="onClick" :disabled="isShow" :type="isShow ? 'info' : 'primary'">
        <RemixIcon icon="map-fill"></RemixIcon>
      </NButton>
    </template>
    <span>测面积</span>
  </n-popover>

  <OlOverlay v-if="isShow" v-for="(item, index) in areaOverlay" :key="item.key" :position="item.center">
    <div class="mouse-overlay">
      <p>面积: {{ numeral(item.area).format("0.00") }}米²</p>
      <RemixIcon @click="onClose(item, index)" class="mouse-overlay-close" icon="close-circle-fill"></RemixIcon>
    </div>
  </OlOverlay>

  <OlOverlay :position="currentOverlay.position" :offset="[0, -90]" v-if="isShow">
    <div class="mouse-overlay">
      <div v-if="currentOverlay.area">
        <p>面积: {{ currentOverlay.area }}米²</p>
        <p>点击设置顶点,右键删除上一个点,双击结束测量</p>
      </div>
      <div v-else>
        <p>0米²</p>
        <p>点击开始测量</p>
      </div>
    </div>
  </OlOverlay>
</template>

<script setup>
import {Draw} from "ol/interaction"
import numeral from "numeral"
import {getArea} from "ol/sphere"
import {getCenter} from "ol/extent"
import {Polygon} from "ol/geom"

defineOptions({
  name: "OlArea"
})

let {map} = inject("openlayers")
let {current, source} = inject("OlMeasure")

let areaOverlay = ref([])
let currentOverlay = reactive({
  position: [0, 0],
  area: 0,
  key: 0
})

let draw = new Draw({
  source: source,
  type: "Polygon",
  minPoint: 3
})
let isShow = computed(() => current.value === "area")

watch(current, value => {
  if (isShow.value) {
    map.addInteraction(draw)
  } else {
    map.removeInteraction(draw)
    areaOverlay.value = []
    currentOverlay = reactive({
      position: [0, 0],
      area: 0,
      key: 0
    })
  }
})

useEventListener(map, "contextmenu", ev => {
  ev.preventDefault()

  draw.removeLastPoint()
  draw.removeLastPoint()
})

useEventListener(map, "pointermove", ev => {
  if (!isShow.value) return
  currentOverlay.position = ev.coordinate

  if (get(draw, ["sketchLineCoords_", "length"], 0) < 3) return

  let geom = new Polygon([draw.sketchLineCoords_])
  currentOverlay.area = getArea(geom, {projection: "EPSG:4326"})
})

draw.on("drawend", ev => {
  let {feature} = ev
  let geometry = feature.getGeometry()
  var extent = geometry.getExtent()

  areaOverlay.value.push({
    center: getCenter(extent),
    area: currentOverlay.area,
    key: Math.random(),
    feature
  })
  currentOverlay = reactive({
    position: [0, 0],
    area: 0
  })
})

const onClick = async () => {
  if (isShow.value) return

  current.value = "area"
}

const onClose = ({feature}, index) => {
  source.removeFeature(feature)
  areaOverlay.value.splice(index, 1)
}
</script>
<style lang="scss" scoped></style>