67.在 Vue 3 中使用 OpenLayers 测量距离和面积(引用封装代码版)

发布于:2025-02-10 ⋅ 阅读:(113) ⋅ 点赞:(0)

前言

在前端 GIS 开发中,测量距离和面积是一个非常常见的功能,尤其是在地图系统的构建中。本文将基于 Vue 3 Composition APIOpenLayers,通过引用封装好的测量工具类,实现地图距离和面积的测量功能。代码模块化,适用于项目中快速集成,同时也方便二次扩展。


项目准备

在开始之前,请确保你已经具备以下开发环境:

  1. Vue 3 项目:已创建基于 Vue CLI 或 Vite 的 Vue 3 工程。

  2. OpenLayers:安装最新的 OpenLayers 版本。使用以下命令安装:

    npm install ol

  3. Element-Plus:本文使用 Element-Plus 的按钮组件作为操作按钮。安装命令如下:

    npm install element-plus


实现功能

我们需要实现以下功能:

  1. 初始化地图,加载 OpenLayers 的基础瓦片图层(OSM)。
  2. 点击按钮时可以测量 长度面积
  3. 提供 清除功能,清空测量的图层。
  4. 提供模块化封装,方便代码复用。

效果预览

运行项目后,可以看到以下效果:

  1. 点击“测量长度”按钮,鼠标在地图上画线段,实时显示距离。
  2. 点击“测量面积”按钮,鼠标画多边形,实时显示面积。
  3. 点击“清除”按钮,移除所有测量结果。


封装测量工具类

我们先封装一个测量工具类,命名为 measure.js。代码如下:

import Draw from 'ol/interaction/Draw';
import VectorSource from 'ol/source/Vector';
import VectorLayer from 'ol/layer/Vector';
import Point from 'ol/geom/Point';
import { unByKey } from 'ol/Observable';
import Overlay from 'ol/Overlay';
import { Feature } from 'ol';
import { getArea, getLength } from 'ol/sphere';
import LineString from 'ol/geom/LineString';
import Polygon from 'ol/geom/Polygon';
import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style';

export default {
    measure(map, measureType, show) {
        let source = new VectorSource(); // 创建一个新的矢量源

        let sketch; // 当前绘制的要素

        let helpTooltipElement; // 帮助提示元素

        let helpTooltip; // 显示帮助消息的覆盖层

        let measureTooltipElement; // 测量提示元素

        let measureTooltip; // 显示测量结果的覆盖层

        const continuePolygonMsg = ''; // 绘制多边形时显示的消息

        const continueLineMsg = ''; // 绘制线条时显示的消息

        createMeasureTooltip(); // 创建测量提示
        createHelpTooltip(); // 创建帮助提示

        const pointerMoveHandler = function (evt) {
            if (evt.dragging) {
                return;
            }
            let helpMsg = '请点击开始绘制'; // 默认帮助消息
            if (sketch) {
                const geom = sketch.getGeometry();
                if (geom instanceof Polygon) {
                    helpMsg = continuePolygonMsg; // 如果是多边形,显示相应消息
                } else if (geom instanceof LineString) {
                    helpMsg = continueLineMsg; // 如果是线条,显示相应消息
                }
            }

            helpTooltipElement.innerHTML = helpMsg; // 更新帮助提示内容
            helpTooltip.setPosition(evt.coordinate); // 设置帮助提示位置

            helpTooltipElement.classList.remove('hidden'); // 显示帮助提示
        };

        map.on('pointermove', pointerMoveHandler); // 监听指针移动事件

        map.getViewport().addEventListener('mouseout', function () {
            helpTooltipElement.classList.add('hidden'); // 鼠标移出视口时隐藏帮助提示
        });

        let draw; // 绘制交互

        const formatLength = function (line) {
            const sourceProj = map.getView().getProjection(); // 获取投影坐标系
            const length = getLength(line, { projection: sourceProj }); // 计算长度
            let output;
            if (length > 100) {
                output = (Math.round(length / 1000 * 100) / 100) + ' km'; // 如果长度大于100米,显示为公里
            } else {
                output = (Math.round(length * 100) / 100) + ' m'; // 否则显示为米
            }
            return output;
        };

        const formatArea = function (polygon) {
            const sourceProj = map.getView().getProjection(); // 获取投影坐标系
            const area = getArea(polygon, { projection: sourceProj }); // 计算面积
            let output;
            if (area > 10000) {
                output = (Math.round(area / 1000000 * 100) / 100) + ' km<sup>2</sup>'; // 如果面积大于10000平方米,显示为平方公里
            } else {
                output = (Math.round(area * 100) / 100) + ' m<sup>2</sup>'; // 否则显示为平方米
            }
            return output;
        };

        for (const layerTmp of map.getLayers().getArray()) {
            if (layerTmp.get('name') == 'feature') {
                source = layerTmp.getSource(); // 获取存放要素的矢量层
            }
        }

        function addInteraction() {
            const type = (measureType == 'area' ? 'Polygon' : 'LineString'); // 根据测量类型设置绘制类型
            draw = new Draw({
                source: source,
                type: type,
                style: new Style({
                    fill: new Fill({
                        color: 'rgba(255, 255, 255, 0.2)', // 填充颜色
                    }),
                    stroke: new Stroke({
                        color: 'rgba(255, 0, 0, 0.5)', // 线条颜色
                        lineDash: [10, 10], // 虚线样式
                        width: 2, // 线条宽度
                    }),
                    image: new CircleStyle({
                        radius: 5, // 圆点半径
                        stroke: new Stroke({
                            color: 'rgba(0, 0, 0, 0.7)', // 圆点边框颜色
                        }),
                        fill: new Fill({
                            color: 'rgba(255, 255, 255, 0.2)', // 圆点填充颜色
                        }),
                    }),
                }),
            });
            map.addInteraction(draw); // 添加绘制交互

            let listener;
            draw.on('drawstart', function (evt) {
                sketch = evt.feature; // 设置当前绘制的要素

                let tooltipCoord = evt.coordinate; // 提示坐标

                listener = sketch.getGeometry().on('change', function (evt) {
                    const geom = evt.target;
                    let output;
                    if (geom instanceof Polygon) {
                        output = formatArea(geom); // 格式化面积
                        tooltipCoord = geom.getInteriorPoint().getCoordinates(); // 获取多边形内部点坐标
                    } else if (geom instanceof LineString) {
                        output = formatLength(geom); // 格式化长度
                        tooltipCoord = geom.getLastCoordinate(); // 获取线条最后一个点的坐���
                    }
                    measureTooltipElement.innerHTML = output; // 更新测量提示内容
                    measureTooltip.setPosition(tooltipCoord); // 设置测量提示位置
                });

                map.on('dblclick', function (evt) {
                    const point = new Point(evt.coordinate);
                    source.addFeature(new Feature(point)); // 添加双击点要素
                });
            });

            draw.on('drawend', function () {
                measureTooltipElement.className = 'tooltip tooltip-static'; // 设置测量提示样式
                measureTooltip.setOffset([0, -7]); // 设置测量提示偏移
                sketch = null; // 清空当前绘制的要素
                measureTooltipElement = null; // 清空测量提示元素
                createMeasureTooltip(); // 创建新的测量提示
                unByKey(listener); // 移除监听器
                map.un('pointermove', pointerMoveHandler); // 移除指针移动事件监听
                map.removeInteraction(draw); // 移除绘制交互
                helpTooltipElement.classList.add('hidden'); // 隐藏帮助提示
            });
        }

        function createHelpTooltip() {
            if (helpTooltipElement) {
                helpTooltipElement.parentNode.removeChild(helpTooltipElement); // 移除旧的帮助提示元素
            }
            helpTooltipElement = document.createElement('div');
            helpTooltipElement.className = 'tooltip hidden'; // 设置帮助提示样式
            helpTooltip = new Overlay({
                element: helpTooltipElement,
                offset: [15, 0], // 设置偏移
                positioning: 'center-left', // 设置定位方式
            });
            map.addOverlay(helpTooltip); // 添加帮助提示覆盖层
        }

        function createMeasureTooltip() {
            if (measureTooltipElement) {
                measureTooltipElement.parentNode.removeChild(measureTooltipElement); // 移除旧的测量提示元素
            }
            measureTooltipElement = document.createElement('div');
            measureTooltipElement.className = 'tooltip tooltip-measure'; // 设置测量提示样式
            measureTooltip = new Overlay({
                element: measureTooltipElement,
                offset: [0, -15], // 设置偏移
                positioning: 'bottom-center', // 设置定位方式
            });
            map.addOverlay(measureTooltip); // 添加测量提示覆盖层
        }

        addInteraction(); // 添加绘制交互

        const vector = new VectorLayer({
            name: 'lineAndArea',
            source: source,
            style: new Style({
                fill: new Fill({
                    color: 'rgba(255, 255, 255, 0.2)', // 填充颜色
                }),
                stroke: new Stroke({
                    color: 'red', // 线条颜色
                    width: 2, // 线条宽度
                }),
                image: new CircleStyle({
                    radius: 7, // 圆点半径
                    fill: new Fill({
                        color: '#ffcc33', // 圆点填充颜色
                    }),
                }),
            }),
            zIndex: 16, // 设置图层顺序
        });

        if (show && measureType) {
            map.addLayer(vector); // 显示测量图层
        } else {
            map.getOverlays().clear(); // 清除所有覆盖层

            map.getLayers().getArray().forEach((layer, index, array) => {
                if (layer.get('name') == 'lineAndArea') {
                    map.removeLayer(layer); // 移除测量图层
                }
            });
        }
    },
};

Vue 3 页面代码

接下来,我们在 src/views/MeasureMap.vue 文件中实现地图页面:

<template>
  <div class="container">
    <div class="w-full flex justify-center flex-wrap">
      <div class="font-bold text-[24px]">在Vue3中使用OpenLayers测量距离和面积( 引用封装代码版 )</div>
    </div>
    <h4>
      <el-button type="success" size="small" @click="getLength('length')">测量长度</el-button>
      <el-button type="success" size="small" @click="getArea('area')">测量面积</el-button>
      <el-button type="warning" size="small" @click="clear">清除</el-button>
    </h4>
    <p class="message">{
  
  { message }}</p>
    <div id="vue-openlayers"></div>
  </div>
</template>

<script lang="ts" setup>
import 'ol/ol.css';
import { ref, onMounted } from 'vue';
import { Map, View } from 'ol';
import Tile from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import MeasureTool from '@/utils/OpenLayersMeasure';

const map = ref<Map | null>(null);
const message = ref<string>('');

const initMap = () => {
  const raster = new Tile({
    source: new OSM(),
    name: 'OSM',
  });

  map.value = new Map({
    target: 'vue-openlayers',
    layers: [raster],
    view: new View({
      center: [-12000000, 4700000],
      zoom: 10,
    }),
  });
};

const getLength = (type: 'length' | 'area') => {
  clear();
  message.value = '请双击以测量长度';
  if (map.value) {
    MeasureTool.measure(map.value, type, true);
  }
};

const getArea = (type: 'length' | 'area') => {
  clear();
  message.value = '请单击以测量面积';
  if (map.value) {
    MeasureTool.measure(map.value, type, true);
  }
};

const clear = () => {
  message.value = '';
  if (map.value) {
    MeasureTool.measure(map.value, '', false);
  }
};

onMounted(() => {
  initMap();
});
</script>

<style scoped>
.container {
  width: 840px;
  height: 590px;
  margin: 50px auto;
  border: 1px solid #42B983;
}

#vue-openlayers {
  width: 800px;
  height: 400px;
  margin: 0 auto;
  border: 1px solid #42B983;
  position: relative;
}
.message{
  color: red;
  font-size: 18px;
  font-weight: bold;
}
</style>

    总结

    通过本文的示例代码,我们实现了基于 Vue 3OpenLayers 的测量距离和面积功能。代码封装后更加简洁,也更方便在项目中快速复用。如果你对 GIS 开发感兴趣,可以尝试扩展此功能,比如支持更多的测量样式、坐标显示等。

    喜欢本篇文章,记得点赞支持!如有问题,欢迎留言交流!


    网站公告

    今日签到

    点亮在社区的每一天
    去签到