Skip to content

useDrawLineString

简介

在地图上绘制线段

使用示例

点我查看代码
vue
<script lang="ts" setup>
import type { OlMapInst } from '@summeruse/ol'
import type { Coordinate } from 'ol/coordinate'
import {
  createStyle,
  createVectorLayer,
  getOSMLayer,
  OlMap,
  useDrawLineString,
  wgs84ToMercator,
} from '@summeruse/ol'
import { NButton, NCard, NConfigProvider, NDataTable, useMessage } from 'naive-ui'
import { Feature, Map as OLMap } from 'ol'
import { LineString } from 'ol/geom'
import { h, onMounted, ref, watch } from 'vue'

const olMapInst = ref<OlMapInst>()
const olMap = new OLMap()
olMap.addLayer(getOSMLayer())

const center = wgs84ToMercator([120, 30])

const message = useMessage()

interface Line {
  id: number
  name: string
  coordinates: Coordinate[]
}

const lineList = ref<Line[]>([])

const isDraw = ref(false)

function getData() {
  // 模拟数据
  return new Promise<Line[]>((resolve) => {
    setTimeout(() => {
      resolve([
        {
          id: 1,
          name: '航线1',
          coordinates: [
            [13319814.6329371, 3473861.2239869847],
            [13324095.10652107, 3480281.9343629396],
            [13328834.20227475, 3483033.667381206],
            [13331127.313123306, 3487467.015021746],
            [13335713.534820417, 3488078.5112480274],
            [13340299.756517528, 3491900.3626622865],
            [13342592.867366083, 3496639.4584159674],
            [13346720.466893481, 3497709.57681196],
          ],
        },
        {
          id: 2,
          name: '航线2',
          coordinates: [
            [13382340.122074375, 3532777.4521949184],
            [13378824.018773258, 3530637.2154029333],
            [13374084.923019577, 3529108.47483723],
            [13371333.19000131, 3527579.7342715263],
            [13372250.434340732, 3523452.134744127],
            [13376989.530094413, 3519171.661160157],
            [13378365.396603547, 3516725.6762550315],
          ],
        },
      ])
    }, 100)
  })
}

const columns = [
  {
    title: '名称',
    key: 'name',
    width: 100,
  },
  {
    title: '操作',
    key: 'action',
    render(row: any) {
      return h(
        'div',
        {
          class: 'flex justify-center items-center gap-2',
        },
        [
          h(
            NButton,
            {
              type: 'info',
              disabled: isDraw.value,
              onClick: () => {
                editLine(row)
              },
            },
            { default: () => '编辑' },
          ),
          h(
            NButton,
            {
              type: 'error',
              disabled: isDraw.value,
              onClick: () => {
                lineList.value = lineList.value.filter(item => item.id !== row.id)
              },
            },
            { default: () => '删除' },
          ),
        ],
      )
    },
  },
]

const { source, layer: vectorLayer } = createVectorLayer({
  style: createStyle({
    strokeOptions: {
      color: 'rgba(0, 0, 255, 1.0)',
      width: 2,
    },
  }),
})
olMap.addLayer(vectorLayer)

watch(
  () => lineList.value,
  (newVal) => {
    source.clear()
    newVal.forEach((item) => {
      const feature = new Feature({
        geometry: new LineString(item.coordinates),
      })
      source.addFeature(feature)
    })
  },
  {
    deep: true,
    immediate: true,
  },
)

const drawId = ref<number>()

const { start, stop, coordinates, setFeatures, features } = useDrawLineString(olMap, {
  size: 1,
})

function editLine(line?: Line) {
  vectorLayer.setOpacity(0.2)
  isDraw.value = true
  drawId.value = line?.id || undefined
  setFeatures(line ? [line.coordinates] : [])
  start()
}

function addLine() {
  editLine()
}

function saveLine() {
  if (features.value.length === 0)
    return message.error('请绘制航线')
  if (drawId.value) {
    const index = lineList.value.findIndex(item => item.id === drawId.value)
    lineList.value[index].coordinates = coordinates.value[0]
  }
  else {
    lineList.value.push({
      id: lineList.value.length + 1,
      name: `航线${lineList.value.length + 1}`,
      coordinates: coordinates.value[0],
    })
  }
  setFeatures([])
  stop()
  isDraw.value = false
  drawId.value = undefined
  vectorLayer.setOpacity(1)
}

onMounted(async () => {
  const res = await getData()
  lineList.value = res
})
</script>

<template>
  <NConfigProvider>
    <NCard title="航线列表" class="mb-2">
      <NDataTable :columns :data="lineList" />
      <NButton @click="addLine">
        添加航线
      </NButton>
      <NButton :disabled="!isDraw" @click="saveLine">
        保存
      </NButton>
    </NCard>
    <OlMap ref="olMapInst" class="w-100% h-400px" :zoom="10" :center :ol-map />
  </NConfigProvider>
</template>

源代码

点我查看代码
ts
import type { Coordinate } from 'ol/coordinate'
import type { Style } from 'ol/style'
import type { VNode } from 'vue'
import type { OLMap } from '../../types'
import type { StyleOptions } from '../../utils'
import { Feature, Overlay } from 'ol'
import { LineString } from 'ol/geom'
import { Draw, Modify } from 'ol/interaction'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import { computed, onMounted, onUnmounted, ref, render } from 'vue'
import { createStyle } from '../../utils'
import { getExtentCenter } from '../../utils/calculate'
import { mercatorExtentToWgs84, wgs84ToMercator } from '../../utils/projection'
import { createToolTipElement } from '../common'

export interface DrawLineStringOptions {
  defaultCoordinates?: Coordinate[][] // 默认线条坐标
  deletePointLabel?: VNode // 删除点的标签
  deleteFeatureLabel?: VNode // 删除线的标签
  style?: Style // 线条样式
  styleOptions?: StyleOptions
  drawStyle?: Style // 绘制时的样式
  drawStyleOptions?: StyleOptions
  modifyStyle?: Style // 修改时的样式
  modifyStyleOptions?: StyleOptions
  zIndex?: number // 图层z-index
  size?: number // 最大线条数量
}

export function useDrawLineString(olMap: OLMap, options: DrawLineStringOptions) {
  const inDraw = ref(true)

  const features = ref<Feature<LineString>[]>([])

  const coordinates = computed(() => {
    return features.value.map((feature) => {
      return feature.getGeometry()!.getCoordinates()
    })
  })

  const source = new VectorSource() // 创建一个矢量图层
  const style = options.styleOptions ? createStyle(options.styleOptions) : options?.style
  const layer = new VectorLayer({
    source,
    style,
    zIndex: options?.zIndex,
  })
  olMap.addLayer(layer)
  const drawStyle = options.drawStyleOptions ? createStyle(options.drawStyleOptions) : options?.drawStyle
  const draw = new Draw({
    source,
    style: drawStyle || style,
    type: 'LineString',
  })
  draw.setActive(false) // 默认不激活
  const modifyStyle = options.modifyStyleOptions ? createStyle(options.modifyStyleOptions) : options?.modifyStyle
  const modify = new Modify({
    source,
    style: modifyStyle || style,
  })
  const overlaySet = new Set<Overlay>()
  olMap.addInteraction(draw)
  onMounted(() => {
    olMap.addInteraction(modify)
  })

  source.on('addfeature', () => {
    if (options.size) {
      if (source.getFeatures().length >= options.size) {
        draw.setActive(false)
      }
    }
  })
  source.on('removefeature', () => {
    if (options.size) {
      if (source.getFeatures().length < options.size) {
        draw.setActive(true)
      }
    }
  })

  const clearOverlay = () => {
    overlaySet.forEach((overlay) => {
      olMap.removeOverlay(overlay)
    })
    overlaySet.clear()
  }

  source.on('change', () => {
    clearOverlay()
    source.getFeatures().forEach((feature) => {
      const geometry = feature.getGeometry() as LineString
      const center = getExtentCenter(mercatorExtentToWgs84(geometry.getExtent()))
      let element = document.createElement('div')
      if (options.deleteFeatureLabel) {
        render(options.deleteFeatureLabel, element)
      }
      else {
        element = createToolTipElement('删除线')
      }
      element.addEventListener('click', () => {
        source.removeFeature(feature)
      })
      const overlay = new Overlay({
        position: wgs84ToMercator(center),
        positioning: 'center-center',
        element,
      })
      overlaySet.add(overlay)
      olMap.addOverlay(overlay)

      const coordinates = geometry.getCoordinates()
      if (coordinates.length < 3)
        return
      coordinates.forEach((coordinate, index) => {
        let element = document.createElement('div')
        if (options.deletePointLabel) {
          render(options.deletePointLabel, element)
        }
        else {
          element = createToolTipElement('删除点')
        }
        element.addEventListener('click', () => {
          feature.setGeometry(new LineString(coordinates.filter((_, i) => i !== index)))
        })
        const overlay = new Overlay({
          position: coordinate,
          positioning: 'top-center',
          offset: [0, -30],
          element,
        })
        overlaySet.add(overlay)
        olMap.addOverlay(overlay)
      })
    })
  })

  draw.on('change:active', () => {
    inDraw.value = draw.getActive()
  })

  source.on('change', () => {
    features.value = source.getFeatures() as Feature<LineString>[]
  })

  /** 销毁 */
  const destroy = () => {
    clearOverlay()
    olMap.removeLayer(layer)
    olMap.removeInteraction(draw)
    olMap.removeInteraction(modify)
  }

  const start = () => {
    if (options.size && features.value.length >= options.size)
      return
    draw.setActive(true)
  }

  const stop = () => {
    draw.setActive(false)
  }

  /** 重新设置坐标 */
  const setFeatures = (coordinates: Coordinate[][]) => {
    source.clear()
    coordinates.forEach((coordinate) => {
      if (coordinate.length < 2)
        return
      const feature = new Feature({
        geometry: new LineString(coordinate),
      })
      source.addFeature(feature)
    })
  }

  /** 恢复到默认 */
  const reset = () => {
    setFeatures(options.defaultCoordinates || [])
  }

  reset() // 初始化

  /** 停止绘制并清空 */
  const clear = () => {
    source.clear()
    stop()
  }

  onUnmounted(() => {
    destroy()
  })

  return {
    inDraw,
    start,
    stop,
    clear,
    setFeatures,
    reset,
    features,
    coordinates,
    destroy,
  }
}

Released under the ISC License.