
























参考文章: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>
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。