惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
AI
AI
博客园 - 三生石上(FineUI控件)
腾讯CDC
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Y
Y Combinator Blog
大猫的无限游戏
大猫的无限游戏
H
Hackread – Cybersecurity News, Data Breaches, AI and More
雷峰网
雷峰网
NISL@THU
NISL@THU
S
Schneier on Security
T
Threatpost
T
Tenable Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
IT之家
IT之家
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog
C
Cybersecurity and Infrastructure Security Agency CISA
P
Privacy & Cybersecurity Law Blog
I
Intezer
Microsoft Azure Blog
Microsoft Azure Blog
月光博客
月光博客
T
Threat Research - Cisco Blogs
SecWiki News
SecWiki News
AWS News Blog
AWS News Blog
博客园 - Franky
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Proofpoint News Feed
V
V2EX
Recorded Future
Recorded Future
Microsoft Security Blog
Microsoft Security Blog
S
Secure Thoughts
Google DeepMind News
Google DeepMind News
MongoDB | Blog
MongoDB | Blog
Apple Machine Learning Research
Apple Machine Learning Research
Project Zero
Project Zero
PCI Perspectives
PCI Perspectives
G
GRAHAM CLULEY
Help Net Security
Help Net Security
Cloudbric
Cloudbric
Recent Announcements
Recent Announcements
V
Visual Studio Blog
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
C
CERT Recently Published Vulnerability Notes
The Cloudflare Blog
Forbes - Security
Forbes - Security
C
Cisco Blogs
O
OpenAI News
www.infosecurity-magazine.com
www.infosecurity-magazine.com

博客园 - ^Mao^

不同缩放下适配 vxe-table 3D饼图 element-plus el-select html导出pdf openlayers增加移动功能 适配 部分页面统计用户访问时长 openlayers基本使用(街景+标注+绘制) 使用openlayer绘制街景地图 正则表达式--取对应表达式的值 vue2实现el-table-column多级效果 echarts-雷达图 echarts--地图 散点图效果 echarts散点图区域设置 vant 的vant-uploader组件问题 Threejs学习 Echarts-普通地图和3D地图实现 test
openlayer实现给线的附近添加点,点支持移动
^Mao^ · 2026-02-05 · via 博客园 - ^Mao^

参考文章:https://mp.weixin.qq.com/s/ghZkTxlUIdm6k6pPoura5g

<template>
  <div class="app">
    <div class="operate-container">
      <div class="operate-row">
        <div class="title">渲染操作</div>
        <div class="operate-box">
          <span class="start-draw" @click="onRenderPoint">渲染单个点</span>
          <span class="start-draw" @click="onRenderPointLinePolygon">渲染点、线、多边形</span>
        </div>
      </div>
      <div class="operate-row">
        <div class="title">其他操作</div>
        <div class="operate-box">
          <span class="start-draw" @click="onAddTag">增加标注</span>
          <span class="start-draw" @click="onShowPopup">增加弹窗信息</span>
          <span class="start-draw" @click="onMove">移动</span>
          <span class="start-draw" @click="onPointLimit">点只能在线上移动</span>
        </div>
      </div>
      <div class="operate-row">
        <div class="title">
          需求-1:在线的附近添加点,添加的点需附着在线上,点支持移动。(可以理解为你需要在某一处街道添加商店)
        </div>
        <div class="operate-box">
          <span @click="onDrawLine">绘制线</span>
          <span @click="onStartRecordPoint">点信息记录开关</span>
          <span @click="onLineAddPoint">线上添加点</span>
        </div>
      </div>
    </div>
    <div id="map">
      <div class="info-window" ref="info_window_ref">
        <div class="header">
          <div class="title">弹窗标题</div>
          <span class="close" @click="hideInfoWindow">
            <el-icon><Close /></el-icon>
          </span>
        </div>
        <div class="info-content" v-html="info_content"></div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import Map from "ol/Map.js";
import XYZ from "ol/source/XYZ";
import SourceVector from "ol/source/vector";
import LayerVector from "ol/layer/vector";
import Tile from "ol/layer/Tile.js";
import View from "ol/View.js";
import { Style, Circle, Fill, Stroke, Text } from "ol/style";
import { Draw, Translate } from "ol/interaction";
import { toLonLat, fromLonLat, transform } from "ol/proj";
import { Feature } from "ol";
import { Point, LineString, Polygon } from "ol/geom";
import { createEmpty, extend, createOrUpdate } from "ol/extent";
import { getDistance } from "ol/sphere";

const ol = {
  geom: {
    Point,
    LineString,
    Polygon,
  },
  proj: {
    fromLonLat,
    toLonLat,
    transform,
  },
  Feature,
  extent: {
    createEmpty,
    extend,
    createOrUpdate,
  },
  style: {
    Text,
    Fill,
    Stroke,
    Style,
    Circle,
  },
  Map,
  layer: {
    Tile,
    vector: LayerVector,
  },
  source: {
    XYZ,
    vector: SourceVector,
  },
  View,
  interaction: {
    Translate,
    Draw,
  },
  sphere: {
    getDistance,
  },
};

const map_instance = ref();

const render_vector_source = ref();
const render_vector_layer = ref();

const defaultStyle = (feature) => {
  var textStyle = new ol.style.Text({
    text: feature.get("name"),
    font: "bold 12px Arial",
    fill: new ol.style.Fill({
      color: "#fff",
    }),
    stroke: new ol.style.Stroke({
      color: "#000",
      width: 2,
    }),
    offsetY: -15, // 文字相对于点的垂直偏移
  });

  var pointStyle = new ol.style.Style({
    image: new ol.style.Circle({
      radius: 5,
      fill: new ol.style.Fill({
        color: "#ff0000",
      }),
      stroke: new ol.style.Stroke({
        color: "#ffffff",
        width: 2,
      }),
    }),
    text: textStyle,
    stroke: new ol.style.Stroke({
      color: "#1677ff",
      width: 3,
    }),
    fill: new ol.style.Fill({
      color: "rgba(22, 119, 255, 0.3)",
    }),
  });

  return pointStyle;
};

const highlightStyle = (feature) =>
  new ol.style.Style({
    image: new ol.style.Circle({
      radius: 5,
      fill: new ol.style.Fill({
        color: "#ffff00",
      }),
      stroke: new ol.style.Stroke({
        color: "#ffffff",
        width: 2,
      }),
    }),
    text: new ol.style.Text({
      text: feature.get("name"),
      font: "bold 12px Arial",
      fill: new ol.style.Fill({
        color: "#fff",
      }),
      stroke: new ol.style.Stroke({
        color: "#000",
        width: 2,
      }),
      offsetY: -15, // 文字相对于点的垂直偏移
    }),
    stroke: new ol.style.Stroke({
      color: "#ffff00",
      width: 3,
    }),
    fill: new ol.style.Fill({
      color: "rgba(255, 255, 0, 0.5)",
    }),
  });

let currentDraggedFeature = null;

const setRenderLayer = () => {
  const vectorSource = new ol.source.vector({
    wrapX: false,
  });

  const vectorLayer = new ol.layer.vector({
    source: vectorSource,
    style: defaultStyle,
  });

  map_instance.value.addLayer(vectorLayer);
  render_vector_source.value = vectorSource;
  render_vector_layer.value = vectorLayer;
};

let currentCoordinate = null;

let lineGeometry = null;
const setMapListener = () => {
  const map = map_instance.value;
  map.on("singleclick", function (evt) {
    currentCoordinate = null;
    hideInfoWindow();
    const feature = map.forEachFeatureAtPixel(evt.pixel, function (feat) {
      return feat;
    });
    if (feature) {
      const data = feature.get("data");
      const temp_content = `
        <div class='info-row'>
          <span class='info-label'>姓名</span>
          <span class='info-value'>${data.name}</span>
        </div>
        <div class='info-row'>
          <span class='info-label'>年龄</span>
          <span class='info-value'>${data.age}</span>
        </div>
        <div class='info-row'>
          <span class='info-label'>住址</span>
          <span class='info-value'>${data.address}</span>
        </div>
      `;
      info_content.value = temp_content;

      currentCoordinate = feature.getGeometry().getExtent();
      updateWindowPosition();
    }
  });

  map.on("postrender", function () {
    if (currentCoordinate && info_window_ref.value.style.display === "block") {
      updateWindowPosition();
    }
  });

  map.on("click", function (evt) {
    const mercatorCoord = evt.coordinate;

    const lonLat = ol.proj.transform(
      mercatorCoord,
      "EPSG:3857", // 源投影(地图默认投影)
      "EPSG:4326", // 目标投影(经纬度投影)
    );

    const longitude = lonLat[0].toFixed(6);
    const latitude = lonLat[1].toFixed(6);

    console.log("点击位置经纬度==============:");
    console.log("经度:", longitude, " 纬度:", latitude);
    click_post_info.value = {
      latitude,
      longitude,
    };
  });
};

const setTranslateInteraction = () => {
  const translateInteraction = new ol.interaction.Translate({
    // 配置1:指定可移动的图层(仅该图层的要素可拖拽,推荐) -->【这种我试了不生效】
    // layers: [render_vector_layer.value],
    sources: [render_vector_source.value],
    // 配置2:可选 - 指定可移动的要素(通过过滤函数,示例:仅允许点移动)
    // filter: function (feature) {
    //   return feature.getGeometry() instanceof ol.geom.Point;
    // },
  });

  map_instance.value.addInteraction(translateInteraction);

  translateInteraction.on("translateend", function (evt) {
    const movedFeature = evt.features.item(0);
    if (!movedFeature) return;

    if (currentDraggedFeature) {
      currentDraggedFeature.setStyle(null);
      currentDraggedFeature = null;
    }

    const geometry = movedFeature.getGeometry();
    const geometryType = geometry.getType();

    let lonLatList = [];

    switch (geometryType) {
      // 3.1 点要素(Point)
      case "Point":
        const pointCoord = geometry.getCoordinates();
        const pointLonLat = ol.proj.toLonLat(pointCoord);
        lonLatList.push({
          type: "Point",
          lon: pointLonLat[0].toFixed(6), // 保留6位小数,避免精度冗余
          lat: pointLonLat[1].toFixed(6),
        });
        break;

      // 3.2 线要素(LineString)
      case "LineString":
        const lineCoords = geometry.getCoordinates(); // 获取线的所有节点投影坐标数组
        lineCoords.forEach((coord) => {
          const lineLonLat = ol.proj.toLonLat(coord);
          lonLatList.push({
            type: "LineString",
            lon: lineLonLat[0].toFixed(6),
            lat: lineLonLat[1].toFixed(6),
          });
        });
        break;

      // 3.3 多边形要素(Polygon)
      case "Polygon":
        const polygonCoords = geometry.getCoordinates()[0]; // 多边形外层闭合环坐标(二维数组,取第0项)
        polygonCoords.forEach((coord) => {
          const polygonLonLat = ol.proj.toLonLat(coord);
          lonLatList.push({
            type: "Polygon",
            lon: polygonLonLat[0].toFixed(6),
            lat: polygonLonLat[1].toFixed(6),
          });
        });
        break;
    }

    console.log("拖拽结束,最终经纬度数据:", lonLatList);
  });

  translateInteraction.on("translatestart", function (evt) {
    currentDraggedFeature = evt.features.item(0);
    if (currentDraggedFeature) {
      currentDraggedFeature.setStyle(highlightStyle);
    }
  });

  translateInteraction.on("translating", function () {
    if (currentDraggedFeature) {
      currentDraggedFeature.setStyle(highlightStyle);
    }

    // 仅限制点要素
    const geometryType = currentDraggedFeature.getGeometry().getType();
    if (geometryType !== "Point") return;
    // 3. 核心:获取拖拽中点的当前坐标,计算线要素上的最近点
    const currentPointCoord = currentDraggedFeature.getGeometry().getCoordinates();
    // ol.extent.createOrUpdate 确保范围有效
    const closestPointCoord = lineGeometry.getClosestPoint(
      currentPointCoord,
      ol.extent.createOrUpdate(currentPointCoord),
    );

    // 4. 将点的坐标更新为线的最近点(实现吸附,限制只能在线上移动)
    currentDraggedFeature.getGeometry().setCoordinates(closestPointCoord);
  });
};

const initMap = () => {
  const map = new ol.Map({
    target: "map",
    layers: [
      new ol.layer.Tile({
        source: new ol.source.XYZ({
          // ArcGIS卫星影像
          // url: "https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png",
          // 高德影像图层
          url: "https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}",
        }),
      }),
    ],
    view: new ol.View({
      center: [0, 0],
      zoom: 1,
    }),
  });
  map_instance.value = map;

  setRenderLayer();
  setMapListener();
};

onMounted(() => {
  initMap();
});

const onRenderPoint = () => {
  const newPoint = new ol.Feature({
    geometry: new ol.geom.Point(ol.proj.fromLonLat([116.286, 39.991])),
    name: "动态添加-点要素(颐和园)",
  });

  render_vector_source.value.addFeature(newPoint);

  map_instance.value.getView().fit(newPoint.getGeometry().getExtent(), {
    padding: [50, 50, 50, 50],
    duration: 1000,
  });

  console.log("单个点要素添加完成!");
};

const onRenderPointLinePolygon = () => {
  const newFeatures = [
    new ol.Feature({
      geometry: new ol.geom.Point(ol.proj.fromLonLat([116.306, 40.028])),
      name: "测试-点",
    }),
    new ol.Feature({
      geometry: new ol.geom.LineString([
        ol.proj.fromLonLat([116.286, 39.991]),
        ol.proj.fromLonLat([116.306, 40.008]),
      ]),
      name: "测试-线",
    }),
    new ol.Feature({
      geometry: new ol.geom.Polygon([
        [
          ol.proj.fromLonLat([116.29, 40.0]),
          ol.proj.fromLonLat([116.32, 40.0]),
          ol.proj.fromLonLat([116.32, 40.02]),
          ol.proj.fromLonLat([116.29, 40.02]),
          ol.proj.fromLonLat([116.29, 40.0]),
        ],
      ]),
      name: "测试-多边形",
    }),
  ];

  render_vector_source.value.addFeatures(newFeatures);

  const extent = ol.extent.createEmpty();
  newFeatures.forEach((feature) => {
    ol.extent.extend(extent, feature.getGeometry().getExtent());
  });
  map_instance.value.getView().fit(extent, {
    padding: [50, 50, 50, 50],
    duration: 500,
  });

  console.log("线+多边形要素批量添加完成!");
};

const onAddTag = () => {
  // 点
  const feature = new ol.Feature({
    geometry: new ol.geom.Point(ol.proj.fromLonLat([113.264296724632, 23.129292061233613])),
    name: "点标注-广州",
  });
  // 2.1 使用图层中的统一样式
  render_vector_source.value.addFeature(feature);
  // 2.2 单独设置
  const feature_2 = new ol.Feature({
    geometry: new ol.geom.Point(ol.proj.fromLonLat([121.45570043159523, 31.24960210236224])),
    name: "点标注-上海",
  });
  feature_2.setStyle(
    new ol.style.Style({
      image: new ol.style.Circle({
        radius: 10,
        fill: new ol.style.Fill({
          color: "#ff9900",
        }),
        stroke: new ol.style.Stroke({
          color: "#ffffff",
          width: 2,
        }),
      }),
      text: new ol.style.Text({
        text: feature_2.get("name"),
        font: "16px bold Microsoft YaHei, sans-serif", // 加粗+更大字体
        fill: new ol.style.Fill({
          color: "#ff0000", // 红色文字
        }),
        stroke: new ol.style.Stroke({
          color: "#ffffff",
          width: 2,
        }),
        offsetY: -20, // 更大的偏移量(因为点半径更大)
        textAlign: "center",
        // 可选:添加文字背景框,提升可读性
        backgroundFill: new ol.style.Fill({
          color: "rgba(255, 255, 255, 0.8)",
        }),
        backgroundStroke: new ol.style.Stroke({
          color: "#ff9900",
          width: 1,
        }),
        padding: [3, 6, 3, 6], // 背景框内边距
      }),
    }),
  );
  render_vector_source.value.addFeature(feature_2);

  // 线
  // const feature = new ol.Feature({
  //   geometry: new ol.geom.LineString([
  //     ol.proj.fromLonLat([116.286, 39.991]),
  //     ol.proj.fromLonLat([116.306, 40.008]),
  //   ]),
  //   name: "测试线",
  // });
  // render_vector_source.value.addFeature(feature);

  // 多边形
  // const feature = new ol.Feature({
  //   geometry: new ol.geom.Polygon([
  //     [
  //       ol.proj.fromLonLat([116.29, 40.0]),
  //       ol.proj.fromLonLat([116.32, 40.0]),
  //       ol.proj.fromLonLat([116.32, 40.02]),
  //       ol.proj.fromLonLat([116.29, 40.02]),
  //       ol.proj.fromLonLat([116.29, 40.0]),
  //     ],
  //   ]),
  //   name: "多边形-标记",
  // });
  // render_vector_source.value.addFeature(feature);
  // // 地图定位到该点
  // map_instance.value.getView().fit(feature.getGeometry().getExtent(), {
  //   padding: [50, 50, 50, 50],
  //   duration: 500,
  // });
};

const info_content = ref("");
const info_window_ref = ref("");
const loadPointData = () => {
  const feature = new ol.Feature({
    geometry: new ol.geom.Point(ol.proj.fromLonLat([113.264296724632, 23.129292061233613])),
    name: "点标注-广州",
    data: {
      name: "张三",
      age: 18,
      address: "上海",
    },
  });
  render_vector_source.value.addFeature(feature);
};

const hideInfoWindow = () => {
  info_window_ref.value.style.display = "none";
  info_content.value = "";
};

const updateWindowPosition = () => {
  if (!currentCoordinate) return;
  const pixel = map_instance.value.getPixelFromCoordinate(currentCoordinate);
  let left = pixel[0] + 15;
  let top = pixel[1] - 15;
  const infoWindowEl = info_window_ref.value;
  infoWindowEl.style.left = left + "px";
  infoWindowEl.style.top = top + "px";
  infoWindowEl.style.display = "block";
};

const onShowPopup = () => {
  loadPointData();
};

const onMove = () => {
  setTranslateInteraction();
};

const onPointLimit = () => {
  const point = new ol.Feature({
    geometry: new ol.geom.Point(ol.proj.fromLonLat([116.286, 39.991])),
    name: "测试点",
  });
  render_vector_source.value.addFeature(point);

  const line = new ol.Feature({
    geometry: new ol.geom.LineString([
      ol.proj.fromLonLat([116.286, 39.991]),
      ol.proj.fromLonLat([116.306, 40.008]),
    ]),
    name: "测试线",
  });
  render_vector_source.value.addFeature(line);
  map_instance.value.getView().fit(point.getGeometry().getExtent(), {
    padding: [50, 50, 50, 50],
    duration: 500,
  });

  lineGeometry = line.getGeometry();
};

const onDrawLine = () => {
  const feature = new ol.Feature({
    geometry: new ol.geom.LineString([
      ol.proj.fromLonLat([116.286, 39.991]),
      ol.proj.fromLonLat([116.306, 40.008]),
      ol.proj.fromLonLat([116.312289, 40.004906]),
    ]),
    name: "测试线",
  });
  render_vector_source.value.addFeature(feature);
  map_instance.value.getView().fit(feature.getGeometry().getExtent(), {
    padding: [50, 50, 50, 50],
    duration: 500,
  });

  // NOTE-1. 记录所处的线
  lineGeometry = feature.getGeometry();
};

// 3. 核心判断逻辑:计算点到线的最短距离,与阈值比较
// threshold:距离阈值(米),小于该值则认为点在线上。默认为10
function getActualPointByTempPoint(pointGeom, lineGeom, threshold = 10) {
  // 将点坐标转换为经纬度(EPSG:4326)
  const pointCoords = ol.proj.toLonLat(pointGeom.getCoordinates());

  // 3.1 获取线上距离点最近的点坐标,并转换为经纬度(EPSG:4326)
  const closestPointCoord = lineGeom.getClosestPoint(pointGeom.getCoordinates());
  const closestPointLonLat = ol.proj.toLonLat(closestPointCoord);
  // 3.2 计算两个点之间的球面距离(单位:米)
  const distance = ol.sphere.getDistance(pointCoords, closestPointLonLat);
  // 3.3 判断距离是否小于阈值,说明这个点是比较近的,可以认为是在线上。
  console.log("distance=>", distance);
  console.log("closestPointCoord=>", closestPointCoord);
  if (distance < threshold) {
    return pointGeom;
  } else {
    // 距离超出阈值,返回线上最近点的新 Point 对象
    return new ol.geom.Point(closestPointCoord); // 注意:这里使用的是投影坐标
  }
}

const click_post_info = ref({
  longitude: 0,
  latitude: 0,
});
const is_record_point = ref(false);
const onStartRecordPoint = () => {
  is_record_point.value = !is_record_point.value;
};
const onLineAddPoint = () => {
  if (!is_record_point.value) return;
  // 1. 创建临时点
  // 距离线有一点距离的点
  const tempPointGeom = new ol.geom.Point(
    ol.proj.fromLonLat([click_post_info.value.longitude, click_post_info.value.latitude]),
  );
  // 2. 获取实际点(可能是原始点或线上最近点)
  const actualPointGeom = getActualPointByTempPoint(tempPointGeom, lineGeometry);
  // 3. 创建 Feature 并添加到地图
  const feature = new ol.Feature({
    geometry: actualPointGeom,
    name: "实际点",
  });
  render_vector_source.value.addFeature(feature);

  // 4. 开启移动
  setTranslateInteraction();
};
</script>

<style lang="less" scoped>
@import url("./app.less");
</style>