feat(map): 实现封闭区域人员定位功能

master
LiuJiaNan 2026-08-13 14:26:23 +08:00
parent c903aa9a99
commit 85885a7d8f
5 changed files with 120 additions and 12 deletions

View File

@ -60,3 +60,7 @@ export const getLargeScreenVideoPage = declareRequest(
"biFullLoading",
"Post > @/keyProject/keyProject/largeScreenVideoPage",
);
export const getClosedAreaListByCorpinfoId = declareRequest(
"biFullLoading",
"Post > @/primeport/closedArea/listByCorpinfoId",
);

View File

@ -56,6 +56,15 @@ const branchOfficeItemAnimation = {
visible: { y: 0, opacity: 1, transition: { duration: 0.3, ease: "easeOut" } },
};
// 接口使用分号分隔坐标点,仅保留可以组成多边形的有效封闭区域。
const formatClosedAreas = (data = []) => data.map(item => ({
...item,
position: (item.location || "").split(";").map((coordinate) => {
const [x, y] = coordinate.split(",").map(Number);
return { x, y };
}).filter(point => Number.isFinite(point.x) && Number.isFinite(point.y)),
})).filter(item => item.position.length >= 3);
function BottomUtils(props) {
const {
mapMethods,
@ -104,6 +113,7 @@ function BottomUtils(props) {
// 底部工具栏被外部操作重置时,人员定位的 WebSocket 和 Cesium 实体不会随勾选状态自动销毁,需要统一清理。
peoplePositionDisconnect();
mapMethods.current.removePeoplePointAll();
mapMethods.current.removePeoplePositionArea();
setPeopleList([]);
setList(produce((draft) => {
draft.forEach((item) => {
@ -189,20 +199,59 @@ function BottomUtils(props) {
const optionsItemsClick = async (index, index1, item) => {
const check = !list[index].list[index1].check;
updateOptionCheck(index, index1, check);
// 两种定位共用同一套人员实体和 WebSocket冲突时保留当前状态并让用户主动取消已开启功能。
if (check && item.type === "person" && list.some(group => group.list?.some(option => option.type === "peoplePosition" && option.check))) {
message.warning("请先取消【人员定位】中的【人员定位】");
return;
}
if (check && item.type === "peoplePosition" && list.some(group => group.list?.some(option => option.type === "person" && option.check))) {
message.warning("请先取消【封闭区域】中的【人员】");
return;
}
if (!check) {
if (item.type === "peoplePosition") {
updateOptionCheck(index, index1, false);
resetPeopleTrajectory(index);
mapMethods.current.removePeopleTrajectory();
peoplePositionDisconnect();
mapMethods.current.removePeoplePointAll();
mapMethods.current.removePeoplePositionArea();
setPeopleList([]);
return;
}
if (item.type === "person" && currentPort === "00003") {
updateOptionCheck(index, index1, false);
peoplePositionDisconnect();
mapMethods.current.removePeoplePointAll();
mapMethods.current.removePeoplePositionArea();
setPeopleList([]);
return;
}
updateOptionCheck(index, index1, false);
mapMethods.current.removeMarkPoint(item.type);
return;
}
if (item.type === "person" && currentPort === "00003") {
const { data } = await props.getClosedAreaListByCorpinfoId({ corpinfoId: currentBranchOffice, pageIndex: 1, pageSize: 9999 });
const areaList = formatClosedAreas(data || []);
if (areaList.length === 0) {
message.warning("暂无有效封闭区域");
return;
}
updateOptionCheck(index, index1, true);
mapMethods.current.removePeoplePointAll();
mapMethods.current.addPeoplePositionArea(areaList);
mapMethods.current.initPeoplePositionClock();
peoplePositionConnect(areaList);
return;
}
if (item.type === "peoplePosition" && currentPort === "00003") {
updateOptionCheck(index, index1, true);
mapMethods.current.removePeoplePositionArea();
// 连接 WebSocket 前初始化本次定位会话的统一时钟,首批人员共享同一采样基准。
mapMethods.current.initPeoplePositionClock();
peoplePositionConnect();
@ -215,11 +264,11 @@ function BottomUtils(props) {
);
if (!isPeoplePositionChecked) {
message.warning("请先选择人员定位!!!");
updateOptionCheck(index, index1, false);
return;
}
}
updateOptionCheck(index, index1, true);
await addMarkPoint(item);
};

View File

@ -255,6 +255,35 @@ export default function useMapMethods(viewerRef, request) {
removeEntityCollection(viewer, "fourColorDiagramEntityCollection");
};
// 人员点位使用贴地显示,区域也通过地形和 3D Tiles 分类贴地,避免零高度多边形被倾斜摄影遮挡。
const addPeoplePositionArea = (areaList = []) => {
const viewer = getViewer();
removeEntityCollection(viewer, "peoplePositionAreaEntityCollection");
const entityCollection = createEntityCollection("peoplePositionAreaEntityCollection");
viewer.dataSources.add(entityCollection);
areaList.forEach((area) => {
entityCollection.entities.add(
new Cesium.Entity({
id: createId(),
name: area.closedAreaName,
polygon: {
hierarchy: Cesium.Cartesian3.fromDegreesArray(formatPolygon(area)),
material: Cesium.Color.fromCssColorString("rgba(221,0,0,0.4)"),
outline: true,
outlineColor: Cesium.Color.fromCssColorString("rgba(51,35,108,0.63)"),
classificationType: Cesium.ClassificationType.BOTH,
zIndex: 1,
},
}),
);
});
};
const removePeoplePositionArea = () => {
const viewer = getViewer();
removeEntityCollection(viewer, "peoplePositionAreaEntityCollection");
};
// 添加墙
const addWall = (id, corpionId) => {
const edgeMap = {
@ -552,6 +581,8 @@ export default function useMapMethods(viewerRef, request) {
removeBranchOfficePoint,
addFourColorDiagram,
removeFourColorDiagram,
addPeoplePositionArea,
removePeoplePositionArea,
addWall,
removeWall,
addMarkPoint,

View File

@ -1,11 +1,12 @@
import { useWebSocket } from "ahooks";
import { useRef } from "react";
import edgeQHD from "./edge/qhd.js";
import { isPointWithinTheArea } from "./utils";
import { isPointInAreas, isPointWithinTheArea } from "./utils";
export default function usePeoplePosition(mapMethods, currentBranchOffice, portArea, onPeopleChange) {
const markType = "peoplePosition";
const points = useRef([]);
const filterAreas = useRef([]);
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
// 生产环境的接口地址由 index.html 在运行时注入 window.process不能使用会在打包阶段被替换的 process.env。
@ -19,14 +20,27 @@ export default function usePeoplePosition(mapMethods, currentBranchOffice, portA
const peopleList = Array.isArray(data) ? data : [data];
for (let i = 0; i < peopleList.length; i++) {
const item = peopleList[i];
if (!item?.locationRawJson)
continue;
const location = JSON.parse(item.locationRawJson).position;
if (!item || !location || !location.lon || !location.lat)
if (!location || !location.lon || !location.lat)
continue;
const id = item.terminalNo || item.staffNo;
if (!id)
continue;
const index = points.current.findIndex(
item1 => item1.id.toString() === (item.terminalNo || item.staffNo).toString(),
item1 => item1.id.toString() === id.toString(),
);
const x = location.lon;
const y = location.lat;
// 封闭区域模式只保留围栏内人员;人员离开围栏后立即移除已有点位。
if (filterAreas.current.length > 0 && !isPointInAreas(filterAreas.current, x, y)) {
if (index !== -1) {
mapMethods.current?.removePeoplePoint(points.current[index].id);
points.current.splice(index, 1);
}
continue;
}
const pointColor = isPointWithinTheArea(edgeQHD(), x, y);
if (index !== -1) {
points.current[index].x = x;
@ -37,7 +51,7 @@ export default function usePeoplePosition(mapMethods, currentBranchOffice, portA
else {
const perLoc = {
...item,
id: item.terminalNo || item.staffNo,
id,
name: item.staffName,
x,
y,
@ -66,5 +80,15 @@ export default function usePeoplePosition(mapMethods, currentBranchOffice, portA
},
);
return { connect, disconnect };
const connectPeoplePosition = (areas = []) => {
filterAreas.current = areas;
connect();
};
const disconnectPeoplePosition = () => {
filterAreas.current = [];
disconnect();
};
return { connect: connectPeoplePosition, disconnect: disconnectPeoplePosition };
}

View File

@ -246,7 +246,7 @@ export function textFormatter(value, maxLength = 4) {
}
}
const isPointInPolygon = (polygon, lng, lat) => {
export const isPointInPolygon = (polygon, lng, lat) => {
const numberOfPoints = polygon.length;
const polygonLats = [];
const polygonLngs = [];
@ -273,19 +273,19 @@ const isPointInPolygon = (polygon, lng, lat) => {
}
return polygonContainsPoint;
};
const forEachIsPointInPolygon = (polygonList = [], lng, lat) => {
export const isPointInAreas = (polygonList = [], lng, lat) => {
return polygonList.some((item) => {
return isPointInPolygon(item.position, lng, lat);
});
};
export const isPointWithinTheArea = (pointBox = {}, x, y) => {
if (forEachIsPointInPolygon(pointBox.redList, x, y)) {
if (isPointInAreas(pointBox.redList, x, y)) {
return "red";
}
else if (forEachIsPointInPolygon(pointBox.orangeList, x, y)) {
else if (isPointInAreas(pointBox.orangeList, x, y)) {
return "orange";
}
else if (forEachIsPointInPolygon(pointBox.yellowList, x, y)) {
else if (isPointInAreas(pointBox.yellowList, x, y)) {
return "yellow";
}
else {