@collabdt/core 0.0.57 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -43
- package/dist/core/components/AppSidebarContent.d.ts.map +1 -1
- package/dist/core/components/AppSidebarContent.js +21 -27
- package/dist/core/components/AppSidebarContent.js.map +1 -1
- package/dist/core/components/authentication/AuthPage.d.ts.map +1 -1
- package/dist/core/components/authentication/AuthPage.js +32 -7
- package/dist/core/components/authentication/AuthPage.js.map +1 -1
- package/dist/core/components/authentication/SignInAuthHeader.d.ts +7 -0
- package/dist/core/components/authentication/SignInAuthHeader.d.ts.map +1 -0
- package/dist/core/components/authentication/SignInAuthHeader.js +32 -0
- package/dist/core/components/authentication/SignInAuthHeader.js.map +1 -0
- package/dist/core/components/authentication/Signin.d.ts.map +1 -1
- package/dist/core/components/authentication/Signin.js +308 -10
- package/dist/core/components/authentication/Signin.js.map +1 -1
- package/dist/core/components/settings/index.js +3 -3
- package/dist/core/components/settings/index.js.map +1 -1
- package/dist/core/components/settings/src/OrganizationSettingsPanel.d.ts.map +1 -1
- package/dist/core/components/settings/src/OrganizationSettingsPanel.js +5 -4
- package/dist/core/components/settings/src/OrganizationSettingsPanel.js.map +1 -1
- package/dist/core/components/settings/src/SettingsSidebar.d.ts.map +1 -1
- package/dist/core/components/settings/src/SettingsSidebar.js +12 -2
- package/dist/core/components/settings/src/SettingsSidebar.js.map +1 -1
- package/dist/core/components/support/BugReportDialog.d.ts.map +1 -1
- package/dist/core/components/support/BugReportDialog.js +122 -9
- package/dist/core/components/support/BugReportDialog.js.map +1 -1
- package/dist/core/components/support/FeatureRequestDialog.d.ts.map +1 -1
- package/dist/core/components/support/FeatureRequestDialog.js +61 -2
- package/dist/core/components/support/FeatureRequestDialog.js.map +1 -1
- package/dist/core/components/ui/AppSidebar.d.ts.map +1 -1
- package/dist/core/components/ui/AppSidebar.js +52 -9
- package/dist/core/components/ui/AppSidebar.js.map +1 -1
- package/dist/core/components/ui/DataTable.js +3 -3
- package/dist/core/components/ui/DataTable.js.map +1 -1
- package/dist/core/components/ui/Dialog.js +1 -1
- package/dist/core/components/ui/Dialog.js.map +1 -1
- package/dist/core/components/ui/InfoSidebar/Container.js +1 -1
- package/dist/core/components/ui/InfoSidebar/Container.js.map +1 -1
- package/dist/core/components/ui/Sensors/CollapsibleSensorItem.d.ts.map +1 -1
- package/dist/core/components/ui/Sensors/CollapsibleSensorItem.js +2 -2
- package/dist/core/components/ui/Sensors/CollapsibleSensorItem.js.map +1 -1
- package/dist/core/components/ui/Sensors/SensorsSection.d.ts.map +1 -1
- package/dist/core/components/ui/Sensors/SensorsSection.js +2 -1
- package/dist/core/components/ui/Sensors/SensorsSection.js.map +1 -1
- package/dist/core/components/ui/Sensors/sensorUtils.d.ts +9 -0
- package/dist/core/components/ui/Sensors/sensorUtils.d.ts.map +1 -1
- package/dist/core/components/ui/Sensors/sensorUtils.js +6 -0
- package/dist/core/components/ui/Sensors/sensorUtils.js.map +1 -1
- package/dist/core/components/ui/Sidebar.d.ts +4 -0
- package/dist/core/components/ui/Sidebar.d.ts.map +1 -1
- package/dist/core/components/ui/Sidebar.js +11 -5
- package/dist/core/components/ui/Sidebar.js.map +1 -1
- package/dist/core/components/ui/Table.js +2 -2
- package/dist/core/components/ui/Table.js.map +1 -1
- package/dist/core/components/viewers/Data/DataMenu.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/DataMenu.js +17 -9
- package/dist/core/components/viewers/Data/DataMenu.js.map +1 -1
- package/dist/core/components/viewers/Data/HeaderButtons.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/HeaderButtons.js +5 -12
- package/dist/core/components/viewers/Data/HeaderButtons.js.map +1 -1
- package/dist/core/components/viewers/Data/MoreOptions.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/MoreOptions.js +14 -27
- package/dist/core/components/viewers/Data/MoreOptions.js.map +1 -1
- package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.js +26 -0
- package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.js.map +1 -1
- package/dist/core/components/viewers/Data/buildingDetails/FieldRenderer.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/buildingDetails/FieldRenderer.js +13 -18
- package/dist/core/components/viewers/Data/buildingDetails/FieldRenderer.js.map +1 -1
- package/dist/core/components/viewers/Data/details/DetailHeader.js +14 -14
- package/dist/core/components/viewers/Data/details/DetailHeader.js.map +1 -1
- package/dist/core/components/viewers/Data/details/FieldValue.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/details/FieldValue.js +13 -3
- package/dist/core/components/viewers/Data/details/FieldValue.js.map +1 -1
- package/dist/core/components/viewers/Data/details/TabSidebar.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/details/TabSidebar.js +22 -0
- package/dist/core/components/viewers/Data/details/TabSidebar.js.map +1 -1
- package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.js +19 -0
- package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.js.map +1 -1
- package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.d.ts +2 -0
- package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.js +84 -3
- package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.js.map +1 -1
- package/dist/core/components/viewers/Data/siteDetails/SiteDetails.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/siteDetails/SiteDetails.js +42 -2
- package/dist/core/components/viewers/Data/siteDetails/SiteDetails.js.map +1 -1
- package/dist/core/components/viewers/Data/utils/Columns.js +2 -2
- package/dist/core/components/viewers/Data/utils/Columns.js.map +1 -1
- package/dist/core/components/viewers/Data/utils/positionParser.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/utils/positionParser.js +6 -5
- package/dist/core/components/viewers/Data/utils/positionParser.js.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.js +8 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.js.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.js +7 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.js.map +1 -1
- package/dist/core/components/viewers/map/datasets/RowActions.js +6 -6
- package/dist/core/components/viewers/map/datasets/RowActions.js.map +1 -1
- package/dist/core/components/viewers/map/datasets/data.js +1 -1
- package/dist/core/components/viewers/map/datasets/data.js.map +1 -1
- package/dist/core/components/viewers/map/datasets/index.d.ts.map +1 -1
- package/dist/core/components/viewers/map/datasets/index.js +28 -9
- package/dist/core/components/viewers/map/datasets/index.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/index.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/index.js +2 -0
- package/dist/core/components/viewers/map/src/MapLayers/index.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.d.ts +19 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.js +163 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.js.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.d.ts +3 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.js +310 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.js.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.d.ts +30 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.js +122 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.js.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.d.ts +13 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.js +58 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.js.map +1 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.d.ts +18 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.js +102 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.js.map +1 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.js +243 -176
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.js.map +1 -1
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.d.ts +24 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.js +46 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.js.map +1 -0
- package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.d.ts +1 -0
- package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.d.ts.map +1 -1
- package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.js +1 -0
- package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.js.map +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.d.ts.map +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.js +54 -31
- package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.js.map +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/index.js +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/index.js.map +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.d.ts +2 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.d.ts.map +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.js +5 -4
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.js.map +1 -1
- package/dist/core/hooks/sites/createSiteHooks.d.ts.map +1 -1
- package/dist/core/hooks/sites/createSiteHooks.js +0 -1
- package/dist/core/hooks/sites/createSiteHooks.js.map +1 -1
- package/dist/core/hooks/useCaptureScreenshot.d.ts.map +1 -1
- package/dist/core/hooks/useCaptureScreenshot.js +111 -33
- package/dist/core/hooks/useCaptureScreenshot.js.map +1 -1
- package/dist/core/hooks/useCurrentViewerPosition.d.ts +18 -0
- package/dist/core/hooks/useCurrentViewerPosition.d.ts.map +1 -0
- package/dist/core/hooks/useCurrentViewerPosition.js +66 -0
- package/dist/core/hooks/useCurrentViewerPosition.js.map +1 -0
- package/dist/core/hooks/useShareUrl.d.ts +2 -0
- package/dist/core/hooks/useShareUrl.d.ts.map +1 -0
- package/dist/core/hooks/useShareUrl.js +74 -0
- package/dist/core/hooks/useShareUrl.js.map +1 -0
- package/dist/core/store/CombineProviders.d.ts.map +1 -1
- package/dist/core/store/CombineProviders.js +2 -0
- package/dist/core/store/CombineProviders.js.map +1 -1
- package/dist/core/store/MapSites/context.d.ts +16 -0
- package/dist/core/store/MapSites/context.d.ts.map +1 -0
- package/dist/core/store/MapSites/context.js +29 -0
- package/dist/core/store/MapSites/context.js.map +1 -0
- package/dist/core/store/MapSites/reducer.d.ts +52 -0
- package/dist/core/store/MapSites/reducer.d.ts.map +1 -0
- package/dist/core/store/MapSites/reducer.js +70 -0
- package/dist/core/store/MapSites/reducer.js.map +1 -0
- package/dist/core/store/index.d.ts +1 -0
- package/dist/core/store/index.d.ts.map +1 -1
- package/dist/core/store/index.js +1 -0
- package/dist/core/store/index.js.map +1 -1
- package/dist/core/styles/globals.css +46 -33
- package/package.json +1 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __spreadValues = (a, b) => {
|
|
8
|
+
for (var prop in b || (b = {}))
|
|
9
|
+
if (__hasOwnProp.call(b, prop))
|
|
10
|
+
__defNormalProp(a, prop, b[prop]);
|
|
11
|
+
if (__getOwnPropSymbols)
|
|
12
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
13
|
+
if (__propIsEnum.call(b, prop))
|
|
14
|
+
__defNormalProp(a, prop, b[prop]);
|
|
15
|
+
}
|
|
16
|
+
return a;
|
|
17
|
+
};
|
|
18
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
+
import * as React from "react";
|
|
20
|
+
import { Source, Layer, Marker } from "react-map-gl/maplibre";
|
|
21
|
+
import { toast } from "sonner";
|
|
22
|
+
import {
|
|
23
|
+
MapContext,
|
|
24
|
+
usePermissions,
|
|
25
|
+
useMenusContext,
|
|
26
|
+
useMapSitesContext
|
|
27
|
+
} from "../../../../../../../store";
|
|
28
|
+
import { MapLayerClickPriority } from "../../../../utils/MapEventManager/MapClickManager";
|
|
29
|
+
import { ViewerNames } from "../../../../../../../types/dbTypes";
|
|
30
|
+
import { useSite, useDeleteSite } from "../../../../../../../hooks/sites/sites";
|
|
31
|
+
import { useDeleteFile } from "../../../../../../../hooks/files/files";
|
|
32
|
+
import { polygonCentroid, uploadGeoJsonToAsset, pointToSegmentDistance } from "./siteGeometry";
|
|
33
|
+
import { SiteContextMenu } from "./SiteContextMenu";
|
|
34
|
+
const SITE_FILL_ID = "site-fill";
|
|
35
|
+
const SITE_OUTLINE_ID = "site-outline";
|
|
36
|
+
const SITE_LABEL_ID = "site-label";
|
|
37
|
+
const VERTEX_HIT_PX = 12;
|
|
38
|
+
const EDGE_HIT_PX = 8;
|
|
39
|
+
const MIN_RING_POINTS = 3;
|
|
40
|
+
const fillLayer = {
|
|
41
|
+
id: SITE_FILL_ID,
|
|
42
|
+
type: "fill",
|
|
43
|
+
paint: { "fill-color": "#3b82f6", "fill-opacity": 0.35 },
|
|
44
|
+
filter: ["==", ["get", "type"], "site"]
|
|
45
|
+
};
|
|
46
|
+
const outlineLayer = {
|
|
47
|
+
id: SITE_OUTLINE_ID,
|
|
48
|
+
type: "line",
|
|
49
|
+
layout: { "line-cap": "round", "line-join": "round" },
|
|
50
|
+
paint: { "line-color": "#1c4587", "line-width": 2 },
|
|
51
|
+
filter: ["==", ["get", "type"], "site"]
|
|
52
|
+
};
|
|
53
|
+
const labelLayer = {
|
|
54
|
+
id: SITE_LABEL_ID,
|
|
55
|
+
type: "symbol",
|
|
56
|
+
layout: {
|
|
57
|
+
"text-field": ["get", "name"],
|
|
58
|
+
"text-font": ["Inter"],
|
|
59
|
+
"text-size": 16,
|
|
60
|
+
"text-anchor": "center",
|
|
61
|
+
"text-allow-overlap": false
|
|
62
|
+
},
|
|
63
|
+
paint: {
|
|
64
|
+
"text-color": "#ffffff",
|
|
65
|
+
"text-halo-color": "#000000",
|
|
66
|
+
"text-halo-width": 1.5
|
|
67
|
+
},
|
|
68
|
+
filter: ["==", ["get", "type"], "site-label"]
|
|
69
|
+
};
|
|
70
|
+
const SiteLayer = () => {
|
|
71
|
+
var _a, _b, _c, _d;
|
|
72
|
+
const { state: mapState } = React.useContext(MapContext);
|
|
73
|
+
const { map, mapClickManager } = mapState.map;
|
|
74
|
+
const { state: mapSitesState, dispatch } = useMapSitesContext();
|
|
75
|
+
const { sites, editingSiteId } = mapSitesState.mapSites;
|
|
76
|
+
const { ability } = usePermissions();
|
|
77
|
+
const canUpdate = ability.can("update", "Site");
|
|
78
|
+
const canDelete = ability.can("delete", "Site");
|
|
79
|
+
const canRead = ability.can("read", "Site");
|
|
80
|
+
const { dispatch: menusDispatch, setSelectedSite, setView } = useMenusContext();
|
|
81
|
+
const [menu, setMenu] = React.useState(null);
|
|
82
|
+
const activeSiteId = (_b = (_a = menu == null ? void 0 : menu.siteId) != null ? _a : editingSiteId) != null ? _b : null;
|
|
83
|
+
const { updateSite } = useSite(activeSiteId ? String(activeSiteId) : "");
|
|
84
|
+
const { deleteSite } = useDeleteSite(activeSiteId != null ? activeSiteId : void 0);
|
|
85
|
+
const { deleteFile } = useDeleteFile(void 0, activeSiteId != null ? activeSiteId : void 0);
|
|
86
|
+
const menuSite = menu ? (_c = sites.find((s) => s.id === menu.siteId)) != null ? _c : null : null;
|
|
87
|
+
const sitesRef = React.useRef(sites);
|
|
88
|
+
sitesRef.current = sites;
|
|
89
|
+
const editingSiteIdRef = React.useRef(editingSiteId);
|
|
90
|
+
editingSiteIdRef.current = editingSiteId;
|
|
91
|
+
const featureCollection = React.useMemo(() => {
|
|
92
|
+
const features = [];
|
|
93
|
+
for (const s of sites) {
|
|
94
|
+
if (s.ring.length < 3) continue;
|
|
95
|
+
features.push({
|
|
96
|
+
type: "Feature",
|
|
97
|
+
geometry: { type: "Polygon", coordinates: [[...s.ring, s.ring[0]]] },
|
|
98
|
+
properties: { type: "site", siteId: s.id, name: s.name }
|
|
99
|
+
});
|
|
100
|
+
features.push({
|
|
101
|
+
type: "Feature",
|
|
102
|
+
geometry: { type: "Point", coordinates: polygonCentroid(s.ring) },
|
|
103
|
+
properties: { type: "site-label", siteId: s.id, name: s.name }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return { type: "FeatureCollection", features };
|
|
107
|
+
}, [sites]);
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
if (!map || !mapClickManager) return;
|
|
110
|
+
const handleSiteClick = (e, features) => {
|
|
111
|
+
var _a2, _b2, _c2;
|
|
112
|
+
if (editingSiteIdRef.current != null) return;
|
|
113
|
+
const feature = features == null ? void 0 : features[0];
|
|
114
|
+
const siteId = Number((_a2 = feature == null ? void 0 : feature.properties) == null ? void 0 : _a2.siteId);
|
|
115
|
+
if (!siteId || Number.isNaN(siteId)) return;
|
|
116
|
+
const native = e.originalEvent;
|
|
117
|
+
setMenu({
|
|
118
|
+
x: (_b2 = native == null ? void 0 : native.clientX) != null ? _b2 : e.point.x,
|
|
119
|
+
y: (_c2 = native == null ? void 0 : native.clientY) != null ? _c2 : e.point.y,
|
|
120
|
+
siteId
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const onEnter = () => {
|
|
124
|
+
map.getCanvas().style.cursor = "pointer";
|
|
125
|
+
};
|
|
126
|
+
const onLeave = () => {
|
|
127
|
+
map.getCanvas().style.cursor = "";
|
|
128
|
+
};
|
|
129
|
+
mapClickManager.register(SITE_FILL_ID, MapLayerClickPriority.SiteLayerClickPriority, handleSiteClick);
|
|
130
|
+
map.on("mouseenter", SITE_FILL_ID, onEnter);
|
|
131
|
+
map.on("mouseleave", SITE_FILL_ID, onLeave);
|
|
132
|
+
return () => {
|
|
133
|
+
mapClickManager.unregister(SITE_FILL_ID);
|
|
134
|
+
map.off("mouseenter", SITE_FILL_ID, onEnter);
|
|
135
|
+
map.off("mouseleave", SITE_FILL_ID, onLeave);
|
|
136
|
+
};
|
|
137
|
+
}, [map, mapClickManager]);
|
|
138
|
+
const updateVertex = (index, lng, lat) => {
|
|
139
|
+
if (editingSiteId == null) return;
|
|
140
|
+
const s = sites.find((x) => x.id === editingSiteId);
|
|
141
|
+
if (!s) return;
|
|
142
|
+
const ring = s.ring.map((p, i) => i === index ? [lng, lat] : p);
|
|
143
|
+
dispatch({ type: "UPDATE_SITE_RING", payload: { id: editingSiteId, ring } });
|
|
144
|
+
};
|
|
145
|
+
const deleteVertex = (index) => {
|
|
146
|
+
if (editingSiteId == null) return;
|
|
147
|
+
const s = sites.find((x) => x.id === editingSiteId);
|
|
148
|
+
if (!s) return;
|
|
149
|
+
if (s.ring.length <= MIN_RING_POINTS) {
|
|
150
|
+
toast.warning("A site needs at least 3 points.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const ring = s.ring.filter((_, i) => i !== index);
|
|
154
|
+
dispatch({ type: "UPDATE_SITE_RING", payload: { id: editingSiteId, ring } });
|
|
155
|
+
};
|
|
156
|
+
React.useEffect(() => {
|
|
157
|
+
if (!map || editingSiteId == null) return;
|
|
158
|
+
const onEditClick = (e) => {
|
|
159
|
+
const site = sitesRef.current.find((s) => s.id === editingSiteId);
|
|
160
|
+
const ring = site == null ? void 0 : site.ring;
|
|
161
|
+
if (!ring || ring.length < 2) return;
|
|
162
|
+
const click = e.point;
|
|
163
|
+
for (const v of ring) {
|
|
164
|
+
const vp = map.project(v);
|
|
165
|
+
if (Math.hypot(vp.x - click.x, vp.y - click.y) <= VERTEX_HIT_PX) return;
|
|
166
|
+
}
|
|
167
|
+
let bestDist = Infinity;
|
|
168
|
+
let bestIdx = -1;
|
|
169
|
+
for (let i = 0; i < ring.length; i++) {
|
|
170
|
+
const a = map.project(ring[i]);
|
|
171
|
+
const b = map.project(ring[(i + 1) % ring.length]);
|
|
172
|
+
const d = pointToSegmentDistance(click.x, click.y, a.x, a.y, b.x, b.y);
|
|
173
|
+
if (d < bestDist) {
|
|
174
|
+
bestDist = d;
|
|
175
|
+
bestIdx = i;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (bestIdx >= 0 && bestDist <= EDGE_HIT_PX) {
|
|
179
|
+
const next = [...ring];
|
|
180
|
+
next.splice(bestIdx + 1, 0, [e.lngLat.lng, e.lngLat.lat]);
|
|
181
|
+
dispatch({ type: "UPDATE_SITE_RING", payload: { id: editingSiteId, ring: next } });
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
map.doubleClickZoom.disable();
|
|
185
|
+
map.on("click", onEditClick);
|
|
186
|
+
return () => {
|
|
187
|
+
map.off("click", onEditClick);
|
|
188
|
+
map.doubleClickZoom.enable();
|
|
189
|
+
};
|
|
190
|
+
}, [map, editingSiteId, dispatch]);
|
|
191
|
+
const handleRename = async (name) => {
|
|
192
|
+
if (!menu) return;
|
|
193
|
+
dispatch({ type: "UPDATE_SITE_NAME", payload: { id: menu.siteId, name } });
|
|
194
|
+
try {
|
|
195
|
+
await updateSite({ siteName: name });
|
|
196
|
+
} catch (e) {
|
|
197
|
+
toast.error("Failed to rename the site.");
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const handleToggleEdit = async () => {
|
|
201
|
+
if (editingSiteId === activeSiteId && activeSiteId != null) {
|
|
202
|
+
const s = sites.find((x) => x.id === activeSiteId);
|
|
203
|
+
dispatch({ type: "SET_EDITING_SITE", payload: { id: null } });
|
|
204
|
+
if ((s == null ? void 0 : s.assetId) && s.ring.length >= 3) {
|
|
205
|
+
const toastId = toast.loading("Saving site shape\u2026");
|
|
206
|
+
try {
|
|
207
|
+
await uploadGeoJsonToAsset(s.assetId, s.ring, s.name);
|
|
208
|
+
const c = polygonCentroid(s.ring);
|
|
209
|
+
await updateSite({ siteLongitude: c[0], siteLatitude: c[1] });
|
|
210
|
+
toast.success("Site shape saved.", { id: toastId });
|
|
211
|
+
} catch (e) {
|
|
212
|
+
toast.error("Failed to save the site shape.", { id: toastId });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
dispatch({ type: "SET_EDITING_SITE", payload: { id: activeSiteId } });
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const handleHide = () => {
|
|
220
|
+
if (!menu) return;
|
|
221
|
+
dispatch({ type: "HIDE_SITE", payload: { id: menu.siteId } });
|
|
222
|
+
setMenu(null);
|
|
223
|
+
};
|
|
224
|
+
const handleInfo = () => {
|
|
225
|
+
if (!menu) return;
|
|
226
|
+
const s = sites.find((x) => x.id === menu.siteId);
|
|
227
|
+
setSelectedSite({ id: menu.siteId, siteName: s == null ? void 0 : s.name });
|
|
228
|
+
setView("detail");
|
|
229
|
+
menusDispatch({ type: "SET_VIEWER", payload: { currentViewer: ViewerNames.sites } });
|
|
230
|
+
setMenu(null);
|
|
231
|
+
};
|
|
232
|
+
const handleDelete = async () => {
|
|
233
|
+
if (!menu) return;
|
|
234
|
+
const id = menu.siteId;
|
|
235
|
+
const s = sites.find((x) => x.id === id);
|
|
236
|
+
try {
|
|
237
|
+
await deleteSite(id);
|
|
238
|
+
if (s == null ? void 0 : s.fileId) {
|
|
239
|
+
try {
|
|
240
|
+
await deleteFile(s.fileId);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
dispatch({ type: "HIDE_SITE", payload: { id } });
|
|
245
|
+
setMenu(null);
|
|
246
|
+
toast.success("Site deleted.");
|
|
247
|
+
} catch (e) {
|
|
248
|
+
toast.error("Failed to delete the site.");
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const editingSite = editingSiteId != null ? (_d = sites.find((s) => s.id === editingSiteId)) != null ? _d : null : null;
|
|
252
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
253
|
+
/* @__PURE__ */ jsxs(Source, { id: "sites-source", type: "geojson", data: featureCollection, children: [
|
|
254
|
+
/* @__PURE__ */ jsx(Layer, __spreadValues({}, fillLayer)),
|
|
255
|
+
/* @__PURE__ */ jsx(Layer, __spreadValues({}, outlineLayer)),
|
|
256
|
+
/* @__PURE__ */ jsx(Layer, __spreadValues({}, labelLayer))
|
|
257
|
+
] }),
|
|
258
|
+
editingSite == null ? void 0 : editingSite.ring.map((vertex, index) => /* @__PURE__ */ jsx(
|
|
259
|
+
Marker,
|
|
260
|
+
{
|
|
261
|
+
longitude: vertex[0],
|
|
262
|
+
latitude: vertex[1],
|
|
263
|
+
draggable: true,
|
|
264
|
+
onDrag: (e) => updateVertex(index, e.lngLat.lng, e.lngLat.lat),
|
|
265
|
+
children: /* @__PURE__ */ jsx(
|
|
266
|
+
"div",
|
|
267
|
+
{
|
|
268
|
+
title: "Drag to move \xB7 double-click to delete",
|
|
269
|
+
onDoubleClick: (ev) => {
|
|
270
|
+
ev.stopPropagation();
|
|
271
|
+
deleteVertex(index);
|
|
272
|
+
},
|
|
273
|
+
style: {
|
|
274
|
+
width: 14,
|
|
275
|
+
height: 14,
|
|
276
|
+
borderRadius: "50%",
|
|
277
|
+
background: "#22c55e",
|
|
278
|
+
border: "2px solid #ffffff",
|
|
279
|
+
boxShadow: "0 0 0 1px #15803d",
|
|
280
|
+
cursor: "grab"
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
},
|
|
285
|
+
`${editingSite.id}-vertex-${index}`
|
|
286
|
+
)),
|
|
287
|
+
menu && menuSite && /* @__PURE__ */ jsx(
|
|
288
|
+
SiteContextMenu,
|
|
289
|
+
{
|
|
290
|
+
x: menu.x,
|
|
291
|
+
y: menu.y,
|
|
292
|
+
site: menuSite,
|
|
293
|
+
isEditing: editingSiteId === menu.siteId,
|
|
294
|
+
canUpdate,
|
|
295
|
+
canDelete,
|
|
296
|
+
canRead,
|
|
297
|
+
onClose: () => setMenu(null),
|
|
298
|
+
onRename: handleRename,
|
|
299
|
+
onToggleEdit: handleToggleEdit,
|
|
300
|
+
onHide: handleHide,
|
|
301
|
+
onInfo: handleInfo,
|
|
302
|
+
onDelete: handleDelete
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
] });
|
|
306
|
+
};
|
|
307
|
+
export {
|
|
308
|
+
SiteLayer
|
|
309
|
+
};
|
|
310
|
+
//# sourceMappingURL=SiteLayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../src/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.tsx"],"sourcesContent":["'use client'\r\n\r\n// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport * as React from 'react'\r\nimport { Source, Layer, Marker } from 'react-map-gl/maplibre'\r\nimport type { LayerProps } from 'react-map-gl/maplibre'\r\nimport type { MapGeoJSONFeature } from 'maplibre-gl'\r\nimport { toast } from 'sonner'\r\n\r\nimport {\r\n MapContext,\r\n usePermissions,\r\n useMenusContext,\r\n useMapSitesContext,\r\n} from '../../../../../../../store'\r\nimport { MapLayerClickPriority, type ClickCallback } from '../../../../utils/MapEventManager/MapClickManager'\r\nimport { ViewerNames, type Site } from '../../../../../../../types/dbTypes'\r\nimport { useSite, useDeleteSite } from '../../../../../../../hooks/sites/sites'\r\nimport { useDeleteFile } from '../../../../../../../hooks/files/files'\r\nimport { polygonCentroid, uploadGeoJsonToAsset, pointToSegmentDistance } from './siteGeometry'\r\nimport { SiteContextMenu } from './SiteContextMenu'\r\n\r\nconst SITE_FILL_ID = 'site-fill'\r\nconst SITE_OUTLINE_ID = 'site-outline'\r\nconst SITE_LABEL_ID = 'site-label'\r\n\r\n// How close (screen px) a click must be to count as \"on a vertex\" / \"on an edge\".\r\nconst VERTEX_HIT_PX = 12\r\nconst EDGE_HIT_PX = 8\r\n// A polygon needs at least 3 corners; deleting below that is blocked.\r\nconst MIN_RING_POINTS = 3\r\n\r\nconst fillLayer: LayerProps = {\r\n id: SITE_FILL_ID,\r\n type: 'fill',\r\n paint: { 'fill-color': '#3b82f6', 'fill-opacity': 0.35 },\r\n filter: ['==', ['get', 'type'], 'site'],\r\n}\r\n\r\nconst outlineLayer: LayerProps = {\r\n id: SITE_OUTLINE_ID,\r\n type: 'line',\r\n layout: { 'line-cap': 'round', 'line-join': 'round' },\r\n paint: { 'line-color': '#1c4587', 'line-width': 2 },\r\n filter: ['==', ['get', 'type'], 'site'],\r\n}\r\n\r\nconst labelLayer: LayerProps = {\r\n id: SITE_LABEL_ID,\r\n type: 'symbol',\r\n layout: {\r\n 'text-field': ['get', 'name'],\r\n 'text-font': ['Inter'],\r\n 'text-size': 16,\r\n 'text-anchor': 'center',\r\n 'text-allow-overlap': false,\r\n },\r\n paint: {\r\n 'text-color': '#ffffff',\r\n 'text-halo-color': '#000000',\r\n 'text-halo-width': 1.5,\r\n },\r\n filter: ['==', ['get', 'type'], 'site-label'],\r\n}\r\n\r\nexport const SiteLayer = () => {\r\n const { state: mapState } = React.useContext(MapContext)\r\n const { map, mapClickManager } = mapState.map\r\n const { state: mapSitesState, dispatch } = useMapSitesContext()\r\n const { sites, editingSiteId } = mapSitesState.mapSites\r\n\r\n const { ability } = usePermissions()\r\n const canUpdate = ability.can('update', 'Site')\r\n const canDelete = ability.can('delete', 'Site')\r\n const canRead = ability.can('read', 'Site')\r\n\r\n const { dispatch: menusDispatch, setSelectedSite, setView } = useMenusContext()\r\n\r\n const [menu, setMenu] = React.useState<{ x: number, y: number, siteId: number } | null>(null)\r\n\r\n // The site the user is currently acting on (menu open) or editing.\r\n const activeSiteId = menu?.siteId ?? editingSiteId ?? null\r\n const { updateSite } = useSite(activeSiteId ? String(activeSiteId) : '')\r\n const { deleteSite } = useDeleteSite(activeSiteId ?? undefined)\r\n const { deleteFile } = useDeleteFile(undefined, activeSiteId ?? undefined)\r\n\r\n const menuSite = menu ? sites.find(s => s.id === menu.siteId) ?? null : null\r\n\r\n // Live mirrors so the once-bound map click handlers read current values.\r\n const sitesRef = React.useRef(sites)\r\n sitesRef.current = sites\r\n const editingSiteIdRef = React.useRef(editingSiteId)\r\n editingSiteIdRef.current = editingSiteId\r\n\r\n // Build the rendered FeatureCollection: a polygon + a single centroid label\r\n // per shown site.\r\n const featureCollection = React.useMemo<GeoJSON.FeatureCollection>(() => {\r\n const features: GeoJSON.Feature[] = []\r\n for (const s of sites) {\r\n if (s.ring.length < 3) continue\r\n features.push({\r\n type: 'Feature',\r\n geometry: { type: 'Polygon', coordinates: [[...s.ring, s.ring[0]]] },\r\n properties: { type: 'site', siteId: s.id, name: s.name },\r\n })\r\n features.push({\r\n type: 'Feature',\r\n geometry: { type: 'Point', coordinates: polygonCentroid(s.ring) },\r\n properties: { type: 'site-label', siteId: s.id, name: s.name },\r\n })\r\n }\r\n return { type: 'FeatureCollection', features }\r\n }, [sites])\r\n\r\n // Lowest-priority click handler: opens the site menu only when no other\r\n // interactive layer was hit at the click point.\r\n React.useEffect(() => {\r\n if (!map || !mapClickManager) return\r\n\r\n const handleSiteClick: ClickCallback = (e, features: MapGeoJSONFeature[]) => {\r\n // While editing a shape, fill clicks are for adding vertices (see the\r\n // edit-click effect), not for (re)opening the menu.\r\n if (editingSiteIdRef.current != null) return\r\n const feature = features?.[0]\r\n const siteId = Number(feature?.properties?.siteId)\r\n if (!siteId || Number.isNaN(siteId)) return\r\n const native = e.originalEvent as MouseEvent | undefined\r\n setMenu({\r\n x: native?.clientX ?? e.point.x,\r\n y: native?.clientY ?? e.point.y,\r\n siteId,\r\n })\r\n }\r\n\r\n const onEnter = () => { map.getCanvas().style.cursor = 'pointer' }\r\n const onLeave = () => { map.getCanvas().style.cursor = '' }\r\n\r\n mapClickManager.register(SITE_FILL_ID, MapLayerClickPriority.SiteLayerClickPriority, handleSiteClick)\r\n map.on('mouseenter', SITE_FILL_ID, onEnter)\r\n map.on('mouseleave', SITE_FILL_ID, onLeave)\r\n\r\n return () => {\r\n mapClickManager.unregister(SITE_FILL_ID)\r\n map.off('mouseenter', SITE_FILL_ID, onEnter)\r\n map.off('mouseleave', SITE_FILL_ID, onLeave)\r\n }\r\n }, [map, mapClickManager])\r\n\r\n const updateVertex = (index: number, lng: number, lat: number) => {\r\n if (editingSiteId == null) return\r\n const s = sites.find(x => x.id === editingSiteId)\r\n if (!s) return\r\n const ring = s.ring.map((p, i) => (i === index ? [lng, lat] as [number, number] : p))\r\n dispatch({ type: 'UPDATE_SITE_RING', payload: { id: editingSiteId, ring } })\r\n }\r\n\r\n // Double-click a vertex to delete it (a polygon keeps at least 3 corners).\r\n const deleteVertex = (index: number) => {\r\n if (editingSiteId == null) return\r\n const s = sites.find(x => x.id === editingSiteId)\r\n if (!s) return\r\n if (s.ring.length <= MIN_RING_POINTS) {\r\n toast.warning('A site needs at least 3 points.')\r\n return\r\n }\r\n const ring = s.ring.filter((_, i) => i !== index)\r\n dispatch({ type: 'UPDATE_SITE_RING', payload: { id: editingSiteId, ring } })\r\n }\r\n\r\n // While editing, a click on an edge (not a vertex) inserts a new vertex there.\r\n React.useEffect(() => {\r\n if (!map || editingSiteId == null) return\r\n\r\n const onEditClick = (e: maplibregl.MapMouseEvent) => {\r\n const site = sitesRef.current.find(s => s.id === editingSiteId)\r\n const ring = site?.ring\r\n if (!ring || ring.length < 2) return\r\n\r\n const click = e.point\r\n // Ignore clicks on/near an existing vertex — those drag (or, on a\r\n // double-click, delete) via the markers.\r\n for (const v of ring) {\r\n const vp = map.project(v as [number, number])\r\n if (Math.hypot(vp.x - click.x, vp.y - click.y) <= VERTEX_HIT_PX) return\r\n }\r\n\r\n // Find the nearest edge segment in screen space.\r\n let bestDist = Infinity\r\n let bestIdx = -1\r\n for (let i = 0; i < ring.length; i++) {\r\n const a = map.project(ring[i] as [number, number])\r\n const b = map.project(ring[(i + 1) % ring.length] as [number, number])\r\n const d = pointToSegmentDistance(click.x, click.y, a.x, a.y, b.x, b.y)\r\n if (d < bestDist) { bestDist = d; bestIdx = i }\r\n }\r\n\r\n if (bestIdx >= 0 && bestDist <= EDGE_HIT_PX) {\r\n const next = [...ring]\r\n next.splice(bestIdx + 1, 0, [e.lngLat.lng, e.lngLat.lat])\r\n dispatch({ type: 'UPDATE_SITE_RING', payload: { id: editingSiteId, ring: next } })\r\n }\r\n }\r\n\r\n // Avoid the map zooming when a vertex is double-clicked to delete it.\r\n map.doubleClickZoom.disable()\r\n map.on('click', onEditClick)\r\n return () => {\r\n map.off('click', onEditClick)\r\n map.doubleClickZoom.enable()\r\n }\r\n }, [map, editingSiteId, dispatch])\r\n\r\n const handleRename = async (name: string) => {\r\n if (!menu) return\r\n dispatch({ type: 'UPDATE_SITE_NAME', payload: { id: menu.siteId, name } })\r\n try {\r\n await updateSite({ siteName: name })\r\n }\r\n catch {\r\n toast.error('Failed to rename the site.')\r\n }\r\n }\r\n\r\n const handleToggleEdit = async () => {\r\n if (editingSiteId === activeSiteId && activeSiteId != null) {\r\n // Finishing an edit: persist the new geometry + centroid.\r\n const s = sites.find(x => x.id === activeSiteId)\r\n dispatch({ type: 'SET_EDITING_SITE', payload: { id: null } })\r\n if (s?.assetId && s.ring.length >= 3) {\r\n const toastId = toast.loading('Saving site shape…')\r\n try {\r\n await uploadGeoJsonToAsset(s.assetId, s.ring, s.name)\r\n const c = polygonCentroid(s.ring)\r\n await updateSite({ siteLongitude: c[0], siteLatitude: c[1] })\r\n toast.success('Site shape saved.', { id: toastId })\r\n }\r\n catch {\r\n toast.error('Failed to save the site shape.', { id: toastId })\r\n }\r\n }\r\n }\r\n else {\r\n // Keep the menu open so the name can be edited alongside the vertices.\r\n dispatch({ type: 'SET_EDITING_SITE', payload: { id: activeSiteId } })\r\n }\r\n }\r\n\r\n const handleHide = () => {\r\n if (!menu) return\r\n dispatch({ type: 'HIDE_SITE', payload: { id: menu.siteId } })\r\n setMenu(null)\r\n }\r\n\r\n const handleInfo = () => {\r\n if (!menu) return\r\n const s = sites.find(x => x.id === menu.siteId)\r\n setSelectedSite({ id: menu.siteId, siteName: s?.name } as Site)\r\n setView('detail')\r\n menusDispatch({ type: 'SET_VIEWER', payload: { currentViewer: ViewerNames.sites } })\r\n setMenu(null)\r\n }\r\n\r\n const handleDelete = async () => {\r\n if (!menu) return\r\n const id = menu.siteId\r\n const s = sites.find(x => x.id === id)\r\n try {\r\n await deleteSite(id)\r\n if (s?.fileId) {\r\n try { await deleteFile(s.fileId) }\r\n catch { /* geometry file orphaned; site itself is gone */ }\r\n }\r\n dispatch({ type: 'HIDE_SITE', payload: { id } })\r\n setMenu(null)\r\n toast.success('Site deleted.')\r\n }\r\n catch {\r\n toast.error('Failed to delete the site.')\r\n }\r\n }\r\n\r\n const editingSite = editingSiteId != null ? sites.find(s => s.id === editingSiteId) ?? null : null\r\n\r\n return (\r\n <>\r\n <Source id=\"sites-source\" type=\"geojson\" data={featureCollection}>\r\n <Layer {...fillLayer} />\r\n <Layer {...outlineLayer} />\r\n <Layer {...labelLayer} />\r\n </Source>\r\n\r\n {/* Draggable vertex handles while editing a site's shape */}\r\n {editingSite?.ring.map((vertex, index) => (\r\n <Marker\r\n key={`${editingSite.id}-vertex-${index}`}\r\n longitude={vertex[0]}\r\n latitude={vertex[1]}\r\n draggable\r\n onDrag={(e: { lngLat: { lng: number, lat: number } }) =>\r\n updateVertex(index, e.lngLat.lng, e.lngLat.lat)}\r\n >\r\n <div\r\n title=\"Drag to move · double-click to delete\"\r\n onDoubleClick={(ev) => { ev.stopPropagation(); deleteVertex(index) }}\r\n style={{\r\n width: 14,\r\n height: 14,\r\n borderRadius: '50%',\r\n background: '#22c55e',\r\n border: '2px solid #ffffff',\r\n boxShadow: '0 0 0 1px #15803d',\r\n cursor: 'grab',\r\n }}\r\n />\r\n </Marker>\r\n ))}\r\n\r\n {menu && menuSite && (\r\n <SiteContextMenu\r\n x={menu.x}\r\n y={menu.y}\r\n site={menuSite}\r\n isEditing={editingSiteId === menu.siteId}\r\n canUpdate={canUpdate}\r\n canDelete={canDelete}\r\n canRead={canRead}\r\n onClose={() => setMenu(null)}\r\n onRename={handleRename}\r\n onToggleEdit={handleToggleEdit}\r\n onHide={handleHide}\r\n onInfo={handleInfo}\r\n onDelete={handleDelete}\r\n />\r\n )}\r\n </>\r\n )\r\n}\r\n"],"mappings":"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","names":["_a","_b","_c"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type Ring = [number, number][];
|
|
2
|
+
/** Area-weighted centroid of a polygon ring; used to place a single label. */
|
|
3
|
+
export declare const polygonCentroid: (pts: Ring) => [number, number];
|
|
4
|
+
/** Bounding box of a ring as [[minLng, minLat], [maxLng, maxLat]]. */
|
|
5
|
+
export declare const ringBounds: (ring: Ring) => [[number, number], [number, number]];
|
|
6
|
+
/** Shortest distance from point (px,py) to the segment (ax,ay)-(bx,by). */
|
|
7
|
+
export declare const pointToSegmentDistance: (px: number, py: number, ax: number, ay: number, bx: number, by: number) => number;
|
|
8
|
+
/** Build the GeoJSON FeatureCollection stored in minio for a site polygon. */
|
|
9
|
+
export declare const ringToFeatureCollection: (ring: Ring, name: string) => GeoJSON.FeatureCollection;
|
|
10
|
+
/**
|
|
11
|
+
* Return the items whose [lng, lat] point falls inside the polygon ring.
|
|
12
|
+
* The Turf polygon is built once, so this is cheap for many points.
|
|
13
|
+
*/
|
|
14
|
+
export declare const filterPointsInRing: <T>(ring: Ring, items: T[], getLngLat: (item: T) => [number, number] | null) => T[];
|
|
15
|
+
/**
|
|
16
|
+
* Extract the outer ring (open, no repeated closing point) from a stored
|
|
17
|
+
* GeoJSON value, accepting a FeatureCollection, Feature, or bare geometry.
|
|
18
|
+
*/
|
|
19
|
+
export declare const ringFromGeoJson: (geo: any) => Ring | null;
|
|
20
|
+
/**
|
|
21
|
+
* Load a site's saved boundary ring from minio (via its attached geometry
|
|
22
|
+
* file). Returns null if the site has no saved boundary or it can't be read.
|
|
23
|
+
*/
|
|
24
|
+
export declare const fetchSiteBoundaryRing: (siteId: number) => Promise<Ring | null>;
|
|
25
|
+
/**
|
|
26
|
+
* (Over)write a site's GeoJSON object in minio under the given asset key.
|
|
27
|
+
* Mirrors the presigned-upload flow used by FileAdder/DatasetAdder.
|
|
28
|
+
*/
|
|
29
|
+
export declare const uploadGeoJsonToAsset: (assetId: string, ring: Ring, name: string) => Promise<void>;
|
|
30
|
+
//# sourceMappingURL=siteGeometry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"siteGeometry.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAA;AAErC,8EAA8E;AAC9E,eAAO,MAAM,eAAe,GAAI,KAAK,IAAI,KAAG,CAAC,MAAM,EAAE,MAAM,CAgB1D,CAAA;AAED,sEAAsE;AACtE,eAAO,MAAM,UAAU,GAAI,MAAM,IAAI,KAAG,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAS1E,CAAA;AAED,2EAA2E;AAC3E,eAAO,MAAM,sBAAsB,GACjC,IAAI,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,MAAM,KACrE,MAOF,CAAA;AAED,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB,GAAI,MAAM,IAAI,EAAE,MAAM,MAAM,KAAG,OAAO,CAAC,iBASzE,CAAA;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAClC,MAAM,IAAI,EACV,OAAO,CAAC,EAAE,EACV,WAAW,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,KAC9C,CAAC,EAYH,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,GAAG,KAAG,IAAI,GAAG,IA0BjD,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAe/E,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAC/B,SAAS,MAAM,EACf,MAAM,IAAI,EACV,MAAM,MAAM,KACX,OAAO,CAAC,IAAI,CASd,CAAA"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { booleanPointInPolygon, polygon as turfPolygon } from "@turf/turf";
|
|
2
|
+
import { uploadToPresignedUrl } from "../../../tools/AddTools/AddFile/utils/uploadToPresignedURLS";
|
|
3
|
+
const polygonCentroid = (pts) => {
|
|
4
|
+
let x = 0, y = 0, area = 0;
|
|
5
|
+
for (let i = 0; i < pts.length; i++) {
|
|
6
|
+
const [x0, y0] = pts[i];
|
|
7
|
+
const [x1, y1] = pts[(i + 1) % pts.length];
|
|
8
|
+
const cross = x0 * y1 - x1 * y0;
|
|
9
|
+
area += cross;
|
|
10
|
+
x += (x0 + x1) * cross;
|
|
11
|
+
y += (y0 + y1) * cross;
|
|
12
|
+
}
|
|
13
|
+
area *= 0.5;
|
|
14
|
+
if (area === 0) {
|
|
15
|
+
const n = pts.length || 1;
|
|
16
|
+
return [pts.reduce((s, p) => s + p[0], 0) / n, pts.reduce((s, p) => s + p[1], 0) / n];
|
|
17
|
+
}
|
|
18
|
+
return [x / (6 * area), y / (6 * area)];
|
|
19
|
+
};
|
|
20
|
+
const ringBounds = (ring) => {
|
|
21
|
+
let minLng = Infinity, minLat = Infinity, maxLng = -Infinity, maxLat = -Infinity;
|
|
22
|
+
for (const [lng, lat] of ring) {
|
|
23
|
+
if (lng < minLng) minLng = lng;
|
|
24
|
+
if (lat < minLat) minLat = lat;
|
|
25
|
+
if (lng > maxLng) maxLng = lng;
|
|
26
|
+
if (lat > maxLat) maxLat = lat;
|
|
27
|
+
}
|
|
28
|
+
return [[minLng, minLat], [maxLng, maxLat]];
|
|
29
|
+
};
|
|
30
|
+
const pointToSegmentDistance = (px, py, ax, ay, bx, by) => {
|
|
31
|
+
const dx = bx - ax;
|
|
32
|
+
const dy = by - ay;
|
|
33
|
+
const lenSq = dx * dx + dy * dy;
|
|
34
|
+
let t = lenSq === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lenSq;
|
|
35
|
+
t = Math.max(0, Math.min(1, t));
|
|
36
|
+
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
37
|
+
};
|
|
38
|
+
const ringToFeatureCollection = (ring, name) => ({
|
|
39
|
+
type: "FeatureCollection",
|
|
40
|
+
features: [
|
|
41
|
+
{
|
|
42
|
+
type: "Feature",
|
|
43
|
+
geometry: { type: "Polygon", coordinates: [[...ring, ring[0]]] },
|
|
44
|
+
properties: { type: "site", name }
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
});
|
|
48
|
+
const filterPointsInRing = (ring, items, getLngLat) => {
|
|
49
|
+
if (ring.length < 3) return [];
|
|
50
|
+
try {
|
|
51
|
+
const poly = turfPolygon([[...ring, ring[0]]]);
|
|
52
|
+
return (items != null ? items : []).filter((item) => {
|
|
53
|
+
const c = getLngLat(item);
|
|
54
|
+
return c != null && booleanPointInPolygon(c, poly);
|
|
55
|
+
});
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const ringFromGeoJson = (geo) => {
|
|
61
|
+
var _a, _b, _c;
|
|
62
|
+
const geometry = (() => {
|
|
63
|
+
var _a2, _b2, _c2;
|
|
64
|
+
if (!geo) return null;
|
|
65
|
+
if (geo.type === "FeatureCollection") {
|
|
66
|
+
const f = ((_a2 = geo.features) != null ? _a2 : []).find(
|
|
67
|
+
(feat) => {
|
|
68
|
+
var _a3, _b3;
|
|
69
|
+
return ((_a3 = feat == null ? void 0 : feat.geometry) == null ? void 0 : _a3.type) === "Polygon" || ((_b3 = feat == null ? void 0 : feat.geometry) == null ? void 0 : _b3.type) === "MultiPolygon";
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
return (_b2 = f == null ? void 0 : f.geometry) != null ? _b2 : null;
|
|
73
|
+
}
|
|
74
|
+
if (geo.type === "Feature") return (_c2 = geo.geometry) != null ? _c2 : null;
|
|
75
|
+
return geo;
|
|
76
|
+
})();
|
|
77
|
+
if (!geometry) return null;
|
|
78
|
+
let coords;
|
|
79
|
+
if (geometry.type === "Polygon") coords = (_a = geometry.coordinates) == null ? void 0 : _a[0];
|
|
80
|
+
else if (geometry.type === "MultiPolygon") coords = (_c = (_b = geometry.coordinates) == null ? void 0 : _b[0]) == null ? void 0 : _c[0];
|
|
81
|
+
if (!Array.isArray(coords) || coords.length < 3) return null;
|
|
82
|
+
const ring = coords.map((c) => [c[0], c[1]]);
|
|
83
|
+
const first = ring[0];
|
|
84
|
+
const last = ring[ring.length - 1];
|
|
85
|
+
if (ring.length > 1 && first[0] === last[0] && first[1] === last[1]) ring.pop();
|
|
86
|
+
return ring;
|
|
87
|
+
};
|
|
88
|
+
const fetchSiteBoundaryRing = async (siteId) => {
|
|
89
|
+
var _a;
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`/api/files/site/${siteId}`);
|
|
92
|
+
if (!res.ok) return null;
|
|
93
|
+
const { files } = await res.json();
|
|
94
|
+
const geomFile = (_a = (files != null ? files : []).find((f) => f.tag === "site-geometry" && f.url)) != null ? _a : (files != null ? files : []).find((f) => (f.extension === "geojson" || f.mimeType && String(f.mimeType).includes("geo+json")) && f.url);
|
|
95
|
+
if (!(geomFile == null ? void 0 : geomFile.url)) return null;
|
|
96
|
+
const geo = await fetch(geomFile.url).then((r) => r.json());
|
|
97
|
+
return ringFromGeoJson(geo);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const uploadGeoJsonToAsset = async (assetId, ring, name) => {
|
|
103
|
+
const fc = ringToFeatureCollection(ring, name);
|
|
104
|
+
const file = new File([JSON.stringify(fc)], `${name || "site"}.geojson`, {
|
|
105
|
+
type: "application/geo+json"
|
|
106
|
+
});
|
|
107
|
+
const presRes = await fetch(`/api/presigned-url-upload?asset=${encodeURIComponent(assetId)}`);
|
|
108
|
+
if (!presRes.ok) throw new Error("Failed to get an upload URL");
|
|
109
|
+
const { presignedUrl } = await presRes.json();
|
|
110
|
+
await uploadToPresignedUrl(presignedUrl, file);
|
|
111
|
+
};
|
|
112
|
+
export {
|
|
113
|
+
fetchSiteBoundaryRing,
|
|
114
|
+
filterPointsInRing,
|
|
115
|
+
pointToSegmentDistance,
|
|
116
|
+
polygonCentroid,
|
|
117
|
+
ringBounds,
|
|
118
|
+
ringFromGeoJson,
|
|
119
|
+
ringToFeatureCollection,
|
|
120
|
+
uploadGeoJsonToAsset
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=siteGeometry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../src/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport { booleanPointInPolygon, polygon as turfPolygon } from '@turf/turf'\r\nimport { uploadToPresignedUrl } from '../../../tools/AddTools/AddFile/utils/uploadToPresignedURLS'\r\n\r\nexport type Ring = [number, number][]\r\n\r\n/** Area-weighted centroid of a polygon ring; used to place a single label. */\r\nexport const polygonCentroid = (pts: Ring): [number, number] => {\r\n let x = 0, y = 0, area = 0\r\n for (let i = 0; i < pts.length; i++) {\r\n const [x0, y0] = pts[i]\r\n const [x1, y1] = pts[(i + 1) % pts.length]\r\n const cross = x0 * y1 - x1 * y0\r\n area += cross\r\n x += (x0 + x1) * cross\r\n y += (y0 + y1) * cross\r\n }\r\n area *= 0.5\r\n if (area === 0) {\r\n const n = pts.length || 1\r\n return [pts.reduce((s, p) => s + p[0], 0) / n, pts.reduce((s, p) => s + p[1], 0) / n]\r\n }\r\n return [x / (6 * area), y / (6 * area)]\r\n}\r\n\r\n/** Bounding box of a ring as [[minLng, minLat], [maxLng, maxLat]]. */\r\nexport const ringBounds = (ring: Ring): [[number, number], [number, number]] => {\r\n let minLng = Infinity, minLat = Infinity, maxLng = -Infinity, maxLat = -Infinity\r\n for (const [lng, lat] of ring) {\r\n if (lng < minLng) minLng = lng\r\n if (lat < minLat) minLat = lat\r\n if (lng > maxLng) maxLng = lng\r\n if (lat > maxLat) maxLat = lat\r\n }\r\n return [[minLng, minLat], [maxLng, maxLat]]\r\n}\r\n\r\n/** Shortest distance from point (px,py) to the segment (ax,ay)-(bx,by). */\r\nexport const pointToSegmentDistance = (\r\n px: number, py: number, ax: number, ay: number, bx: number, by: number,\r\n): number => {\r\n const dx = bx - ax\r\n const dy = by - ay\r\n const lenSq = dx * dx + dy * dy\r\n let t = lenSq === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lenSq\r\n t = Math.max(0, Math.min(1, t))\r\n return Math.hypot(px - (ax + t * dx), py - (ay + t * dy))\r\n}\r\n\r\n/** Build the GeoJSON FeatureCollection stored in minio for a site polygon. */\r\nexport const ringToFeatureCollection = (ring: Ring, name: string): GeoJSON.FeatureCollection => ({\r\n type: 'FeatureCollection',\r\n features: [\r\n {\r\n type: 'Feature',\r\n geometry: { type: 'Polygon', coordinates: [[...ring, ring[0]]] },\r\n properties: { type: 'site', name },\r\n },\r\n ],\r\n})\r\n\r\n/**\r\n * Return the items whose [lng, lat] point falls inside the polygon ring.\r\n * The Turf polygon is built once, so this is cheap for many points.\r\n */\r\nexport const filterPointsInRing = <T>(\r\n ring: Ring,\r\n items: T[],\r\n getLngLat: (item: T) => [number, number] | null,\r\n): T[] => {\r\n if (ring.length < 3) return []\r\n try {\r\n const poly = turfPolygon([[...ring, ring[0]]])\r\n return (items ?? []).filter((item) => {\r\n const c = getLngLat(item)\r\n return c != null && booleanPointInPolygon(c, poly)\r\n })\r\n }\r\n catch {\r\n return []\r\n }\r\n}\r\n\r\n/**\r\n * Extract the outer ring (open, no repeated closing point) from a stored\r\n * GeoJSON value, accepting a FeatureCollection, Feature, or bare geometry.\r\n */\r\nexport const ringFromGeoJson = (geo: any): Ring | null => {\r\n const geometry = (() => {\r\n if (!geo) return null\r\n if (geo.type === 'FeatureCollection') {\r\n const f = (geo.features ?? []).find(\r\n (feat: any) => feat?.geometry?.type === 'Polygon' || feat?.geometry?.type === 'MultiPolygon',\r\n )\r\n return f?.geometry ?? null\r\n }\r\n if (geo.type === 'Feature') return geo.geometry ?? null\r\n return geo\r\n })()\r\n\r\n if (!geometry) return null\r\n\r\n let coords: any\r\n if (geometry.type === 'Polygon') coords = geometry.coordinates?.[0]\r\n else if (geometry.type === 'MultiPolygon') coords = geometry.coordinates?.[0]?.[0]\r\n if (!Array.isArray(coords) || coords.length < 3) return null\r\n\r\n const ring: Ring = coords.map((c: number[]) => [c[0], c[1]] as [number, number])\r\n // Drop the closing point if it duplicates the first.\r\n const first = ring[0]\r\n const last = ring[ring.length - 1]\r\n if (ring.length > 1 && first[0] === last[0] && first[1] === last[1]) ring.pop()\r\n return ring\r\n}\r\n\r\n/**\r\n * Load a site's saved boundary ring from minio (via its attached geometry\r\n * file). Returns null if the site has no saved boundary or it can't be read.\r\n */\r\nexport const fetchSiteBoundaryRing = async (siteId: number): Promise<Ring | null> => {\r\n try {\r\n const res = await fetch(`/api/files/site/${siteId}`)\r\n if (!res.ok) return null\r\n const { files } = await res.json()\r\n const geomFile = (files ?? []).find((f: any) => f.tag === 'site-geometry' && f.url)\r\n ?? (files ?? []).find((f: any) =>\r\n (f.extension === 'geojson' || (f.mimeType && String(f.mimeType).includes('geo+json'))) && f.url)\r\n if (!geomFile?.url) return null\r\n const geo = await fetch(geomFile.url).then(r => r.json())\r\n return ringFromGeoJson(geo)\r\n }\r\n catch {\r\n return null\r\n }\r\n}\r\n\r\n/**\r\n * (Over)write a site's GeoJSON object in minio under the given asset key.\r\n * Mirrors the presigned-upload flow used by FileAdder/DatasetAdder.\r\n */\r\nexport const uploadGeoJsonToAsset = async (\r\n assetId: string,\r\n ring: Ring,\r\n name: string,\r\n): Promise<void> => {\r\n const fc = ringToFeatureCollection(ring, name)\r\n const file = new File([JSON.stringify(fc)], `${name || 'site'}.geojson`, {\r\n type: 'application/geo+json',\r\n })\r\n const presRes = await fetch(`/api/presigned-url-upload?asset=${encodeURIComponent(assetId)}`)\r\n if (!presRes.ok) throw new Error('Failed to get an upload URL')\r\n const { presignedUrl } = await presRes.json()\r\n await uploadToPresignedUrl(presignedUrl, file)\r\n}\r\n"],"mappings":"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","names":["_a","_b","_c"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Site } from '../../../../../../../types/dbTypes';
|
|
2
|
+
/**
|
|
3
|
+
* Shared "show site(s) on the map" action used by the sites table row menu and
|
|
4
|
+
* the "View All on Map" header button. It switches to the map viewer FIRST (so
|
|
5
|
+
* the action always navigates, even if a boundary fails to load), then loads
|
|
6
|
+
* each site's polygon GeoJSON from minio, hands it to the SiteLayer via the
|
|
7
|
+
* MapSites store, and fits the camera to the loaded boundaries. Sites without a
|
|
8
|
+
* saved boundary fall back to flying to their point.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useShowSitesOnMap(): (sites: Site[], opts?: {
|
|
11
|
+
switchViewer?: boolean;
|
|
12
|
+
}) => Promise<void>;
|
|
13
|
+
//# sourceMappingURL=useShowSitesOnMap.d.ts.map
|
package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useShowSitesOnMap.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.ts"],"names":[],"mappings":"AAOA,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,oCAAoC,CAAA;AAG3E;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,YAMQ,IAAI,EAAE,SAAS;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,mBAmDjF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { MapContext, useMenusContext, useMapSitesContext } from "../../../../../../../store";
|
|
4
|
+
import { ViewerNames } from "../../../../../../../types/dbTypes";
|
|
5
|
+
import { ringFromGeoJson, ringBounds } from "./siteGeometry";
|
|
6
|
+
function useShowSitesOnMap() {
|
|
7
|
+
const { dispatch: mapSitesDispatch } = useMapSitesContext();
|
|
8
|
+
const { dispatch: menusDispatch } = useMenusContext();
|
|
9
|
+
const { state: mapState } = React.useContext(MapContext);
|
|
10
|
+
const { map } = mapState.map;
|
|
11
|
+
return React.useCallback(async (sites, opts) => {
|
|
12
|
+
var _a;
|
|
13
|
+
if ((_a = opts == null ? void 0 : opts.switchViewer) != null ? _a : true) {
|
|
14
|
+
menusDispatch({ type: "SET_VIEWER", payload: { currentViewer: ViewerNames.map } });
|
|
15
|
+
}
|
|
16
|
+
const valid = sites.filter((s) => s == null ? void 0 : s.id);
|
|
17
|
+
const rings = await Promise.all(valid.map(async (site) => {
|
|
18
|
+
var _a2, _b;
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(`/api/files/site/${site.id}`);
|
|
21
|
+
if (!res.ok) return null;
|
|
22
|
+
const { files } = await res.json();
|
|
23
|
+
const geomFile = (_a2 = (files != null ? files : []).find((f) => f.tag === "site-geometry" && f.url)) != null ? _a2 : (files != null ? files : []).find((f) => (f.extension === "geojson" || f.mimeType && String(f.mimeType).includes("geo+json")) && f.url);
|
|
24
|
+
if (!(geomFile == null ? void 0 : geomFile.url)) return null;
|
|
25
|
+
const geo = await fetch(geomFile.url).then((r) => r.json());
|
|
26
|
+
const ring = ringFromGeoJson(geo);
|
|
27
|
+
if (!ring) return null;
|
|
28
|
+
mapSitesDispatch({
|
|
29
|
+
type: "SHOW_SITE",
|
|
30
|
+
payload: {
|
|
31
|
+
site: {
|
|
32
|
+
id: site.id,
|
|
33
|
+
name: (_b = site.siteName) != null ? _b : "Site",
|
|
34
|
+
ring,
|
|
35
|
+
assetId: geomFile.assetId,
|
|
36
|
+
fileId: geomFile.id
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return ring;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
if (!map) return;
|
|
46
|
+
const foundRings = rings.filter((r) => !!r);
|
|
47
|
+
if (foundRings.length > 0) {
|
|
48
|
+
map.fitBounds(ringBounds(foundRings.flat()), { padding: 60, duration: 1e3 });
|
|
49
|
+
} else {
|
|
50
|
+
const pt = valid.find((s) => typeof s.siteLongitude === "number" && typeof s.siteLatitude === "number");
|
|
51
|
+
if (pt) map.flyTo({ center: [pt.siteLongitude, pt.siteLatitude], zoom: 18, duration: 1e3 });
|
|
52
|
+
}
|
|
53
|
+
}, [map, mapSitesDispatch, menusDispatch]);
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
useShowSitesOnMap
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=useShowSitesOnMap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../src/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.ts"],"sourcesContent":["'use client'\r\n\r\n// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport * as React from 'react'\r\nimport { MapContext, useMenusContext, useMapSitesContext } from '../../../../../../../store'\r\nimport { ViewerNames, type Site } from '../../../../../../../types/dbTypes'\r\nimport { ringFromGeoJson, ringBounds, type Ring } from './siteGeometry'\r\n\r\n/**\r\n * Shared \"show site(s) on the map\" action used by the sites table row menu and\r\n * the \"View All on Map\" header button. It switches to the map viewer FIRST (so\r\n * the action always navigates, even if a boundary fails to load), then loads\r\n * each site's polygon GeoJSON from minio, hands it to the SiteLayer via the\r\n * MapSites store, and fits the camera to the loaded boundaries. Sites without a\r\n * saved boundary fall back to flying to their point.\r\n */\r\nexport function useShowSitesOnMap() {\r\n const { dispatch: mapSitesDispatch } = useMapSitesContext()\r\n const { dispatch: menusDispatch } = useMenusContext()\r\n const { state: mapState } = React.useContext(MapContext)\r\n const { map } = mapState.map\r\n\r\n return React.useCallback(async (sites: Site[], opts?: { switchViewer?: boolean }) => {\r\n if (opts?.switchViewer ?? true) {\r\n menusDispatch({ type: 'SET_VIEWER', payload: { currentViewer: ViewerNames.map } })\r\n }\r\n\r\n const valid = sites.filter(s => s?.id)\r\n const rings = await Promise.all(valid.map(async (site): Promise<Ring | null> => {\r\n try {\r\n const res = await fetch(`/api/files/site/${site.id}`)\r\n if (!res.ok) return null\r\n const { files } = await res.json()\r\n // Prefer the file tagged as the site boundary; fall back to any geojson\r\n // attachment so older sites (saved before tagging) still resolve.\r\n const geomFile = (files ?? []).find((f: any) => f.tag === 'site-geometry' && f.url)\r\n ?? (files ?? []).find((f: any) =>\r\n (f.extension === 'geojson' || (f.mimeType && String(f.mimeType).includes('geo+json'))) && f.url)\r\n if (!geomFile?.url) return null\r\n const geo = await fetch(geomFile.url).then(r => r.json())\r\n const ring = ringFromGeoJson(geo)\r\n if (!ring) return null\r\n mapSitesDispatch({\r\n type: 'SHOW_SITE',\r\n payload: {\r\n site: {\r\n id: site.id,\r\n name: site.siteName ?? 'Site',\r\n ring,\r\n assetId: geomFile.assetId,\r\n fileId: geomFile.id,\r\n },\r\n },\r\n })\r\n return ring\r\n }\r\n catch {\r\n return null\r\n }\r\n }))\r\n\r\n if (!map) return\r\n const foundRings = rings.filter((r): r is Ring => !!r)\r\n if (foundRings.length > 0) {\r\n // Fit to the combined extent of every loaded boundary.\r\n map.fitBounds(ringBounds(foundRings.flat() as Ring), { padding: 60, duration: 1000 })\r\n }\r\n else {\r\n // No boundaries found — fly to the first site that has a point.\r\n const pt = valid.find(s => typeof s.siteLongitude === 'number' && typeof s.siteLatitude === 'number')\r\n if (pt) map.flyTo({ center: [pt.siteLongitude as number, pt.siteLatitude as number], zoom: 18, duration: 1000 })\r\n }\r\n }, [map, mapSitesDispatch, menusDispatch])\r\n}\r\n"],"mappings":";AAKA,YAAY,WAAW;AACvB,SAAS,YAAY,iBAAiB,0BAA0B;AAChE,SAAS,mBAA8B;AACvC,SAAS,iBAAiB,kBAA6B;AAUhD,SAAS,oBAAoB;AAClC,QAAM,EAAE,UAAU,iBAAiB,IAAI,mBAAmB;AAC1D,QAAM,EAAE,UAAU,cAAc,IAAI,gBAAgB;AACpD,QAAM,EAAE,OAAO,SAAS,IAAI,MAAM,WAAW,UAAU;AACvD,QAAM,EAAE,IAAI,IAAI,SAAS;AAEzB,SAAO,MAAM,YAAY,OAAO,OAAe,SAAsC;AAxBvF;AAyBI,SAAI,kCAAM,iBAAN,YAAsB,MAAM;AAC9B,oBAAc,EAAE,MAAM,cAAc,SAAS,EAAE,eAAe,YAAY,IAAI,EAAE,CAAC;AAAA,IACnF;AAEA,UAAM,QAAQ,MAAM,OAAO,OAAK,uBAAG,EAAE;AACrC,UAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,IAAI,OAAO,SAA+B;AA9BpF,UAAAA,KAAA;AA+BM,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,mBAAmB,KAAK,EAAE,EAAE;AACpD,YAAI,CAAC,IAAI,GAAI,QAAO;AACpB,cAAM,EAAE,MAAM,IAAI,MAAM,IAAI,KAAK;AAGjC,cAAM,YAAYA,OAAA,wBAAS,CAAC,GAAG,KAAK,CAAC,MAAW,EAAE,QAAQ,mBAAmB,EAAE,GAAG,MAAhE,OAAAA,OACZ,wBAAS,CAAC,GAAG,KAAK,CAAC,OACpB,EAAE,cAAc,aAAc,EAAE,YAAY,OAAO,EAAE,QAAQ,EAAE,SAAS,UAAU,MAAO,EAAE,GAAG;AACnG,YAAI,EAAC,qCAAU,KAAK,QAAO;AAC3B,cAAM,MAAM,MAAM,MAAM,SAAS,GAAG,EAAE,KAAK,OAAK,EAAE,KAAK,CAAC;AACxD,cAAM,OAAO,gBAAgB,GAAG;AAChC,YAAI,CAAC,KAAM,QAAO;AAClB,yBAAiB;AAAA,UACf,MAAM;AAAA,UACN,SAAS;AAAA,YACP,MAAM;AAAA,cACJ,IAAI,KAAK;AAAA,cACT,OAAM,UAAK,aAAL,YAAiB;AAAA,cACvB;AAAA,cACA,SAAS,SAAS;AAAA,cAClB,QAAQ,SAAS;AAAA,YACnB;AAAA,UACF;AAAA,QACF,CAAC;AACD,eAAO;AAAA,MACT,SACM;AACJ,eAAO;AAAA,MACT;AAAA,IACF,CAAC,CAAC;AAEF,QAAI,CAAC,IAAK;AACV,UAAM,aAAa,MAAM,OAAO,CAAC,MAAiB,CAAC,CAAC,CAAC;AACrD,QAAI,WAAW,SAAS,GAAG;AAEzB,UAAI,UAAU,WAAW,WAAW,KAAK,CAAS,GAAG,EAAE,SAAS,IAAI,UAAU,IAAK,CAAC;AAAA,IACtF,OACK;AAEH,YAAM,KAAK,MAAM,KAAK,OAAK,OAAO,EAAE,kBAAkB,YAAY,OAAO,EAAE,iBAAiB,QAAQ;AACpG,UAAI,GAAI,KAAI,MAAM,EAAE,QAAQ,CAAC,GAAG,eAAyB,GAAG,YAAsB,GAAG,MAAM,IAAI,UAAU,IAAK,CAAC;AAAA,IACjH;AAAA,EACF,GAAG,CAAC,KAAK,kBAAkB,aAAa,CAAC;AAC3C;","names":["_a"]}
|
package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { Building } from '../../../../../../../types/dbTypes';
|
|
3
|
+
interface AssociateBuildingsDialogProps {
|
|
4
|
+
siteId: number;
|
|
5
|
+
siteName: string;
|
|
6
|
+
buildings: Building[];
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
/** Called with the associated buildings after a successful connect. */
|
|
9
|
+
onAssociated?: (buildings: Building[]) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Shown after a site polygon is drawn when database buildings are detected
|
|
13
|
+
* inside it. The user picks (multi-select) which buildings to associate with
|
|
14
|
+
* the new site; selected buildings are connected via `updateSite`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const AssociateBuildingsDialog: ({ siteId, siteName, buildings, onClose, onAssociated, }: AssociateBuildingsDialogProps) => React.JSX.Element;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=AssociateBuildingsDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssociateBuildingsDialog.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAS9B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAA;AAElE,UAAU,6BAA6B;IACrC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,uEAAuE;IACvE,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAA;CAC/C;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAI,yDAEtC,6BAA6B,sBAsF/B,CAAA"}
|