<template>
<div ref="mapElement" class="map-container"></div>
</template>
<script setup>
import { ref, defineExpose } from 'vue'
const {proxy} = getCurrentInstance();
import OlMap from 'ol/Map';
import OSM from 'ol/source/OSM';
import View from 'ol/View';
import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import Polygon from 'ol/geom/Polygon';
import TileLayer from 'ol/layer/Tile';
import ImageLayer from 'ol/layer/Image';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import ImageStatic from 'ol/source/ImageStatic';
import Icon from 'ol/style/Icon';
import Style from 'ol/style/Style';
import Stroke from 'ol/style/Stroke';
import Text from 'ol/style/Text';
import Transform from 'ol-ext/interaction/Transform';
import Draw from 'ol/interaction/Draw';
import { fromLonLat, toLonLat } from 'ol/proj';
import Tooltip from 'ol-ext/overlay/Tooltip';
import { click,shiftKeyOnly,always } from 'ol/events/condition';
import {v4 as uuidV4} from 'uuid'
const props = defineProps({
schemeId: {
type: String,
default: "",
},
backgroundUrl: {
type: String,
default: ''
},
isTileLayer: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['addFeature', 'updateFeature', 'featureSelected',"featureDeleted"])
const mapElement = ref(null)
const bounds = ref({})
let mapInstance = ref(null)
let vectorSource = null
let modifyInteraction = null;
let drawLine = ref(null)
let drawPoly = ref(null)
let selectedFeature = ref(null)
var startangle = 0;
var startRadius = 10;
var d=[0,0];
var firstPoint = false;
// 提取公共的样式创建方法
const createFeatureStyle = (featureInfo) => {
return new Style({
image: new Icon({
src: featureInfo.icon,
scale: 1,
rotation: 1.5708,
color: featureInfo.color
}),
text: featureInfo.showLabel ? new Text({
text: featureInfo.label,
fill: new Stroke({
// color: featureInfo.type
color: '#fff'
}),
offsetY: 30
}) : undefined
});
}
// 初始化地图
async function initMap ({center,zoom,minZoom,maxZoom,scope,featureList}){
console.log("init")
bounds.value = scope
vectorSource = new VectorSource({
features: featureList.map(featureInfo => {
const {id, label, type, position, icon, color, showLabel, visible} = featureInfo;
const coordinate = fromLonLat([position.longitude, position.latitude]);
const feature = new Feature({
geometry: new Point(coordinate),
name: label,
type: type,
params: featureInfo,
showLabel: showLabel || false,
visible: visible !== undefined ? visible : true
})
feature.setId(id)
// 使用公共方法创建样式
feature.setStyle(createFeatureStyle(featureInfo))
return feature
}),
style: null // 禁用图层默认样式
});
let backgroundLayer;
if (props.isTileLayer) {
backgroundLayer = new TileLayer({
source: new OSM({
url: props.backgroundUrl,
attributions: ''
}),
});
} else {
const loadImage = async () => {
const img = new Image();
img.src = props.backgroundUrl;
await new Promise((resolve) => {
img.onload = resolve;
});
const imageWidth = img.width;
const imageHeight = img.height;
const aspectRatio = imageWidth / imageHeight;
// 假设图片高度在地图上覆盖 0.1 个经纬度单位
const targetHeight = 180; // 调整这个值可以改变图片大小
const targetWidth = targetHeight * aspectRatio;
// 定义图片范围,使其中心在 [0, 0]
const imageExtent = [
-targetWidth / 2, // minX (左边界)
-targetHeight /2/aspectRatio, // minY (下边界)
targetWidth / 2, // maxX (右边界)
targetHeight /2/aspectRatio, // maxY (上边界)
];
return imageExtent;
};
// 使用方式
try {
const imageExtent = await loadImage();
backgroundLayer = new ImageLayer({
source: new ImageStatic({
url: props.backgroundUrl,
imageExtent: imageExtent, // 使用计算后的 extent
projection: 'EPSG:4326',
}),
});
console.log('backgroundLayer\n',backgroundLayer)
} catch (error) {
console.error("加载图片失败:", error);
}
}
const vectorLayer = new VectorLayer({
source: vectorSource,
});
if (mapInstance.value){
mapInstance.value.setTarget(null);
mapInstance.value = null;
}
mapInstance.value = new OlMap({
target: mapElement.value,
layers: [
backgroundLayer,
vectorLayer
],
view: new View({
center: fromLonLat(center),
zoom: zoom,
minZoom: minZoom,
maxZoom: maxZoom,
multiWorld: true // 允许无限缩放
})
});
mapInstance.value.on('click', (event) => {
if (selectedMarkForAdding.value) {
addMarkToMap(event.coordinate, selectedMarkForAdding.value)
selectedMarkForAdding.value = null
}
})
drawLine.value = new Draw({ type: 'LineString' });
mapInstance.value.addInteraction(drawLine.value);
drawPoly.value = new Draw({ type: 'Polygon' });
mapInstance.value.addInteraction(drawPoly.value);
drawPoly.value.setActive(false);
drawLine.value.setActive(false);
drawLine.value.on('drawstart', function(evt) {
const tooltip = new Tooltip({
formatLength: function(line) {
const length = line;
if (length > 1000) {
return (length / 1000).toFixed(2) + ' km';
} else {
return length.toFixed(2) + ' m';
}
}
});
mapInstance.value.addOverlay(tooltip);
tooltip.setFeature(evt.feature);
});
drawLine.value.on(['change:active','drawend'], function() {
mapInstance.value.getOverlays().forEach(overlay => {
if (overlay instanceof Tooltip) {
mapInstance.value.removeOverlay(overlay);
}
});
});
drawPoly.value.on('drawstart', function(evt) {
const tooltip = new Tooltip({
formatArea: function(polygon) {
const area = polygon.getArea();
if (area > 1000000) {
return (area / 1000000).toFixed(2) + ' km²';
} else {
return area.toFixed(2) + ' m²';
}
}
});
mapInstance.value.addOverlay(tooltip);
tooltip.setFeature(evt.feature);
});
drawPoly.value.on(['change:active','drawend'], function() {
mapInstance.value.getOverlays().forEach(overlay => {
if (overlay instanceof Tooltip) {
mapInstance.value.removeOverlay(overlay);
}
});
});
window.addEventListener('resize', updateMapSize);
// 在地图初始化完成后绘制边界框
mapInstance.value.once('postrender', () => {
setupModifyInteraction();
drawBounds();
});
}
// 存储每个 feature 的自定义属性
const featureStates = new Map(); // feature => { rotation, translation }
const setupModifyInteraction = () => {
var transform = new Transform({
enableRotatedTransform: false, // 是否强制交互手柄匹配地图旋转角度
addCondition: shiftKeyOnly, // 按住Shift键可多选要素
hitTolerance: 2, // 点击容差(像素),避免精确点击
translateFeature: true, // 点击要素是否可直接平移
scale: true, // 是否启用缩放
rotate: true, // 是否启用旋转
keepAspectRatio: always, // 是否强制保持宽高比
translate: true, // 是否启用平移
stretch: true, // 是否启用拉伸(非均匀缩放)
pointRadius: function(f) { // 点要素的缩放半径(动态获取要素的radius属性)
var radius = f.get('radius') || 10;
return [radius, radius];
}
});
mapInstance.value.addInteraction(transform); // 将交互添加到地图
transform.on (['select'], function(e) {
if (e.features.getLength()) {
selectedFeature.value = e.features.getArray()[0];
emit('featureSelected', selectedFeature.value.getProperties().params)
}
if (firstPoint && e.features && e.features.getLength()) {
transform.setCenter(e.features.getArray()[0].getGeometry().getFirstCoordinate());
}
});
// 旋转开始:记录初始旋转值
transform.on('rotatestart', function(e) {
const feature = e.feature;
const state = ensureFeatureState(feature);
state.startRotation = state.rotation; // 记录开始前的角度
console.log('Rotate start, base angle:', state.rotation);
});
// 旋转中:更新 feature 的旋转状态
transform.on('rotating', function(e) {
const feature = e.feature;
const state = ensureFeatureState(feature);
// e.angle 是相对于 rotatestart 的增量(正值表示逆时针)
const newRotation = state.startRotation + e.angle;
state.rotation = newRotation;
// 触发样式重绘
feature.changed();
});
// 平移过程:记录累计位移(可选用于显示)
let d = [0, 0];
transform.on('translating', function(e) {
d[0] += e.delta[0];
d[1] += e.delta[1];
console.log(`累计平移: ${d[0].toFixed(2)}, ${d[1].toFixed(2)} px`);
});
// 变换结束:清空临时数据或发送到服务器
transform.on(['rotateend', 'translateend', 'scaleend'], function(e) {
console.log('最终角度 (弧度):', featureStates.get(e.feature)?.rotation);
console.log('最终角度 (度):', ((featureStates.get(e.feature)?.rotation || 0) * 180 / Math.PI).toFixed(2));
// 可在此保存状态到数据库或导出 GeoJSON
});
}
// 初始化要素状态
const ensureFeatureState = (feature)=> {
if (!featureStates.has(feature)) {
// 默认旋转为0,平移为[0,0]
featureStates.set(feature, {
rotation: 0, // 弧度
translate: [0, 0] // 像素偏移(可选)
});
}
return featureStates.get(feature);
}
// 更新地图尺寸
const updateMapSize = () => {
mapInstance.value && mapInstance.value.updateSize();
}
const addMarkToMap = (coordinate, mark) => {
// 检查坐标是否在边界范围内
if (!isCoordinateInBounds(coordinate)) {
proxy.$message({
message: '只能在指定范围内添加军标',
type: 'warning',
duration: 5 * 1000
})
return;
}
const params = {...mark,id: uuidV4(), showLabel: true,visible: true}
const feature = new Feature({
geometry: new Point(coordinate),
name: mark.label,
type: mark.type,
params: params,
showLabel: params.showLabel,
visible: params.visible
})
feature.setId(params.id)
feature.setStyle(
new Style({
image: new Icon({
src: mark.icon,
scale: 1
}),
text: new Text({
text: params.label,
fill: new Stroke({
// color: params.type
color: '#fff'
}),
offsetY: 30
})
})
)
vectorSource.addFeature(feature)
const lonLat = toLonLat(coordinate);
params.position = {
longitude: lonLat[0],
latitude: lonLat[1]
}
const featureInfo = {
...params,
position: {
longitude: lonLat[0],
latitude: lonLat[1]
},
icon: mark.icon
};
emit('addFeature', featureInfo);
}
const updateFeature = (updatedFeature) => {
const feature = vectorSource.getFeatureById(updatedFeature.id);
if (feature) {
feature.setProperties(updatedFeature);
// 使用公共方法创建样式
feature.setStyle(createFeatureStyle(updatedFeature))
}
}
const updateFeatureVisibility = (updatedFeature) => {
const feature = vectorSource.getFeatureById(updatedFeature.id);
if (feature) {
feature.setProperties(updatedFeature);
feature.set('visible', updatedFeature.visible);
if(updatedFeature.visible) {
// 使用公共方法创建样式
feature.setStyle(createFeatureStyle(updatedFeature))
}else{
feature.setStyle([])
}
emit('updateFeature', updatedFeature)
}
}
// 删除当前选中的要素
const deleteSelectedFeature = () => {
if (selectedFeature.value) {
const featureId = selectedFeature.value.getId();
vectorSource.removeFeature(selectedFeature.value);
emit('featureDeleted', featureId); // 通知父组件
selectedFeature.value = null; // 清空选中
}
}
// 移动要素到新位置
const moveFeature = (featureId, newPosition) => {
const feature = vectorSource.getFeatureById(featureId);
if (feature) {
const newCoord = fromLonLat([newPosition.longitude, newPosition.latitude]);
feature.getGeometry().setCoordinates(newCoord);
// 更新要素属性
const featureProps = feature.getProperties();
const updatedFeature = {
...featureProps.params,
position: newPosition
};
feature.set('params', updatedFeature);
emit('updateFeature', updatedFeature);
}
}
// 批量删除要素
const deleteFeaturesByIds = (featureIds) => {
featureIds.forEach(id => {
const feature = vectorSource.getFeatureById(id);
if (feature) {
vectorSource.removeFeature(feature);
}
});
emit('featuresDeleted', featureIds);
}
function drawBounds(){
if (! bounds.value) return;
const { north_east, south_west } = bounds.value;
if (!north_east || !south_west) return;
// 检查是否已经绘制过bounds框,避免重复绘制
const existingBoundsFeature = vectorSource.getFeatures().find(feature =>
feature.get('type') === 'bounds'
);
if (existingBoundsFeature) return;
// 转换坐标
const neCoord = fromLonLat([north_east[0], north_east[1]]);
const swCoord = fromLonLat([south_west[0], south_west[1]]);
// 创建边界框坐标数组
const coordinates = [
[swCoord[0], swCoord[1]], // 西南
[neCoord[0], swCoord[1]], // 西北
[neCoord[0], neCoord[1]], // 东北
[swCoord[0], neCoord[1]], // 东南
[swCoord[0], swCoord[1]] // 回到西南,闭合多边形
];
// 创建边界框要素
const boundsFeature = new Feature({
geometry: new Polygon([coordinates]),
type: 'bounds',
name: '地图边界'
});
// 设置样式
boundsFeature.setStyle(new Style({
stroke: new Stroke({
color: '#02eaea',
width: 2,
lineDash: [5, 5] // 虚线样式
})
}));
// 添加到地图
vectorSource.addFeature(boundsFeature);
}
// 添加检查坐标是否在边界范围内的方法
const isCoordinateInBounds = (coordinate) => {
console.log(bounds.value)
if (!bounds.value) return true;
const { north_east, south_west } = bounds.value;
if (!north_east || !south_west) return true;
// 转换坐标
const lonLat = toLonLat(coordinate);
const [longitude, latitude] = lonLat;
// 检查是否在边界范围内,不假设坐标大小关系
const minLon = Math.min(south_west[0], north_east[0]);
const maxLon = Math.max(south_west[0], north_east[0]);
const minLat = Math.min(south_west[1], north_east[1]);
const maxLat = Math.max(south_west[1], north_east[1]);
return (
longitude >= minLon &&
longitude <= maxLon &&
latitude >= minLat &&
latitude <= maxLat
);
}
const selectedMarkForAdding = ref(null)
defineExpose({
initMap,
updateMapSize,
addMarkToMap,
updateFeature,
updateFeatureVisibility,
mapInstance,
selectedMarkForAdding,
selectedFeature,
drawLine,
deleteSelectedFeature,
moveFeature,
deleteFeaturesByIds
})
</script>
<style scoped lang="scss">
.map-container {
width: 100%;
height: 100%;
}
</style>
优化选中旋转、放大缩小,移动图标
最新发布