|
|
@@ -1,68 +1,73 @@
|
|
|
-import vjmap, { Map } from 'vjmap';
|
|
|
+import vjmap, { Map, GeoBounds, GeoProjection, Service } from 'vjmap';
|
|
|
import { env } from './env';
|
|
|
import { App, MapThreeLayer } from 'vjmap3d';
|
|
|
import 'vjmap/dist/vjmap.min.css';
|
|
|
import { useAppStore } from '/@/store/modules/app';
|
|
|
+import { message, Modal } from 'ant-design-vue';
|
|
|
+import { updatateGoaf } from './cad.api';
|
|
|
+import { createApp, h } from 'vue';
|
|
|
+import GoafPopup from './components/goafPopup.vue';
|
|
|
|
|
|
+// ===================== 全局状态 =====================
|
|
|
+let map: Map | null = null;
|
|
|
+let svc: Service | null = null;
|
|
|
+let prj: GeoProjection | null = null;
|
|
|
+let allMarkers: any[] = [];
|
|
|
+let sensorId = 1;
|
|
|
+let currentPopup: any = null;
|
|
|
+
|
|
|
+// ===================== 地图初始化部分 =====================
|
|
|
+// 初始化地图
|
|
|
const createMapApp = async () => {
|
|
|
const appStore = useAppStore();
|
|
|
const containerId = 'map3dContainer';
|
|
|
- // @ts-ignore
|
|
|
const containerDiv = document.getElementById(containerId);
|
|
|
- // document.getElementById(containerId).style.background = env.background || (env.darkTheme ? '#2c3e50' : 'rgb(200, 211, 220)');
|
|
|
- containerDiv.style.background = 'transparent';
|
|
|
- // debugger;
|
|
|
- containerDiv.style.width = containerDiv?.parentElement?.clientWidth / appStore.widthScale + 'px';
|
|
|
- containerDiv.style.height = containerDiv?.parentElement?.clientHeight / appStore.heightScale + 'px';
|
|
|
-
|
|
|
- const svc = new vjmap.Service(env.serviceUrl, env.accessToken);
|
|
|
- if (env.workspace) {
|
|
|
- // 如果有工作区,切换至相对应的工作区
|
|
|
- svc.switchWorkspace(env.workspace);
|
|
|
+
|
|
|
+ if (!containerDiv) {
|
|
|
+ message.error("地图容器不存在!");
|
|
|
+ return null;
|
|
|
}
|
|
|
- // 打开地图
|
|
|
+
|
|
|
+ containerDiv.style.background = 'transparent';
|
|
|
+ containerDiv.style.width = `${containerDiv?.parentElement?.clientWidth / appStore.widthScale}px`;
|
|
|
+ containerDiv.style.height = `${containerDiv?.parentElement?.clientHeight / appStore.heightScale}px`;
|
|
|
+
|
|
|
+ svc = new vjmap.Service(env.serviceUrl, env.accessToken);
|
|
|
+ if (env.workspace) svc.switchWorkspace(env.workspace);
|
|
|
+
|
|
|
const style = env.darkTheme ? vjmap.openMapDarkStyle() : vjmap.openMapLightStyle();
|
|
|
- style.clipbounds = Math.pow(2, 6); // 只传值,不传范围,表示之前的范围放大多少倍
|
|
|
- // const httpDwgUrl = 'https://vjmap.com/static/assets/data/gym.dwg'; // 长传到服务器上的dwg文件
|
|
|
- const httpDwgUrl = './test.dwg';
|
|
|
+ style.clipbounds = Math.pow(2, 6);
|
|
|
+ const httpDwgUrl = 'https://vjmap.com/static/assets/data/gym.dwg';
|
|
|
+
|
|
|
const res = await svc.openMap({
|
|
|
- // mapid: env.mapId, // 地图ID,上传文件后获得的mapid
|
|
|
version: env.version,
|
|
|
- fileid: httpDwgUrl, // 使用文件地址
|
|
|
- // @ts-ignore
|
|
|
- mapopenway: env.mapopenway || vjmap.MapOpenWay.GeomRender, // 以几何数据渲染方式打开
|
|
|
+ fileid: httpDwgUrl,
|
|
|
+ mapopenway: env.mapopenway || vjmap.MapOpenWay.GeomRender,
|
|
|
style,
|
|
|
});
|
|
|
+
|
|
|
if (res.error) {
|
|
|
- // 如果打开出错
|
|
|
+ message.error(`地图加载失败:${res.error}`);
|
|
|
console.error(res.error);
|
|
|
}
|
|
|
- // 获取地图范围
|
|
|
- const mapExtent = vjmap.GeoBounds.fromString(res.bounds);
|
|
|
- // 根据地图范围建立几何投影坐标系
|
|
|
- const prj = new vjmap.GeoProjection(mapExtent);
|
|
|
-
|
|
|
- // 地图对象
|
|
|
- const map = new vjmap.Map({
|
|
|
- container: containerId, // DIV容器ID
|
|
|
- style: svc.rasterStyle(), // 样式,这里是栅格样式
|
|
|
- center: prj.toLngLat(mapExtent.center()), // 设置地图中心点
|
|
|
- zoom: 7, // 设置地图缩放级别
|
|
|
+
|
|
|
+ const mapExtent = GeoBounds.fromString(res.bounds);
|
|
|
+ prj = new GeoProjection(mapExtent);
|
|
|
+
|
|
|
+ map = new vjmap.Map({
|
|
|
+ container: containerId,
|
|
|
+ style: svc.rasterStyle(),
|
|
|
+ center: prj.toLngLat(mapExtent.center()),
|
|
|
+ zoom: 7,
|
|
|
pitch: 0,
|
|
|
- antialias: true, // 反锯齿
|
|
|
- renderWorldCopies: false, // 不显示多屏地图
|
|
|
+ antialias: true,
|
|
|
+ renderWorldCopies: false,
|
|
|
});
|
|
|
|
|
|
- // 关联服务对象和投影对象
|
|
|
- // map.attach(svc, prj);
|
|
|
await map.onLoad();
|
|
|
|
|
|
- // 创建3d图层
|
|
|
const mapLayer = new MapThreeLayer(map, {
|
|
|
- stat: {
|
|
|
- show: false,
|
|
|
- left: '0',
|
|
|
- },
|
|
|
+ stat: { show: false, left: '0' },
|
|
|
scene: {
|
|
|
showAxesHelper: false,
|
|
|
axesHelperSize: 1,
|
|
|
@@ -77,11 +82,411 @@ const createMapApp = async () => {
|
|
|
});
|
|
|
const app: App = mapLayer.app;
|
|
|
map.addLayer(new vjmap.ThreeLayer({ context: mapLayer as any }));
|
|
|
+ map.doubleClickZoom.disable();
|
|
|
|
|
|
- map.doubleClickZoom.disable(); // 禁止双击缩放
|
|
|
return app;
|
|
|
};
|
|
|
+
|
|
|
+// 对外导出地图初始化方法
|
|
|
export const initMap2d = async () => {
|
|
|
const app = await createMapApp();
|
|
|
return app;
|
|
|
};
|
|
|
+
|
|
|
+// ===================== 地图操作处理部分 =====================
|
|
|
+// 拾取点位
|
|
|
+const pickPoint = async (markerOptions: any) => {
|
|
|
+ if (!map) return null;
|
|
|
+ let marker: any = null;
|
|
|
+ const actionPoint = await vjmap.Draw.actionDrawPoint(map, {
|
|
|
+ updatecoordinate: (e: any) => {
|
|
|
+ if (!e.lnglat) return;
|
|
|
+ if (!marker) {
|
|
|
+ marker = new vjmap.createMarker(markerOptions);
|
|
|
+ marker.setLngLat(e.lnglat);
|
|
|
+ marker.addTo(map);
|
|
|
+ } else {
|
|
|
+ marker.setLngLat(e.lnglat);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ contextMenu: (e: any) => {
|
|
|
+ new vjmap.ContextMenu({
|
|
|
+ event: e.event.originalEvent,
|
|
|
+ theme: "dark",
|
|
|
+ width: "250px",
|
|
|
+ items: [
|
|
|
+ {
|
|
|
+ label: '取消',
|
|
|
+ onClick: () => map?.fire("keyup", { keyCode: 27 })
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (actionPoint.cancel) {
|
|
|
+ marker?.remove();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return marker;
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 判断坐标是否有效
|
|
|
+ */
|
|
|
+export const getGoafIsPositioned = (goafItem: any): boolean => {
|
|
|
+ const hasValidX = isValidSingleCoord(goafItem.xcoordinate);
|
|
|
+ const hasValidY = isValidSingleCoord(goafItem.ycoordinate);
|
|
|
+ return hasValidX && hasValidY;
|
|
|
+};
|
|
|
+
|
|
|
+// 获取所有标记状态列表
|
|
|
+export const getMarkerStatusList = () => {
|
|
|
+ if (!map) return [];
|
|
|
+ return allMarkers.map(marker => ({
|
|
|
+ sensorId: marker.data?.sensorId,
|
|
|
+ id: marker.data?.id,
|
|
|
+ data: marker.data,
|
|
|
+ lnglat: marker.getLngLat(),
|
|
|
+ position: map?.fromLngLat(marker.getLngLat())
|
|
|
+ }));
|
|
|
+};
|
|
|
+
|
|
|
+// 移除指定传感器标记
|
|
|
+export const removePosition = async (sensorId: number | string, id: string | number) => {
|
|
|
+ if (!map) {
|
|
|
+ message.error("地图未初始化完成!");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const targetIndex = allMarkers.findIndex(marker => marker.data?.sensorId === sensorId || marker.data?.id === id);
|
|
|
+ if (targetIndex === -1) {
|
|
|
+ message.warning("未找到该传感器标记!");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await updatateGoaf({
|
|
|
+ id: id,
|
|
|
+ xcoordinate: "",
|
|
|
+ ycoordinate: ""
|
|
|
+ });
|
|
|
+ console.info("移除布点成功!");
|
|
|
+ } catch (apiError) {
|
|
|
+ message.error(`同步移除信息失败:${(apiError as Error).message}`);
|
|
|
+ console.error("移除接口调用失败:", apiError);
|
|
|
+ }
|
|
|
+
|
|
|
+ allMarkers[targetIndex].remove();
|
|
|
+ allMarkers.splice(targetIndex, 1);
|
|
|
+ syncLocalStorage();
|
|
|
+
|
|
|
+ return true;
|
|
|
+};
|
|
|
+
|
|
|
+// 布点方法
|
|
|
+export const bindPosition = async (props?: any) => {
|
|
|
+ if (!map || !svc) {
|
|
|
+ message.error("地图未初始化完成,请先加载地图!");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ map.fire("keyup", { keyCode: 27 });
|
|
|
+
|
|
|
+ const el = createMarkerElement({
|
|
|
+ alarmLevel: props?.alarmLevel,
|
|
|
+ devicePos: props?.devicePos
|
|
|
+ }, false);
|
|
|
+ const markerOptions = {
|
|
|
+ element: el,
|
|
|
+ anchor: 'bottom',
|
|
|
+ offset: [0, 0]
|
|
|
+ };
|
|
|
+
|
|
|
+ let marker: any = null;
|
|
|
+ if (!props || !props.position) {
|
|
|
+ marker = await pickPoint(markerOptions);
|
|
|
+ if (!marker) return null;
|
|
|
+
|
|
|
+ const confirmResult = await new Promise<boolean>((resolve) => {
|
|
|
+ Modal.confirm({
|
|
|
+ title: '保存布点',
|
|
|
+ content: '是否确认保存该布点?',
|
|
|
+ okText: '确定',
|
|
|
+ cancelText: '取消',
|
|
|
+ maskClosable: false,
|
|
|
+ centered: true,
|
|
|
+ onOk: () => resolve(true),
|
|
|
+ onCancel: () => resolve(false)
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!confirmResult) {
|
|
|
+ marker.remove();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const lnglat = marker.getLngLat();
|
|
|
+ const id = props?.id;
|
|
|
+
|
|
|
+ if (!id) {
|
|
|
+ marker.remove();
|
|
|
+ message.error("缺少密闭ID,无法同步布点信息到服务器");
|
|
|
+ throw new Error("缺少密闭ID,无法同步布点信息到服务器");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await updatateGoaf({
|
|
|
+ id: id,
|
|
|
+ xcoordinate: lnglat.lng.toString(),
|
|
|
+ ycoordinate: lnglat.lat.toString()
|
|
|
+ });
|
|
|
+ } catch (apiError) {
|
|
|
+ marker.remove();
|
|
|
+ message.error(`同步布点信息失败:${(apiError as Error).message}`);
|
|
|
+ throw new Error(`同步布点信息失败:${(apiError as Error).message}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentLngLat = marker.getLngLat();
|
|
|
+ marker.remove();
|
|
|
+
|
|
|
+ const finalEl = createMarkerElement({
|
|
|
+ alarmLevel: props?.alarmLevel,
|
|
|
+ devicePos: props?.devicePos
|
|
|
+ }, true);
|
|
|
+
|
|
|
+ marker = vjmap.createMarker({
|
|
|
+ element: finalEl,
|
|
|
+ anchor: 'bottom',
|
|
|
+ offset: [0, 0]
|
|
|
+ });
|
|
|
+ marker.setLngLat(currentLngLat);
|
|
|
+ marker.addTo(map);
|
|
|
+ marker.data = {
|
|
|
+ sensorId: sensorId++,
|
|
|
+ id: id,
|
|
|
+ ...(props || {})
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ if (!Array.isArray(props.position)) {
|
|
|
+ throw new Error("预设坐标position格式错误,必须是[x, y]数组");
|
|
|
+ }
|
|
|
+ const lngLat = map.toLngLat(props.position);
|
|
|
+ if (!lngLat) {
|
|
|
+ throw new Error("转换坐标失败,无效的position");
|
|
|
+ }
|
|
|
+
|
|
|
+ marker = vjmap.createMarker(markerOptions);
|
|
|
+ marker.setLngLat(lngLat);
|
|
|
+ marker.data = {
|
|
|
+ ...props.data,
|
|
|
+ sensorId: props.data?.sensorId || sensorId++,
|
|
|
+ id: props.data?.id || props.id,
|
|
|
+ ...(props || {})
|
|
|
+ };
|
|
|
+ marker.addTo(map);
|
|
|
+ }
|
|
|
+
|
|
|
+ bindMarkerEvents(marker, marker.data?.id);
|
|
|
+ allMarkers.push(marker);
|
|
|
+ syncLocalStorage();
|
|
|
+
|
|
|
+ return {
|
|
|
+ sensorId: marker.data.sensorId,
|
|
|
+ id: marker.data.id,
|
|
|
+ lnglat: marker.getLngLat(),
|
|
|
+ xcoordinate: marker.getLngLat().lng.toString(),
|
|
|
+ ycoordinate: marker.getLngLat().lat.toString()
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+// 渲染密闭列表标记
|
|
|
+export const renderGoafMarkers = async (goafList: any[]) => {
|
|
|
+ if (!map || !prj) {
|
|
|
+ message.error("地图或投影坐标系未初始化完成!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ allMarkers.forEach(marker => marker.remove());
|
|
|
+ allMarkers = [];
|
|
|
+ sensorId = 1;
|
|
|
+
|
|
|
+ const validGoafList = goafList.filter(item => getGoafIsPositioned(item));
|
|
|
+ if (validGoafList.length === 0) {
|
|
|
+ console.info("当前密闭列表无有效布点坐标,暂无标记可渲染");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const goafItem of validGoafList) {
|
|
|
+ try {
|
|
|
+ const lnglat = new vjmap.LngLat(Number(goafItem.xcoordinate), Number(goafItem.ycoordinate));
|
|
|
+ const el = createMarkerElement(goafItem, true);
|
|
|
+
|
|
|
+ const marker = vjmap.createMarker({
|
|
|
+ element: el,
|
|
|
+ anchor: 'bottom',
|
|
|
+ offset: [0, 0]
|
|
|
+ });
|
|
|
+ marker.setLngLat(lnglat);
|
|
|
+ marker.data = {
|
|
|
+ sensorId: sensorId++,
|
|
|
+ id: goafItem.id,
|
|
|
+ ...goafItem
|
|
|
+ };
|
|
|
+ marker.addTo(map);
|
|
|
+
|
|
|
+ bindMarkerEvents(marker, goafItem.id);
|
|
|
+ allMarkers.push(marker);
|
|
|
+ } catch (err) {
|
|
|
+ console.error(`渲染密闭${goafItem.id}标记失败:`, err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// ===================== 内部辅助函数 =====================
|
|
|
+/**
|
|
|
+ * 校验单个坐标是否有效
|
|
|
+ */
|
|
|
+const isValidSingleCoord = (coord: any): boolean => {
|
|
|
+ return coord && coord.trim() !== "" && !isNaN(Number(coord));
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 同步标记数据到本地缓存
|
|
|
+ */
|
|
|
+const syncLocalStorage = (): void => {
|
|
|
+ if (!map) return;
|
|
|
+ const markerData = allMarkers.map((m) => ({
|
|
|
+ position: map?.fromLngLat(m.getLngLat()),
|
|
|
+ data: m.data
|
|
|
+ }));
|
|
|
+ localStorage.setItem("marker_sensor", JSON.stringify(markerData));
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 关闭并清理弹窗
|
|
|
+ */
|
|
|
+const closeAndCleanPopup = (): void => {
|
|
|
+ if (currentPopup) {
|
|
|
+ currentPopup.remove();
|
|
|
+ currentPopup = null;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 绑定标记的拖拽和点击事件
|
|
|
+ */
|
|
|
+const bindMarkerEvents = (marker: any, id: string | number): void => {
|
|
|
+ marker.setDraggable(false);
|
|
|
+ let isDragging = false;
|
|
|
+
|
|
|
+ marker.on("dragstart", () => { isDragging = false; });
|
|
|
+ marker.on("dragend", () => {
|
|
|
+ setTimeout(() => { isDragging = false; }, 150);
|
|
|
+ const lnglat = marker.getLngLat();
|
|
|
+ if (id) {
|
|
|
+ updatateGoaf({
|
|
|
+ id,
|
|
|
+ xcoordinate: lnglat.lng.toString(),
|
|
|
+ ycoordinate: lnglat.lat.toString()
|
|
|
+ }).catch((err: Error) => {
|
|
|
+ message.error(`拖拽后同步坐标失败:${err.message}`);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ marker.getElement().addEventListener('click', () => {
|
|
|
+ if (!isDragging) {
|
|
|
+ showMarkerPopup(marker);
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+/**
|
|
|
+ * 创建传感器标记 DOM 元素
|
|
|
+ */
|
|
|
+const createMarkerElement = (goafItem: any, showInfo: boolean = false): HTMLDivElement => {
|
|
|
+ const customImage = `/src/assets/icons/location-icon.svg`;
|
|
|
+ const customImage3D = `/src/assets/icons/location-icon3D.png`;
|
|
|
+ const el = document.createElement('div');
|
|
|
+ el.className = 'marker';
|
|
|
+ el.style.cssText = `
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ cursor: move;
|
|
|
+ `;
|
|
|
+
|
|
|
+ if (showInfo) {
|
|
|
+ const alarmLevelDiv = document.createElement('div');
|
|
|
+ alarmLevelDiv.style.cssText = `
|
|
|
+ font-size: 14px;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 2px;
|
|
|
+ white-space: nowrap;
|
|
|
+ font-weight: 600;
|
|
|
+ `;
|
|
|
+ alarmLevelDiv.textContent = goafItem.alarmLevel;
|
|
|
+ el.appendChild(alarmLevelDiv);
|
|
|
+ }
|
|
|
+
|
|
|
+ const iconDiv = document.createElement('div');
|
|
|
+ iconDiv.style.cssText = `
|
|
|
+ background-image: url("${showInfo ? customImage3D : customImage}");
|
|
|
+ width: 40px;
|
|
|
+ height: 45px;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ background-position: center;
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ `;
|
|
|
+ el.appendChild(iconDiv);
|
|
|
+
|
|
|
+ if (showInfo) {
|
|
|
+ const devicePosDiv = document.createElement('div');
|
|
|
+ devicePosDiv.style.cssText = `
|
|
|
+ position: absolute;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #333;
|
|
|
+ margin-top: 2px;
|
|
|
+ white-space: nowrap;
|
|
|
+ max-width: 120px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ bottom: -20px;
|
|
|
+ `;
|
|
|
+ devicePosDiv.textContent = goafItem.devicePos;
|
|
|
+ el.appendChild(devicePosDiv);
|
|
|
+ }
|
|
|
+
|
|
|
+ return el;
|
|
|
+};
|
|
|
+
|
|
|
+// 显示标记信息弹窗
|
|
|
+const showMarkerPopup = (marker: any) => {
|
|
|
+ if (!map) return;
|
|
|
+
|
|
|
+ closeAndCleanPopup();
|
|
|
+
|
|
|
+ const lnglat = marker.getLngLat();
|
|
|
+ const data = marker.data || {};
|
|
|
+
|
|
|
+ const tempContainer = document.createElement('div');
|
|
|
+ const popupApp = createApp({
|
|
|
+ render: () => h(GoafPopup, { data })
|
|
|
+ });
|
|
|
+ popupApp.mount(tempContainer);
|
|
|
+
|
|
|
+ currentPopup = new vjmap.Popup({
|
|
|
+ closeButton: true,
|
|
|
+ closeOnClick: false,
|
|
|
+ maxWidth: '800px',
|
|
|
+ offset: [0, 0],
|
|
|
+ })
|
|
|
+ .setLngLat(lnglat)
|
|
|
+ .setDOMContent(tempContainer)
|
|
|
+ .addTo(map);
|
|
|
+
|
|
|
+ currentPopup.on('close', () => {
|
|
|
+ popupApp.unmount();
|
|
|
+ tempContainer.remove();
|
|
|
+ currentPopup = null;
|
|
|
+ });
|
|
|
+};
|