976 lines
31 KiB
Vue
976 lines
31 KiB
Vue
<template>
|
||
<div v-loading="geoTiffLoading" class="flex h100vh">
|
||
<div class="header flex justify-between">
|
||
<div class="tips flex items-center justify-between">
|
||
<div class="dot1">未提交</div>
|
||
<div class="dot2">已提交</div>
|
||
<div class="dot3">已选择,待提交</div>
|
||
<el-tooltip class="box-item" effect="dark" content="右键拖动生成套索区域选中/取消图形" placement="bottom">
|
||
<i class="iconfont icon-wenhao"></i>
|
||
</el-tooltip>
|
||
</div>
|
||
<el-form :model="queryParams" ref="form" label-width="80px" inline class="flex items-center">
|
||
<el-form-item label="请选择项目:" prop="pid" label-width="100" style="margin-bottom: 0; color: #fff">
|
||
<el-select v-model="selectedProjectId" placeholder="请选择" @change="handleSelect" clearable>
|
||
<el-option v-for="item in ProjectList" :key="item.id" :label="item.name" :value="item.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="请选择方阵:" prop="pid" label-width="100" style="margin-bottom: 0">
|
||
<!-- <el-select v-model="matrixValue" placeholder="请选择" @change="handleChange" clearable>
|
||
<el-option v-for="item in matrixOptions" :key="item.id" :label="item.matrixName" :value="item.id" />
|
||
</el-select> -->
|
||
<el-cascader
|
||
:options="matrixOptions"
|
||
placeholder="请选择"
|
||
@change="handleChange"
|
||
:props="{ value: 'matrixId', label: 'name' }"
|
||
v-model="queryParams.matrixId"
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<div class="ol-map" id="olMap"></div>
|
||
<div class="aside">
|
||
<el-tree
|
||
style="max-width: 600px"
|
||
:data="progressCategoryList"
|
||
ref="treeRef"
|
||
@check-change="handleCheckChange"
|
||
:props="treeProps"
|
||
:load="loadNode"
|
||
node-key="id"
|
||
lazy
|
||
:render-content="renderContent"
|
||
check-strictly
|
||
:expand-on-click-node="false"
|
||
/>
|
||
</div>
|
||
<div class="submit">
|
||
<el-button type="primary" size="default" @click="submit" :loading="loading">提交</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import Map from 'ol/Map'; // OpenLayers的主要类,用于创建和管理地图
|
||
import View from 'ol/View'; // OpenLayers的视图类,定义地图的视图属性
|
||
import { Tile as TileLayer, WebGLTile } from 'ol/layer'; // OpenLayers的瓦片图层类
|
||
import { Raster, XYZ } from 'ol/source'; // OpenLayers的瓦片数据源,包括XYZ格式和OpenStreetMap专用的数据源
|
||
import { defaults as defaultControls, defaults, FullScreen, MousePosition, ScaleLine } from 'ol/control';
|
||
import { fromLonLat, toLonLat, transform, transformExtent } from 'ol/proj';
|
||
import { useUserStoreHook } from '@/store/modules/user';
|
||
import { getProjectSquare, listProgressCategory, addDaily, workScheduleListPosition } from '@/api/progress/plan';
|
||
import { ProgressCategoryVO, progressPlanDetailForm } from '@/api/progress/plan/types';
|
||
import { Circle, Fill, Stroke, Style, Text } from 'ol/style';
|
||
import { defaults as defaultInteractions, DragPan } from 'ol/interaction';
|
||
import Feature from 'ol/Feature';
|
||
import { Point, Polygon } from 'ol/geom';
|
||
import VectorSource from 'ol/source/Vector';
|
||
import VectorLayer from 'ol/layer/Vector';
|
||
import Node from 'element-plus/es/components/tree/src/model/node.mjs';
|
||
import { ElCheckbox } from 'element-plus';
|
||
import { LassoSelector } from '@/utils/lassoSelect';
|
||
import { FeatureCollection, Geometry } from 'geojson';
|
||
import { MapViewFitter } from '@/utils/setMapCenter';
|
||
import PointerInteraction from 'ol/interaction/Pointer';
|
||
import { Coordinate } from 'ol/coordinate';
|
||
import GeoTIFF, { fromBlob, fromUrl, fromArrayBuffer, Pool } from 'geotiff';
|
||
import GeoTIFFSource from 'ol/source/GeoTIFF';
|
||
import ImageLayer from 'ol/layer/Image';
|
||
import Static from 'ol/source/ImageStatic';
|
||
import proj4 from 'proj4';
|
||
import { register } from 'ol/proj/proj4';
|
||
import gcoord from 'gcoord';
|
||
import { createXYZ } from 'ol/tilegrid';
|
||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||
const orthophoto = '@/assets/images/orthophoto.tif';
|
||
const selector = ref<LassoSelector | null>(null);
|
||
// 获取用户 store
|
||
const userStore = useUserStoreHook();
|
||
// 从 store 中获取项目列表和当前选中的项目
|
||
const currentProject = computed(() => userStore.selectedProject);
|
||
const ProjectList = computed(() => userStore.projects);
|
||
const selectedProjectId = ref(userStore.selectedProject?.id || '');
|
||
const treeRef = ref();
|
||
const queryParams = ref({
|
||
pid: undefined,
|
||
name: undefined,
|
||
unitType: undefined,
|
||
projectId: currentProject.value.id,
|
||
matrixId: undefined,
|
||
params: {}
|
||
});
|
||
const submitForm = ref<progressPlanDetailForm>({
|
||
id: '',
|
||
finishedDetailIdList: [] as string[]
|
||
});
|
||
const loading = ref(false);
|
||
const geoTiffLoading = ref(false);
|
||
const matrixOptions = ref([]);
|
||
const matrixValue = ref<number | undefined>(matrixOptions.value.length > 0 ? matrixOptions.value[0].id : undefined);
|
||
const progressCategoryList = ref<ProgressCategoryVO[]>([]);
|
||
const treeProps = {
|
||
children: 'children',
|
||
label: 'name',
|
||
isLeaf: 'leaf',
|
||
hasChildren: 'hasChildren', // 重要
|
||
disabled: 'disabled'
|
||
};
|
||
|
||
//切换项目
|
||
const handleSelect = (projectId: string) => {
|
||
const selectedProject = ProjectList.value.find((p) => p.id === projectId);
|
||
if (selectedProject) {
|
||
userStore.setSelectedProject(selectedProject);
|
||
|
||
resetMatrix();
|
||
getList();
|
||
}
|
||
};
|
||
|
||
/** 进度类别树选中事件 */
|
||
const handleCheckChange = (data: any, checked: boolean, indeterminate: boolean) => {
|
||
const node: Node | undefined = treeRef.value?.getNode(data.id);
|
||
|
||
if (node.level === 2) {
|
||
//二级节点 展开显示图层 收起删除
|
||
const children = node.childNodes.map((child) => child.data);
|
||
children.forEach((child) => {
|
||
child.disabled = !checked;
|
||
});
|
||
// 让 el-tree 刷新显示
|
||
treeRef.value.updateKeyChildren(data.id, children);
|
||
if (checked) {
|
||
addPointToMap(data.threeChildren); // 添加点到地图
|
||
clearLevel3Checked(node.level); // 清除三级节点的选中状态
|
||
treeRef.value.setChecked(data.id, true, false);
|
||
} else {
|
||
data.threeChildren.forEach((child: any) => {
|
||
const feature = featureMap[child.id];
|
||
if (feature && sharedSource.hasFeature(feature)) {
|
||
sharedSource.removeFeature(feature);
|
||
}
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
// if (!checked) return (submitForm.value.id = ''); // 只处理第三级节点的选中事件
|
||
if (!checked) {
|
||
// ✅ 只在取消的是当前 id 时清空
|
||
if (submitForm.value.id === data.id) {
|
||
submitForm.value.id = '';
|
||
}
|
||
return;
|
||
}
|
||
|
||
const parent = node.parent;
|
||
if (!parent) return;
|
||
//消除所有节点的选中状态
|
||
clearLevel3Checked(node.level); // 清除三级节点的选中状态
|
||
// 设置当前点击项为选中
|
||
treeRef.value.setChecked(data.id, true, false);
|
||
submitForm.value.id = data.id; // 设置提交表单的id
|
||
console.log('submitForm', submitForm.value);
|
||
};
|
||
|
||
//清除某一级节点所有选中状态
|
||
const clearLevel3Checked = (level) => {
|
||
const rootNodes = treeRef.value.store.root.childNodes;
|
||
|
||
const clearChecked = (nodes: any[]) => {
|
||
nodes.forEach((node) => {
|
||
if (node.level === level) {
|
||
treeRef.value.setChecked(node.key, false, false);
|
||
} else if (node.childNodes?.length) {
|
||
clearChecked(node.childNodes);
|
||
}
|
||
});
|
||
};
|
||
|
||
clearChecked(rootNodes);
|
||
};
|
||
|
||
/** 关闭节点事件 */
|
||
// const closeNode = (node: any) => {
|
||
// // 清除子节点
|
||
// if (node.pid) {
|
||
// // node.threeChildren.forEach((child: any) => {
|
||
// // const feature = featureMap[child.id];
|
||
// // if (feature && sharedSource.hasFeature(feature)) {
|
||
// // sharedSource.removeFeature(feature);
|
||
// // }
|
||
// // });
|
||
// }
|
||
// };
|
||
|
||
// /** 打开节点事件 */
|
||
// const openNode = (node: any) => {
|
||
// // 清除子节点
|
||
// if (!node.pid) return;
|
||
// // addPointToMap(node.threeChildren); // 添加点到地图
|
||
// };
|
||
|
||
//懒加载子节点
|
||
const loadNode = async (node: any, resolve: (data: any[]) => void) => {
|
||
if (node.level !== 2) {
|
||
// 只对二级节点加载子节点
|
||
resolve(node.data.children || []);
|
||
return;
|
||
}
|
||
|
||
const secondLevelNodeId = node.data.id;
|
||
const res = await workScheduleListPosition(secondLevelNodeId); // 替换成你的 API
|
||
|
||
const children = res.data.detailList || [];
|
||
|
||
if (children.length === 0) {
|
||
proxy?.$modal.msgWarning(`节点 "${node.data.name}" 为空`);
|
||
resolve([]);
|
||
}
|
||
|
||
// 标记子节点为叶子节点
|
||
const threeLeafList = children.map((detail) => {
|
||
return {
|
||
...detail,
|
||
name: detail.date, // 设置为叶子节点
|
||
leaf: true, // 标记为叶子节点
|
||
disabled: true
|
||
};
|
||
});
|
||
progressCategoryList.value.forEach((item, i) => {
|
||
let indexNum = item.children.findIndex((item) => item.id === secondLevelNodeId);
|
||
if (indexNum !== -1) {
|
||
item.children[indexNum].threeChildren = res.data.facilityList; // 将子节点添加到当前节点的threeChildren属性中
|
||
// item.children[indexNum].detailChildren = children; // 将子节点添加到当前节点的threeChildren属性中
|
||
}
|
||
});
|
||
|
||
resolve(threeLeafList);
|
||
};
|
||
|
||
/** 提交按钮点击事件 */
|
||
const submit = () => {
|
||
console.log('sunbmitForm', submitForm.value);
|
||
const { finishedDetailIdList, id } = submitForm.value;
|
||
if (!id || finishedDetailIdList.length === 0) return proxy?.$modal.msgWarning('请选择图层以及日期');
|
||
|
||
loading.value = true;
|
||
addDaily(submitForm.value)
|
||
.then(() => {
|
||
proxy?.$modal.msgSuccess('提交成功');
|
||
const scale = Math.max(map.getView().getZoom() / 10, 1); // 获取当前缩放比例
|
||
sharedSource.getFeatures().forEach((feature) => {
|
||
if (feature.get('highlighted')) {
|
||
feature.setStyle(successStyle(feature.get('name'), scale)); // 转为成功样式
|
||
feature.set('highlighted', false); // 重置高亮状态
|
||
feature.set('status', '2'); // 设置为完成状态
|
||
}
|
||
});
|
||
resetTreeAndMap();
|
||
})
|
||
.catch((error) => {
|
||
proxy?.$modal.msgError(`提交失败: ${error.message}`);
|
||
});
|
||
};
|
||
|
||
//重置树形结构选中以及图层高亮
|
||
const resetTreeAndMap = () => {
|
||
// 重置树形结构选中状态
|
||
clearLevel3Checked(3);
|
||
//取消加载状态
|
||
loading.value = false;
|
||
// 清除地图上的所有高亮
|
||
const scale = Math.max(map.getView().getZoom() / 10, 1); // 获取当前缩放比例
|
||
sharedSource.getFeatures().forEach((feature) => {
|
||
if (feature.get('highlighted')) {
|
||
feature.setStyle(defaultStyle(feature.get('name'), scale)); // 恢复默认样式
|
||
feature.set('highlighted', false); // 重置高亮状态
|
||
}
|
||
});
|
||
// 清空已完成列表
|
||
submitForm.value.finishedDetailIdList = [];
|
||
};
|
||
|
||
/** 方阵选择器改变事件 */
|
||
const handleChange = (value: number) => {
|
||
queryParams.value.matrixId = value[1];
|
||
|
||
getList();
|
||
};
|
||
|
||
//限定部分节点能选择
|
||
const renderContent = (context, { node, data }) => {
|
||
if (node.level === 3) {
|
||
return h('span', { class: 'custom-tree-node' }, [
|
||
h(ElCheckbox, {
|
||
modelValue: node.checked,
|
||
'onUpdate:modelValue': (val) => node.setChecked(val),
|
||
style: 'margin-right: 8px;',
|
||
disabled: data.disabled
|
||
}),
|
||
h('span', [node.label])
|
||
]);
|
||
}
|
||
if (node.level === 2) {
|
||
return h('span', { class: 'custom-tree-node' }, [
|
||
h(ElCheckbox, {
|
||
modelValue: node.checked,
|
||
'onUpdate:modelValue': (val) => node.setChecked(val),
|
||
style: 'margin-right: 8px;',
|
||
disabled: !data.threeChildren || data.threeChildren.length == 0
|
||
}),
|
||
h('span', { onDblclick: () => handlePosition(data, node) }, node.label)
|
||
]);
|
||
} else {
|
||
return h('span', node.label);
|
||
}
|
||
};
|
||
|
||
const handlePosition = (data: any, node: any) => {
|
||
if (!data.threeChildren) return;
|
||
|
||
const fitter = new MapViewFitter(map); // 传入你的 OpenLayers 地图实例
|
||
const features: GeoJSON.Feature[] = data.threeChildren.map((item) => {
|
||
if ('type' in item && item.type === 'Feature') {
|
||
return item as GeoJSON.Feature;
|
||
}
|
||
const raw = item;
|
||
|
||
let coordinates: any;
|
||
if (raw.type === 'Polygon') {
|
||
coordinates = [(raw.positions as [string, string][]).map(([lng, lat]) => [parseFloat(lng), parseFloat(lat)])];
|
||
} else if (raw.type === 'Point') {
|
||
const [lng, lat] = raw.positions as [string, string];
|
||
coordinates = [parseFloat(lng), parseFloat(lat)];
|
||
} else {
|
||
throw new Error(`Unsupported geometry type: ${raw.type}`);
|
||
}
|
||
|
||
return {
|
||
type: 'Feature',
|
||
geometry: {
|
||
type: raw.type,
|
||
coordinates
|
||
},
|
||
properties: {
|
||
id: raw.id,
|
||
name: raw.name,
|
||
status: raw.status,
|
||
finishDate: raw.finishDate
|
||
}
|
||
};
|
||
});
|
||
|
||
if (features?.length) {
|
||
const featureCollection: FeatureCollection<Geometry> = {
|
||
type: 'FeatureCollection',
|
||
features
|
||
};
|
||
|
||
fitter.fit(featureCollection);
|
||
}
|
||
};
|
||
|
||
//切换项目重置方阵
|
||
const resetMatrix = () => {
|
||
matrixValue.value = undefined;
|
||
queryParams.value.matrixId = undefined;
|
||
matrixOptions.value = [];
|
||
};
|
||
|
||
/** 查询进度类别列表 */
|
||
const getList = async () => {
|
||
if (!queryParams.value.matrixId) {
|
||
const res = await getProjectSquare(currentProject.value.id);
|
||
if (res.data.length === 0) {
|
||
proxy?.$modal.msgWarning('当前项目下没有方阵,请先创建方阵');
|
||
} else {
|
||
let matrixList = res.data.map((item) => {
|
||
return {
|
||
...item,
|
||
matrixId: item.projectId
|
||
};
|
||
});
|
||
if (!matrixValue.value) matrixValue.value = matrixList[0].id;
|
||
matrixOptions.value = matrixList;
|
||
queryParams.value.matrixId = matrixList[0].children[0].matrixId;
|
||
}
|
||
}
|
||
loading.value = true;
|
||
queryParams.value.projectId = currentProject.value.id;
|
||
const res = await listProgressCategory(queryParams.value);
|
||
const data = proxy?.handleTree<ProgressCategoryVO>(res.data, 'id', 'pid');
|
||
if (data) {
|
||
progressCategoryList.value = data;
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const imageExtent = ref(null);
|
||
const imageLayer = ref(null);
|
||
import { get as getProjection } from 'ol/proj';
|
||
|
||
const initGeoTiff = async () => {
|
||
// const tiff = await fromUrl('/image/clean_rgba_cleaned.tif');
|
||
// const image = await tiff.getImage();
|
||
// const width = image.getWidth();
|
||
// const height = image.getHeight();
|
||
// const bbox = image.getBoundingBox(); // [minX, minY, maxX, maxY]
|
||
// console.log('bbox', bbox);
|
||
// const rasters = await image.readRasters({ interleave: true });
|
||
// // 创建 Canvas
|
||
// const canvas = document.createElement('canvas');
|
||
// canvas.width = width;
|
||
// canvas.height = height;
|
||
// const ctx = canvas.getContext('2d')!;
|
||
// const imageData: any = ctx.createImageData(width, height);
|
||
// // 设置 RGBA 数据
|
||
// imageData.data.set(rasters); // ✅ 完整设置,不用手动循环
|
||
// ctx.putImageData(imageData, 0, 0);
|
||
// // 将 canvas 转成 Data URL 用作图层 source
|
||
// const imageUrl = canvas.toDataURL();
|
||
// // 转换为 WGS84 经纬度
|
||
// const minLonLat = transform([bbox[0], bbox[1]], 'EPSG:32648', 'EPSG:4326');
|
||
// const maxLonLat = transform([bbox[2], bbox[3]], 'EPSG:32648', 'EPSG:4326');
|
||
// // 转为 GCJ02(高德地图坐标系)
|
||
// const gcjMin = gcoord.transform(minLonLat as [number, number, number], gcoord.WGS84, gcoord.GCJ02);
|
||
// const gcjMax = gcoord.transform(maxLonLat as [number, number, number], gcoord.WGS84, gcoord.GCJ02);
|
||
// // 再转 EPSG:3857 供 OpenLayers 使用
|
||
// const minXY = fromLonLat(gcjMin);
|
||
// const maxXY = fromLonLat(gcjMax);
|
||
|
||
// imageExtent.value = [...minXY, ...maxXY];
|
||
|
||
// imageLayer.value = new ImageLayer({
|
||
// source: new Static({
|
||
// url: imageUrl,
|
||
// imageExtent: imageExtent.value,
|
||
// projection: 'EPSG:3857'
|
||
// })
|
||
// });
|
||
// console.log('imageExtent', imageExtent.value);
|
||
|
||
// 1. 你的原始瓦片的边界(来自 .tfw 或你知道的数据)
|
||
|
||
// 1. 你的 bbox 是 WGS84 经纬度
|
||
const bbox = [107.13149481208748, 23.80411597354268, 107.13487254421389, 23.80801427852998];
|
||
|
||
// 2. 转成 GCJ02(高德坐标系)
|
||
const gcjMin = gcoord.transform([bbox[0], bbox[1]], gcoord.WGS84, gcoord.GCJ02);
|
||
const gcjMax = gcoord.transform([bbox[2], bbox[3]], gcoord.WGS84, gcoord.GCJ02);
|
||
|
||
// 3. 再转换成 EPSG:3857,用于 OpenLayers
|
||
const minXY = fromLonLat(gcjMin);
|
||
const maxXY = fromLonLat(gcjMax);
|
||
|
||
// 4. 组成瓦片范围 extent
|
||
const tileExtent = [...minXY, ...maxXY];
|
||
console.log('tileExtent', tileExtent);
|
||
|
||
// 5. 创建 tileGrid
|
||
const tileGrid = createXYZ({
|
||
extent: tileExtent,
|
||
tileSize: 256,
|
||
minZoom: 10,
|
||
maxZoom: 18
|
||
});
|
||
|
||
// 6. 使用 Web Mercator 投影 EPSG:3857
|
||
const projection = getProjection('EPSG:3857');
|
||
|
||
// 7. 创建瓦片图层
|
||
imageLayer.value = new TileLayer({
|
||
source: new XYZ({
|
||
projection,
|
||
tileGrid,
|
||
tileUrlFunction: (tileCoord) => {
|
||
if (!tileCoord) return '';
|
||
let [z, x, y] = tileCoord;
|
||
console.log(z, x, y);
|
||
y = Math.pow(2, z) - y - 1;
|
||
return `http://192.168.110.2:8000/api/projects/3/tasks/c2e3227f-343f-48b1-88c0-1432d6eab33f/orthophoto/tiles/${z}/${x}/${y}`;
|
||
}
|
||
})
|
||
});
|
||
const source = imageLayer.value.getSource();
|
||
const projections = source.getProjection();
|
||
|
||
console.log('图层使用的坐标系:', projections?.getCode());
|
||
};
|
||
|
||
let map: any = null;
|
||
const layerData = reactive<any>({});
|
||
const centerPosition = ref(fromLonLat([107.12932403398425, 23.805564054229908]));
|
||
const initOLMap = () => {
|
||
console.log(111);
|
||
// const scoure = new TileLayer({
|
||
// // 设置图层的数据源为XYZ类型。XYZ是一个通用的瓦片图层源,它允许你通过URL模板来获取瓦片
|
||
// source: new XYZ({
|
||
// url: 'http://192.168.110.2:8000/api/projects/3/tasks/c2e3227f-343f-48b1-88c0-1432d6eab33f/orthophoto/tiles/{z}/{x}/{y}'
|
||
// })
|
||
// });
|
||
// console.log(scoure);
|
||
|
||
map = new Map({
|
||
// 设置地图容器的ID
|
||
target: 'olMap',
|
||
// 定义地图的图层列表,用于显示特定的地理信息。
|
||
layers: [
|
||
// 高德地图
|
||
// TileLayer表示一个瓦片图层,它由一系列瓦片(通常是图片)组成,用于在地图上显示地理数据。
|
||
new TileLayer({
|
||
// 设置图层的数据源为XYZ类型。XYZ是一个通用的瓦片图层源,它允许你通过URL模板来获取瓦片
|
||
source: new XYZ({
|
||
url: 'https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}'
|
||
})
|
||
}),
|
||
new TileLayer({
|
||
// 设置图层的数据源为XYZ类型。XYZ是一个通用的瓦片图层源,它允许你通过URL模板来获取瓦片
|
||
source: new XYZ({
|
||
url: 'http://webst02.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8'
|
||
})
|
||
})
|
||
// imageLayer.value
|
||
// imageLayer.value
|
||
],
|
||
// 设置地图的视图参数
|
||
// View表示地图的视图,它定义了地图的中心点、缩放级别、旋转角度等参数。
|
||
view: new View({
|
||
// fromLonLat是一个函数,用于将经纬度坐标转换为地图的坐标系统。
|
||
center: centerPosition.value, //地图中心点
|
||
zoom: 15, // 缩放级别
|
||
minZoom: 0, // 最小缩放级别
|
||
// maxZoom: 18, // 最大缩放级别
|
||
constrainResolution: true // 因为存在非整数的缩放级别,所以设置该参数为true来让每次缩放结束后自动缩放到距离最近的一个整数级别,这个必须要设置,当缩放在非整数级别时地图会糊
|
||
// projection: 'EPSG:4326' // 投影坐标系,默认是3857
|
||
}),
|
||
|
||
//加载控件到地图容器中
|
||
controls: defaultControls({
|
||
zoom: false,
|
||
rotate: false,
|
||
attribution: false
|
||
}).extend([new ScaleLine()]),
|
||
interactions: defaultInteractions({
|
||
doubleClickZoom: false // 禁用双击缩放
|
||
})
|
||
});
|
||
map.on('click', (e: any) => {
|
||
var coordinate = e.coordinate;
|
||
|
||
// 将投影坐标转换为经纬度坐标
|
||
var lonLatCoordinate = toLonLat(coordinate);
|
||
// 输出转换后的经纬度坐标
|
||
console.log('经纬度坐标:', lonLatCoordinate);
|
||
const zoom = map.getView().getZoom();
|
||
const scale = Math.max(zoom / 10, 1); // 缩放比例,根据需要调整公式
|
||
map.forEachFeatureAtPixel(e.pixel, (feature: Feature) => {
|
||
toggleFeatureHighlight(feature);
|
||
});
|
||
});
|
||
map.getView().on('change:resolution', () => {
|
||
const zoom = map.getView().getZoom();
|
||
const scale = Math.max(zoom / 10, 1); // 缩放比例,根据需要调整公式
|
||
|
||
sharedSource.getFeatures().forEach((feature) => {
|
||
const style = feature.getStyle();
|
||
|
||
if (style instanceof Style && style.getText()) {
|
||
style.getText().setScale(scale);
|
||
feature.setStyle(style); // 重新应用样式
|
||
}
|
||
});
|
||
});
|
||
map.on('moveend', (e: any) => {
|
||
// console.log('地图移动', e);
|
||
// 获取当前缩放级别
|
||
var zoomLevel = map.getView().getZoom();
|
||
console.log('当前缩放级别:', zoomLevel);
|
||
});
|
||
|
||
// 4. 添加 pointermove 鼠标悬停事件
|
||
let lastFeature: Feature | null = null;
|
||
|
||
map.on('pointermove', (evt) => {
|
||
map.getTargetElement().style.cursor = '';
|
||
|
||
const feature = map.forEachFeatureAtPixel(evt.pixel, (f) => f);
|
||
const zoom = map.getView().getZoom();
|
||
const scale = Math.max(zoom / 10, 1);
|
||
|
||
// 👉 若当前划入的 feature 是不允许 hover 的,直接跳过处理
|
||
const currStatus = feature?.get('status');
|
||
const currHighlighted = feature?.get('highlighted');
|
||
if (feature && (currStatus === '2' || currHighlighted === true || feature.getGeometry()?.getType() !== 'Polygon')) {
|
||
return; // ❌ 不执行 hover 效果,也不更新 lastFeature
|
||
}
|
||
|
||
// ✅ 若进入了新的可 hover feature
|
||
if (feature && feature !== lastFeature) {
|
||
if (lastFeature) {
|
||
const lastStatus = lastFeature.get('status');
|
||
const lastHighlighted = lastFeature.get('highlighted');
|
||
|
||
if (lastStatus === '2') {
|
||
lastFeature.setStyle(successStyle(lastFeature.get('name'), scale));
|
||
} else if (lastHighlighted === true) {
|
||
lastFeature.setStyle(highlightStyle(lastFeature.get('name'), scale));
|
||
} else {
|
||
lastFeature.setStyle(defaultStyle(lastFeature.get('name'), scale));
|
||
}
|
||
}
|
||
|
||
feature.setStyle(hoverStyle(feature.get('name'), scale));
|
||
map.getTargetElement().style.cursor = 'pointer';
|
||
lastFeature = feature;
|
||
} else if (!feature && lastFeature) {
|
||
// ✅ 鼠标移出所有图形时恢复
|
||
const lastStatus = lastFeature.get('status');
|
||
const lastHighlighted = lastFeature.get('highlighted');
|
||
|
||
if (lastStatus === '2') {
|
||
lastFeature.setStyle(successStyle(lastFeature.get('name'), scale));
|
||
} else if (lastHighlighted === true) {
|
||
lastFeature.setStyle(highlightStyle(lastFeature.get('name'), scale));
|
||
} else {
|
||
lastFeature.setStyle(defaultStyle(lastFeature.get('name'), scale));
|
||
}
|
||
|
||
lastFeature = null;
|
||
}
|
||
});
|
||
};
|
||
|
||
// 你已有的 imageExtent 是 [minX, minY, maxX, maxY]
|
||
const createExtentBorderLayer = (extent: number[]) => {
|
||
// 构造矩形坐标,闭合成环,顺序可以是顺时针或逆时针
|
||
const coords = [
|
||
[
|
||
[extent[0], extent[1]],
|
||
[extent[0], extent[3]],
|
||
[extent[2], extent[3]],
|
||
[extent[2], extent[1]],
|
||
[extent[0], extent[1]]
|
||
]
|
||
];
|
||
|
||
const polygonFeature = new Feature(new Polygon(coords));
|
||
|
||
polygonFeature.setStyle(
|
||
new Style({
|
||
stroke: new Stroke({
|
||
color: 'red', // 你想要的边框颜色
|
||
width: 3 // 线宽
|
||
}),
|
||
fill: null // 不填充,纯边框
|
||
})
|
||
);
|
||
|
||
return new VectorLayer({
|
||
source: new VectorSource({
|
||
features: [polygonFeature]
|
||
})
|
||
});
|
||
};
|
||
|
||
const highlightStyle = (name, scale) => {
|
||
return new Style({
|
||
stroke: new Stroke({
|
||
color: 'orange',
|
||
width: 4
|
||
}),
|
||
fill: new Fill({
|
||
color: 'rgba(255,165,0,0.3)' // 半透明橙色
|
||
}),
|
||
text: new Text({
|
||
font: '10px Microsoft YaHei',
|
||
text: name,
|
||
placement: 'line', // 👈 关键属性
|
||
offsetX: 50, // 向右偏移 10 像素
|
||
offsetY: 20, // 向下偏移 5 像素
|
||
scale,
|
||
fill: new Fill({ color: '#FFFFFF' })
|
||
})
|
||
});
|
||
};
|
||
|
||
const defaultStyle = (name, scale) => {
|
||
return new Style({
|
||
stroke: new Stroke({
|
||
color: '#003366',
|
||
width: 2
|
||
}),
|
||
text: new Text({
|
||
font: '10px Microsoft YaHei',
|
||
text: name,
|
||
scale,
|
||
placement: 'line', // 👈 关键属性
|
||
offsetX: 50, // 向右偏移 10 像素
|
||
offsetY: 20, // 向下偏移 5 像素
|
||
fill: new Fill({ color: '#FFFFFF ' })
|
||
}),
|
||
fill: new Fill({ color: 'skyblue' })
|
||
});
|
||
};
|
||
|
||
const successStyle = (name, scale) => {
|
||
return new Style({
|
||
stroke: new Stroke({
|
||
color: '#2E7D32 ',
|
||
width: 2
|
||
}),
|
||
text: new Text({
|
||
font: '10px Microsoft YaHei',
|
||
text: name,
|
||
scale,
|
||
placement: 'line', // 👈 关键属性
|
||
offsetX: 50, // 向右偏移 10 像素
|
||
offsetY: 20, // 向下偏移 5 像素
|
||
fill: new Fill({ color: '#FFFFFF ' })
|
||
}),
|
||
fill: new Fill({ color: '#7bdd63 ' })
|
||
});
|
||
};
|
||
|
||
const hoverStyle = (name, scale) => {
|
||
return new Style({
|
||
stroke: new Stroke({
|
||
color: 'orange',
|
||
width: 2
|
||
}),
|
||
fill: new Fill({
|
||
color: 'rgba(255,165,0,0.3)' // 半透明橙色
|
||
}),
|
||
text: new Text({
|
||
font: '10px Microsoft YaHei',
|
||
text: name,
|
||
scale,
|
||
placement: 'line', // 👈 关键属性
|
||
offsetX: 50, // 向右偏移 10 像素
|
||
offsetY: 20, // 向下偏移 5 像素
|
||
fill: new Fill({ color: '#FFFFFF ' })
|
||
})
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 创建图层
|
||
* @param {*} pointObj 坐标数组
|
||
* @param {*} type 类型
|
||
* @param {*} id 唯一id
|
||
* @param {*} name 名称
|
||
* */
|
||
// 共享 source 和图层(全局一次性创建)
|
||
const sharedSource = new VectorSource();
|
||
const sharedLayer = new VectorLayer({
|
||
source: sharedSource,
|
||
renderMode: 'image' // 提高渲染性能
|
||
} as any);
|
||
// id => Feature 映射表
|
||
const featureMap: Record<string, Feature> = {};
|
||
const creatPoint = (pointObj: Array<any>, type: string, id: string, name?: string, status?: string) => {
|
||
let geometry;
|
||
|
||
if (type === 'Point') {
|
||
geometry = new Point(fromLonLat(pointObj));
|
||
} else if (type === 'Polygon') {
|
||
const coords = pointObj.map((arr: any) => fromLonLat(arr));
|
||
geometry = new Polygon([coords]);
|
||
}
|
||
|
||
const feature = new Feature({ geometry });
|
||
const zoom = map.getView().getZoom();
|
||
const scale = Math.max(zoom / 30, 1); // 缩放比例,根据需要调整公式
|
||
const pointStyle = new Style({
|
||
image: new Circle({
|
||
radius: 2,
|
||
fill: new Fill({ color: 'red' })
|
||
}),
|
||
text: new Text({
|
||
font: '12px Microsoft YaHei',
|
||
text: name,
|
||
scale,
|
||
fill: new Fill({ color: '#7bdd63' })
|
||
})
|
||
});
|
||
|
||
const polygonStyle = status == '2' ? successStyle(name, scale) : defaultStyle(name || '', scale);
|
||
|
||
feature.setStyle(type === 'Point' ? pointStyle : polygonStyle);
|
||
feature.set('name', name || ''); // 设置名称
|
||
feature.set('status', status || ''); // 设置完成状态 2为完成 其他为未完成
|
||
feature.set('id', id || '');
|
||
// 缓存 feature(用于后续判断)
|
||
featureMap[id] = feature;
|
||
};
|
||
|
||
// 添加点到地图
|
||
const addPointToMap = (features: Array<any>) => {
|
||
features.forEach((item: any) => {
|
||
const fid = item.id;
|
||
|
||
// 没创建过就先创建
|
||
if (!featureMap[fid]) {
|
||
creatPoint(item.positions, item.type, fid, item.name, item.status);
|
||
}
|
||
|
||
// 添加到共享 source 中(避免重复添加)
|
||
const feature = featureMap[fid];
|
||
if (!sharedSource.hasFeature(feature)) {
|
||
sharedSource.addFeature(feature);
|
||
}
|
||
});
|
||
};
|
||
|
||
//选中几何图形
|
||
const toggleFeatureHighlight = (feature: Feature, addIfNotExist = true) => {
|
||
const zoom = map.getView().getZoom();
|
||
const scale = Math.max(zoom / 10, 1);
|
||
if (feature.get('status') === '2') return;
|
||
if (feature.getGeometry()?.getType() !== 'Polygon') return;
|
||
|
||
const isHighlighted = feature.get('highlighted') === true;
|
||
|
||
if (isHighlighted) {
|
||
feature.setStyle(defaultStyle(feature.get('name'), scale));
|
||
feature.set('highlighted', false);
|
||
const id = feature.get('id');
|
||
const idx = submitForm.value.finishedDetailIdList.indexOf(id);
|
||
if (idx > -1) submitForm.value.finishedDetailIdList.splice(idx, 1);
|
||
} else if (addIfNotExist) {
|
||
feature.setStyle(highlightStyle(feature.get('name'), scale));
|
||
feature.set('highlighted', true);
|
||
const id = feature.get('id');
|
||
if (!submitForm.value.finishedDetailIdList.includes(id)) submitForm.value.finishedDetailIdList.push(id);
|
||
}
|
||
};
|
||
|
||
onMounted(async () => {
|
||
// 地图初始化
|
||
// geoTiffLoading.value = true;
|
||
await initGeoTiff();
|
||
initOLMap();
|
||
// geoTiffLoading.value = false;
|
||
map.addLayer(sharedLayer);
|
||
selector.value = new LassoSelector(map, sharedSource, (features, isInvert = false) => {
|
||
features.forEach((feature) => {
|
||
if (isInvert) {
|
||
// Shift + 左键 -> 只执行取消选中
|
||
if (feature.get('highlighted') === true) {
|
||
toggleFeatureHighlight(feature, false); // 取消选中,addIfNotExist = false
|
||
}
|
||
} else {
|
||
// 普通左键 -> 只执行选中
|
||
if (feature.get('highlighted') !== true) {
|
||
toggleFeatureHighlight(feature, true); // 选中,addIfNotExist = true
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
enableMiddleMousePan(map);
|
||
getList();
|
||
creatPoint(fromLonLat([107.13149145799198, 23.804125705140834]), 'Point', '1', '测试点1', '1');
|
||
});
|
||
|
||
function enableMiddleMousePan(map: Map) {
|
||
// 先移除默认的 DragPan(通常响应左键)
|
||
const interactions = map.getInteractions();
|
||
interactions.forEach((interaction) => {
|
||
if (interaction instanceof DragPan) {
|
||
map.removeInteraction(interaction);
|
||
}
|
||
});
|
||
|
||
// 添加只响应中键的 DragPan
|
||
const middleButtonDragPan = new DragPan({
|
||
condition: (event) => {
|
||
// 只允许中键 (mouse button 1) 拖动
|
||
return event.originalEvent instanceof MouseEvent && event.originalEvent.button === 1;
|
||
}
|
||
});
|
||
|
||
map.addInteraction(middleButtonDragPan);
|
||
|
||
// 禁用中键点击默认滚动行为(浏览器可能会出现滚动箭头)
|
||
// map.getViewport().addEventListener('mousedown', (e) => {
|
||
// if (e.button === 1) e.preventDefault();
|
||
// });
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.ol-map {
|
||
height: 100vh;
|
||
width: 100%;
|
||
position: absolute;
|
||
z-index: 1;
|
||
}
|
||
.header {
|
||
height: 70px;
|
||
width: 100%;
|
||
position: absolute;
|
||
z-index: 2;
|
||
background: rgba(255, 255, 255, 0.2); /* 半透明白色 */
|
||
backdrop-filter: blur(10px); /* 背景模糊 */
|
||
-webkit-backdrop-filter: blur(10px); /* 兼容 Safari */
|
||
}
|
||
.aside {
|
||
position: absolute;
|
||
top: 10%;
|
||
left: 30px;
|
||
width: 300px;
|
||
height: 70vh;
|
||
background-color: rgba(255, 255, 255, 0.8);
|
||
z-index: 3;
|
||
overflow: auto;
|
||
}
|
||
.submit {
|
||
position: absolute;
|
||
bottom: 70px;
|
||
right: 70px;
|
||
z-index: 3;
|
||
}
|
||
.custom-tree-node {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.tips {
|
||
margin: 0 15px;
|
||
position: relative;
|
||
font-size: 18px;
|
||
color: #fff;
|
||
> div {
|
||
margin: 0 25px;
|
||
position: relative;
|
||
font-size: 12px;
|
||
&::before {
|
||
position: absolute;
|
||
content: '';
|
||
display: inline-block;
|
||
left: -15px;
|
||
top: 30%;
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
}
|
||
}
|
||
.dot1 {
|
||
&::before {
|
||
background-color: #1d6fe9;
|
||
}
|
||
}
|
||
.dot2 {
|
||
&::before {
|
||
background-color: #67c23a;
|
||
}
|
||
}
|
||
.dot3 {
|
||
&::before {
|
||
background-color: #ff8d1a;
|
||
}
|
||
}
|
||
}
|
||
</style>
|