@collabdt/core 0.0.57 → 0.2.0
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 +4 -26
- package/dist/core/components/support/BugReportDialog.d.ts.map +1 -1
- package/dist/core/components/support/BugReportDialog.js +32 -0
- 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 +32 -0
- package/dist/core/components/support/FeatureRequestDialog.js.map +1 -1
- package/dist/core/components/viewers/Data/DataMenu.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/DataMenu.js +9 -1
- 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/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/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/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 +23 -2
- package/dist/core/components/viewers/Data/siteDetails/SiteDetails.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/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 +77 -30
- package/dist/core/hooks/useCaptureScreenshot.js.map +1 -1
- 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/package.json +1 -1
|
@@ -1,123 +1,154 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Input, Label } from "../../../../../../ui/";
|
|
4
4
|
import * as React from "react";
|
|
5
|
-
import { MapContext } from "../../../../../../../store";
|
|
5
|
+
import { MapContext, useMapSitesContext } from "../../../../../../../store";
|
|
6
6
|
import { Button } from "../../../../../../ui/Button";
|
|
7
7
|
import { AddItemDialog } from "../../../../../../ui/AddItemDialog";
|
|
8
8
|
import { BoxSelect } from "lucide-react";
|
|
9
9
|
import { useTranslations } from "next-intl";
|
|
10
|
+
import { toast } from "sonner";
|
|
11
|
+
import { useCreateSite } from "../../../../../../../hooks/sites/sites";
|
|
12
|
+
import { useBuildings } from "../../../../../../../hooks/buildings/buildings";
|
|
13
|
+
import { persistDrawnSite } from "./persistSite";
|
|
14
|
+
import { polygonCentroid, filterPointsInRing } from "../../../MapLayers/src/SiteLayer/siteGeometry";
|
|
15
|
+
import { AssociateBuildingsDialog } from "./AssociateBuildingsDialog";
|
|
16
|
+
const DRAW_TOAST_ID = "site-adder-draw";
|
|
10
17
|
const SiteAdder = ({ isOpen = false, onCancel }) => {
|
|
11
18
|
const t = useTranslations("SiteAdder");
|
|
19
|
+
const tf = React.useCallback(
|
|
20
|
+
(key, fallback, values) => t.has(key) ? t(key, values) : fallback,
|
|
21
|
+
[t]
|
|
22
|
+
);
|
|
12
23
|
const [siteName, setSiteName] = React.useState("");
|
|
13
24
|
const [isDrawing, setIsDrawing] = React.useState(false);
|
|
14
25
|
const [drawingPoints, setDrawingPoints] = React.useState([]);
|
|
15
|
-
const {
|
|
26
|
+
const { state: mapState } = React.useContext(MapContext);
|
|
16
27
|
const { map } = mapState.map;
|
|
17
28
|
const inputRef = React.useRef(null);
|
|
29
|
+
const { createSite } = useCreateSite();
|
|
30
|
+
const { dispatch: mapSitesDispatch } = useMapSitesContext();
|
|
31
|
+
const { buildings, isLoading: buildingsLoading } = useBuildings();
|
|
32
|
+
const [pendingDetection, setPendingDetection] = React.useState(null);
|
|
33
|
+
const [associateState, setAssociateState] = React.useState(null);
|
|
34
|
+
const isPersistingRef = React.useRef(false);
|
|
35
|
+
const drawingPointsRef = React.useRef([]);
|
|
18
36
|
const hasMapApi = (mapInstance) => Boolean(mapInstance) && typeof mapInstance.getSource === "function" && typeof mapInstance.getLayer === "function" && typeof mapInstance.addSource === "function" && typeof mapInstance.addLayer === "function" && typeof mapInstance.on === "function" && typeof mapInstance.off === "function";
|
|
19
37
|
const setCursor = (cursor) => {
|
|
20
38
|
if (!map) return;
|
|
21
39
|
if (map.getCanvas().style.cursor === cursor) return;
|
|
22
40
|
map.getCanvas().style.cursor = cursor;
|
|
23
41
|
};
|
|
42
|
+
const getSource = () => map == null ? void 0 : map.getSource("add-geojson");
|
|
24
43
|
const updateDrawingFeatures = (points) => {
|
|
25
|
-
|
|
26
|
-
const source = map.getSource("add-geojson");
|
|
44
|
+
const source = getSource();
|
|
27
45
|
if (!source) return;
|
|
46
|
+
const closable = points.length >= 3;
|
|
28
47
|
const features = [];
|
|
29
|
-
|
|
48
|
+
points.forEach((point, index) => {
|
|
30
49
|
features.push({
|
|
31
50
|
type: "Feature",
|
|
32
|
-
geometry: {
|
|
33
|
-
type: "Point",
|
|
34
|
-
coordinates: point
|
|
35
|
-
},
|
|
51
|
+
geometry: { type: "Point", coordinates: point },
|
|
36
52
|
properties: {
|
|
37
53
|
isDrawPoint: true,
|
|
38
|
-
pointIndex: index
|
|
54
|
+
pointIndex: index,
|
|
55
|
+
isFirstPoint: index === 0,
|
|
56
|
+
// The first point turns green once a click on it would close the site.
|
|
57
|
+
closable: closable && index === 0
|
|
39
58
|
}
|
|
40
59
|
});
|
|
41
|
-
}
|
|
60
|
+
});
|
|
42
61
|
if (points.length > 1) {
|
|
43
62
|
features.push({
|
|
44
63
|
type: "Feature",
|
|
45
64
|
geometry: {
|
|
46
65
|
type: "LineString",
|
|
47
|
-
|
|
66
|
+
// Close the loop visually so the user sees the final shape.
|
|
67
|
+
coordinates: closable ? [...points, points[0]] : points
|
|
48
68
|
},
|
|
49
|
-
properties: {
|
|
50
|
-
isDrawing: true
|
|
51
|
-
}
|
|
69
|
+
properties: { isDrawing: true }
|
|
52
70
|
});
|
|
53
71
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
if (closable) {
|
|
73
|
+
features.push({
|
|
74
|
+
type: "Feature",
|
|
75
|
+
geometry: { type: "Polygon", coordinates: [[...points, points[0]]] },
|
|
76
|
+
properties: { isDrawPreview: true }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
source.setData({ type: "FeatureCollection", features });
|
|
80
|
+
};
|
|
81
|
+
const commitPoints = (points) => {
|
|
82
|
+
drawingPointsRef.current = points;
|
|
83
|
+
setDrawingPoints(points);
|
|
84
|
+
updateDrawingFeatures(points);
|
|
85
|
+
};
|
|
86
|
+
const isNearFirstPoint = (clickPoint, points, pixelThreshold = 14) => {
|
|
87
|
+
if (!map || points.length < 3) return false;
|
|
88
|
+
const first = map.project(points[0]);
|
|
89
|
+
const click = map.project(clickPoint);
|
|
90
|
+
return Math.hypot(first.x - click.x, first.y - click.y) <= pixelThreshold;
|
|
58
91
|
};
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
const source = map.getSource("add-geojson");
|
|
92
|
+
const clearDrawingFeatures = () => {
|
|
93
|
+
const source = getSource();
|
|
62
94
|
if (!source) return;
|
|
63
|
-
|
|
64
|
-
const siteFeature = {
|
|
65
|
-
type: "Feature",
|
|
66
|
-
geometry: {
|
|
67
|
-
type: "Polygon",
|
|
68
|
-
coordinates: [closedPoints]
|
|
69
|
-
},
|
|
70
|
-
properties: {
|
|
71
|
-
type: "site",
|
|
72
|
-
name: siteName || "Unnamed Site",
|
|
73
|
-
id: Date.now().toString()
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
const existingData = source._data;
|
|
77
|
-
const existingSites = existingData.features.filter(
|
|
78
|
-
(f) => {
|
|
79
|
-
var _a;
|
|
80
|
-
return ((_a = f.properties) == null ? void 0 : _a.type) === "site";
|
|
81
|
-
}
|
|
82
|
-
);
|
|
83
|
-
source.setData({
|
|
84
|
-
type: "FeatureCollection",
|
|
85
|
-
features: [...existingSites, siteFeature]
|
|
86
|
-
});
|
|
95
|
+
source.setData({ type: "FeatureCollection", features: [] });
|
|
87
96
|
};
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return i;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return -1;
|
|
97
|
+
const resetDrawingState = () => {
|
|
98
|
+
drawingPointsRef.current = [];
|
|
99
|
+
setDrawingPoints([]);
|
|
100
|
+
setIsDrawing(false);
|
|
101
|
+
setCursor("");
|
|
102
|
+
toast.dismiss(DRAW_TOAST_ID);
|
|
98
103
|
};
|
|
99
104
|
const startDrawing = () => {
|
|
100
|
-
|
|
105
|
+
if (!hasMapApi(map) || !map.getSource("add-geojson")) {
|
|
106
|
+
toast.error(tf("mapNotReady", "The map is still loading. Please try again in a moment."));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
drawingPointsRef.current = [];
|
|
101
110
|
setDrawingPoints([]);
|
|
111
|
+
setIsDrawing(true);
|
|
102
112
|
setCursor("crosshair");
|
|
103
113
|
};
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
const undoLastPoint = () => {
|
|
115
|
+
const points = drawingPointsRef.current;
|
|
116
|
+
if (points.length === 0) return;
|
|
117
|
+
commitPoints(points.slice(0, -1));
|
|
118
|
+
};
|
|
119
|
+
const cancelDrawing = () => {
|
|
120
|
+
clearDrawingFeatures();
|
|
121
|
+
resetDrawingState();
|
|
122
|
+
toast.info(tf("drawCancelled", "Site drawing cancelled."));
|
|
123
|
+
};
|
|
124
|
+
const finishDrawing = async () => {
|
|
125
|
+
if (isPersistingRef.current) return;
|
|
126
|
+
const points = drawingPointsRef.current;
|
|
127
|
+
if (points.length < 3) {
|
|
128
|
+
toast.warning(tf("needThreePoints", "Add at least 3 points before closing the site."));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const name = siteName.trim() || tf("unnamedSite", "Unnamed Site");
|
|
132
|
+
const ring = [...points];
|
|
133
|
+
const centroid = polygonCentroid(ring);
|
|
134
|
+
isPersistingRef.current = true;
|
|
135
|
+
const toastId = toast.loading(tf("savingSite", "Saving site\u2026"));
|
|
136
|
+
try {
|
|
137
|
+
const { siteId, assetId, fileId } = await persistDrawnSite({ name, ring, centroid, createSite });
|
|
138
|
+
mapSitesDispatch({
|
|
139
|
+
type: "SHOW_SITE",
|
|
140
|
+
payload: { site: { id: siteId, name, ring, assetId, fileId } }
|
|
120
141
|
});
|
|
142
|
+
toast.success(tf("siteCreated", `Site "${name}" created.`, { name }), { id: toastId });
|
|
143
|
+
clearDrawingFeatures();
|
|
144
|
+
resetDrawingState();
|
|
145
|
+
setSiteName("");
|
|
146
|
+
isPersistingRef.current = false;
|
|
147
|
+
setPendingDetection({ siteId, siteName: name, ring });
|
|
148
|
+
onCancel == null ? void 0 : onCancel();
|
|
149
|
+
} catch (e) {
|
|
150
|
+
toast.error(tf("siteCreateFailed", "Failed to create the site. Please try again."), { id: toastId });
|
|
151
|
+
isPersistingRef.current = false;
|
|
121
152
|
}
|
|
122
153
|
};
|
|
123
154
|
React.useEffect(() => {
|
|
@@ -132,16 +163,34 @@ const SiteAdder = ({ isOpen = false, onCancel }) => {
|
|
|
132
163
|
data: geojson
|
|
133
164
|
});
|
|
134
165
|
}
|
|
166
|
+
if (!map.getLayer("add-draw-fill")) {
|
|
167
|
+
map.addLayer({
|
|
168
|
+
id: "add-draw-fill",
|
|
169
|
+
type: "fill",
|
|
170
|
+
source: "add-geojson",
|
|
171
|
+
paint: {
|
|
172
|
+
"fill-color": "#3b82f6",
|
|
173
|
+
"fill-opacity": 0.2
|
|
174
|
+
},
|
|
175
|
+
filter: [
|
|
176
|
+
"all",
|
|
177
|
+
["in", "$type", "Polygon"],
|
|
178
|
+
["==", "isDrawPreview", true]
|
|
179
|
+
]
|
|
180
|
+
});
|
|
181
|
+
}
|
|
135
182
|
if (!map.getLayer("add-draw-points")) {
|
|
136
183
|
map.addLayer({
|
|
137
184
|
id: "add-draw-points",
|
|
138
185
|
type: "circle",
|
|
139
186
|
source: "add-geojson",
|
|
140
187
|
paint: {
|
|
141
|
-
|
|
142
|
-
"
|
|
143
|
-
"circle-
|
|
144
|
-
"circle-
|
|
188
|
+
// The first point is emphasised (and turns green once closing is
|
|
189
|
+
// possible) so it reads as "click here to close".
|
|
190
|
+
"circle-radius": ["case", ["==", ["get", "closable"], true], 8, 5],
|
|
191
|
+
"circle-color": ["case", ["==", ["get", "closable"], true], "#22c55e", "#ffffff"],
|
|
192
|
+
"circle-stroke-width": 2,
|
|
193
|
+
"circle-stroke-color": ["case", ["==", ["get", "closable"], true], "#15803d", "#000000"]
|
|
145
194
|
},
|
|
146
195
|
filter: [
|
|
147
196
|
"all",
|
|
@@ -171,117 +220,135 @@ const SiteAdder = ({ isOpen = false, onCancel }) => {
|
|
|
171
220
|
]
|
|
172
221
|
});
|
|
173
222
|
}
|
|
174
|
-
if (!map.getLayer("add-sites")) {
|
|
175
|
-
map.addLayer({
|
|
176
|
-
id: "add-sites",
|
|
177
|
-
type: "fill",
|
|
178
|
-
source: "add-geojson",
|
|
179
|
-
paint: {
|
|
180
|
-
"fill-color": "#3b82f6",
|
|
181
|
-
"fill-opacity": 0.5
|
|
182
|
-
},
|
|
183
|
-
filter: [
|
|
184
|
-
"all",
|
|
185
|
-
["in", "$type", "Polygon"],
|
|
186
|
-
["==", "type", "site"]
|
|
187
|
-
]
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
if (!map.getLayer("add-site-outline")) {
|
|
191
|
-
map.addLayer({
|
|
192
|
-
id: "add-site-outline",
|
|
193
|
-
type: "line",
|
|
194
|
-
source: "add-geojson",
|
|
195
|
-
layout: {
|
|
196
|
-
"line-cap": "round",
|
|
197
|
-
"line-join": "round"
|
|
198
|
-
},
|
|
199
|
-
paint: {
|
|
200
|
-
"line-color": "#1c4587",
|
|
201
|
-
"line-width": 2
|
|
202
|
-
},
|
|
203
|
-
filter: [
|
|
204
|
-
"all",
|
|
205
|
-
["in", "$type", "Polygon"],
|
|
206
|
-
["==", "type", "site"]
|
|
207
|
-
]
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
if (!map.getLayer("add-labels")) {
|
|
211
|
-
map.addLayer({
|
|
212
|
-
id: "add-labels",
|
|
213
|
-
type: "symbol",
|
|
214
|
-
source: "add-geojson",
|
|
215
|
-
layout: {
|
|
216
|
-
"text-field": ["get", "name"],
|
|
217
|
-
"text-font": ["Inter"],
|
|
218
|
-
"text-size": 12,
|
|
219
|
-
"text-offset": [0, -1.5],
|
|
220
|
-
"text-anchor": "center"
|
|
221
|
-
},
|
|
222
|
-
paint: {
|
|
223
|
-
"text-color": "#fff",
|
|
224
|
-
"text-halo-color": "#000",
|
|
225
|
-
"text-halo-width": 1
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
223
|
}, [map]);
|
|
230
224
|
React.useEffect(() => {
|
|
231
|
-
if (!hasMapApi(map)) return;
|
|
225
|
+
if (!hasMapApi(map) || !isDrawing) return;
|
|
232
226
|
const clickHandler = (e) => {
|
|
233
|
-
if (!isDrawing) return;
|
|
234
227
|
const clickPoint = [e.lngLat.lng, e.lngLat.lat];
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
stopDrawing();
|
|
239
|
-
setSiteName("");
|
|
240
|
-
} else {
|
|
241
|
-
const newPoints = [...drawingPoints, clickPoint];
|
|
242
|
-
setDrawingPoints(newPoints);
|
|
243
|
-
updateDrawingFeatures(newPoints);
|
|
228
|
+
if (isNearFirstPoint(clickPoint, drawingPointsRef.current)) {
|
|
229
|
+
finishDrawing();
|
|
230
|
+
return;
|
|
244
231
|
}
|
|
232
|
+
commitPoints([...drawingPointsRef.current, clickPoint]);
|
|
245
233
|
};
|
|
246
|
-
const mouseMoveHandler = (
|
|
247
|
-
if (isDrawing) setCursor("crosshair");
|
|
248
|
-
else setCursor(null);
|
|
249
|
-
};
|
|
234
|
+
const mouseMoveHandler = () => setCursor("crosshair");
|
|
250
235
|
map.on("click", clickHandler);
|
|
251
236
|
map.on("mousemove", mouseMoveHandler);
|
|
252
237
|
return () => {
|
|
253
238
|
map.off("click", clickHandler);
|
|
254
239
|
map.off("mousemove", mouseMoveHandler);
|
|
255
240
|
};
|
|
256
|
-
}, [map, isDrawing
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
)
|
|
279
|
-
] }),
|
|
280
|
-
/* @__PURE__ */ jsx("div", { className: "flex gap-2", children: /* @__PURE__ */ jsx(Button, { onClick: startDrawing, children: t("startDrawing") }) }),
|
|
281
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("startDrawingClick") })
|
|
282
|
-
] })
|
|
241
|
+
}, [map, isDrawing]);
|
|
242
|
+
React.useEffect(() => {
|
|
243
|
+
if (!isDrawing) return;
|
|
244
|
+
const onKey = (e) => {
|
|
245
|
+
if (e.key === "Enter") {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
finishDrawing();
|
|
248
|
+
} else if (e.key === "Escape") {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
cancelDrawing();
|
|
251
|
+
} else if (e.key === "Backspace") {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
undoLastPoint();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
window.addEventListener("keydown", onKey);
|
|
257
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
258
|
+
}, [isDrawing]);
|
|
259
|
+
React.useEffect(() => {
|
|
260
|
+
if (!isDrawing) {
|
|
261
|
+
toast.dismiss(DRAW_TOAST_ID);
|
|
262
|
+
return;
|
|
283
263
|
}
|
|
284
|
-
|
|
264
|
+
const count = drawingPoints.length;
|
|
265
|
+
const canFinish = count >= 3;
|
|
266
|
+
const title = canFinish ? tf("drawReadyTitle", "Ready to finish your site") : tf("drawTitle", "Draw your site");
|
|
267
|
+
const description = canFinish ? tf(
|
|
268
|
+
"drawReadyHint",
|
|
269
|
+
`${count} points placed. Click the first point, press Enter, or tap Finish to close it. Backspace removes the last point, Esc cancels.`,
|
|
270
|
+
{ count }
|
|
271
|
+
) : tf(
|
|
272
|
+
"drawHint",
|
|
273
|
+
`Click on the map to add each corner (${count} so far). Add at least 3 to form a site. Esc cancels.`,
|
|
274
|
+
{ count }
|
|
275
|
+
);
|
|
276
|
+
toast.info(title, {
|
|
277
|
+
id: DRAW_TOAST_ID,
|
|
278
|
+
description,
|
|
279
|
+
duration: Infinity,
|
|
280
|
+
action: canFinish ? { label: tf("finish", "Finish"), onClick: () => finishDrawing() } : void 0,
|
|
281
|
+
cancel: { label: tf("cancel", "Cancel"), onClick: () => cancelDrawing() }
|
|
282
|
+
});
|
|
283
|
+
}, [isDrawing, drawingPoints.length]);
|
|
284
|
+
React.useEffect(() => {
|
|
285
|
+
if (!isOpen && isDrawing) {
|
|
286
|
+
clearDrawingFeatures();
|
|
287
|
+
resetDrawingState();
|
|
288
|
+
}
|
|
289
|
+
}, [isOpen, isDrawing]);
|
|
290
|
+
React.useEffect(() => () => {
|
|
291
|
+
toast.dismiss(DRAW_TOAST_ID);
|
|
292
|
+
}, []);
|
|
293
|
+
React.useEffect(() => {
|
|
294
|
+
if (!pendingDetection) return;
|
|
295
|
+
if (buildingsLoading && buildings.length === 0) return;
|
|
296
|
+
const inside = filterPointsInRing(pendingDetection.ring, buildings, (b) => typeof b.buildingLongitude === "number" && typeof b.buildingLatitude === "number" ? [b.buildingLongitude, b.buildingLatitude] : null);
|
|
297
|
+
if (inside.length > 0) {
|
|
298
|
+
setAssociateState({
|
|
299
|
+
siteId: pendingDetection.siteId,
|
|
300
|
+
siteName: pendingDetection.siteName,
|
|
301
|
+
buildings: inside
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
setPendingDetection(null);
|
|
305
|
+
}, [pendingDetection, buildings, buildingsLoading]);
|
|
306
|
+
if (isDrawing) return null;
|
|
307
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
308
|
+
associateState && /* @__PURE__ */ jsx(
|
|
309
|
+
AssociateBuildingsDialog,
|
|
310
|
+
{
|
|
311
|
+
siteId: associateState.siteId,
|
|
312
|
+
siteName: associateState.siteName,
|
|
313
|
+
buildings: associateState.buildings,
|
|
314
|
+
onClose: () => setAssociateState(null)
|
|
315
|
+
}
|
|
316
|
+
),
|
|
317
|
+
/* @__PURE__ */ jsx(
|
|
318
|
+
AddItemDialog,
|
|
319
|
+
{
|
|
320
|
+
open: isOpen,
|
|
321
|
+
onOpenChange: (open) => !open && (onCancel == null ? void 0 : onCancel()),
|
|
322
|
+
title: t("title"),
|
|
323
|
+
icon: BoxSelect,
|
|
324
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 pointer-events-auto", children: [
|
|
325
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
326
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "site-name", children: t("nameLabel") }),
|
|
327
|
+
/* @__PURE__ */ jsx(
|
|
328
|
+
Input,
|
|
329
|
+
{
|
|
330
|
+
id: "site-name",
|
|
331
|
+
ref: inputRef,
|
|
332
|
+
type: "text",
|
|
333
|
+
placeholder: t("namePlaceholder"),
|
|
334
|
+
value: siteName,
|
|
335
|
+
onChange: (e) => setSiteName(e.target.value)
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
] }),
|
|
339
|
+
/* @__PURE__ */ jsxs("ol", { className: "list-decimal pl-4 text-xs text-muted-foreground space-y-1", children: [
|
|
340
|
+
/* @__PURE__ */ jsx("li", { children: tf("step1", "Name your site (optional), then start drawing.") }),
|
|
341
|
+
/* @__PURE__ */ jsx("li", { children: tf("step2", "Click on the map to drop each corner.") }),
|
|
342
|
+
/* @__PURE__ */ jsx("li", { children: tf("step3", "Click the first corner (or press Enter) to close it.") })
|
|
343
|
+
] }),
|
|
344
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-2", children: /* @__PURE__ */ jsxs(Button, { onClick: startDrawing, children: [
|
|
345
|
+
/* @__PURE__ */ jsx(BoxSelect, { className: "size-4" }),
|
|
346
|
+
t("startDrawing")
|
|
347
|
+
] }) })
|
|
348
|
+
] })
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
] });
|
|
285
352
|
};
|
|
286
353
|
export {
|
|
287
354
|
SiteAdder
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../../src/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.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 { Input, Label } from '../../../../../../ui/'\r\nimport * as React from \"react\";\r\nimport { MapContext } from '../../../../../../../store'\r\nimport { CursorType } from '../../../../../../../types/global'\r\nimport { Button } from '../../../../../../ui/Button'\r\nimport { AddItemDialog } from '../../../../../../ui/AddItemDialog'\r\nimport { BoxSelect } from 'lucide-react'\r\nimport { useTranslations } from 'next-intl'\r\n\r\ninterface SiteAdderProps {\r\n isOpen?: boolean\r\n onCancel?: () => void\r\n}\r\n\r\nexport const SiteAdder = ({ isOpen = false, onCancel }: SiteAdderProps) => {\r\n // Translation\r\n const t = useTranslations('SiteAdder')\r\n\r\n const [siteName, setSiteName] = React.useState<string>('')\r\n const [isDrawing, setIsDrawing] = React.useState<boolean>(false)\r\n const [drawingPoints, setDrawingPoints] = React.useState<[number, number][]>([])\r\n const { dispatch: mapDispatch, state: mapState } = React.useContext(MapContext)\r\n const { map } = mapState.map!\r\n const inputRef = React.useRef<HTMLInputElement>(null)\r\n\r\n // Added this to fix the issue of map not available.\r\n const hasMapApi = (\r\n mapInstance: unknown,\r\n ): mapInstance is maplibregl.Map => (\r\n Boolean(mapInstance)\r\n && typeof (mapInstance as maplibregl.Map).getSource === 'function'\r\n && typeof (mapInstance as maplibregl.Map).getLayer === 'function'\r\n && typeof (mapInstance as maplibregl.Map).addSource === 'function'\r\n && typeof (mapInstance as maplibregl.Map).addLayer === 'function'\r\n && typeof (mapInstance as maplibregl.Map).on === 'function'\r\n && typeof (mapInstance as maplibregl.Map).off === 'function'\r\n )\r\n\r\n const setCursor = (cursor: CursorType) => {\r\n if (!map) return\r\n if (map.getCanvas().style.cursor === cursor) return\r\n map.getCanvas().style.cursor = cursor\r\n }\r\n\r\n const updateDrawingFeatures = (points: [number, number][]) => {\r\n if (!map) return\r\n\r\n const source = map.getSource('add-geojson') as maplibregl.GeoJSONSource\r\n if (!source) return\r\n\r\n const features: GeoJSON.Feature[] = []\r\n\r\n // Add points\r\n for (const [index, point] of points.entries()) {\r\n features.push({\r\n type: 'Feature',\r\n geometry: {\r\n type: 'Point',\r\n coordinates: point,\r\n },\r\n properties: {\r\n isDrawPoint: true,\r\n pointIndex: index,\r\n },\r\n })\r\n }\r\n\r\n // Add connecting lines if we have more than one point\r\n if (points.length > 1) {\r\n features.push({\r\n type: 'Feature',\r\n geometry: {\r\n type: 'LineString',\r\n coordinates: points,\r\n },\r\n properties: {\r\n isDrawing: true,\r\n },\r\n })\r\n }\r\n\r\n source.setData({\r\n type: 'FeatureCollection',\r\n features,\r\n })\r\n }\r\n\r\n const addSite = (points: [number, number][]) => {\r\n if (!map || points.length < 3) return\r\n\r\n const source = map.getSource('add-geojson') as maplibregl.GeoJSONSource\r\n if (!source) return\r\n\r\n // Close the polygon by adding the first point at the end\r\n const closedPoints = [...points, points[0]]\r\n\r\n const siteFeature: GeoJSON.Feature = {\r\n type: 'Feature',\r\n geometry: {\r\n type: 'Polygon',\r\n coordinates: [closedPoints],\r\n },\r\n properties: {\r\n type: 'site',\r\n name: siteName || 'Unnamed Site',\r\n id: Date.now().toString(),\r\n },\r\n }\r\n\r\n // Get existing features (sites only, not drawing features)\r\n const existingData = source._data as GeoJSON.FeatureCollection\r\n const existingSites = existingData.features.filter(f =>\r\n f.properties?.type === 'site',\r\n )\r\n\r\n // Add the new site\r\n source.setData({\r\n type: 'FeatureCollection',\r\n features: [...existingSites, siteFeature],\r\n })\r\n }\r\n\r\n const isPointNearExisting = (clickPoint: [number, number], existingPoints: [number, number][], threshold = 0.0001): number => {\r\n for (const [i, existingPoint] of existingPoints.entries()) {\r\n const distance = Math.sqrt(\r\n Math.pow(clickPoint[0] - existingPoint[0], 2)\r\n + Math.pow(clickPoint[1] - existingPoint[1], 2),\r\n )\r\n if (distance < threshold) {\r\n return i\r\n }\r\n }\r\n return -1\r\n }\r\n\r\n const startDrawing = () => {\r\n setIsDrawing(true)\r\n setDrawingPoints([])\r\n setCursor('crosshair')\r\n }\r\n\r\n const stopDrawing = () => {\r\n setIsDrawing(false)\r\n setDrawingPoints([])\r\n setCursor(null)\r\n\r\n // Clear drawing features\r\n const source = map?.getSource('add-geojson') as maplibregl.GeoJSONSource\r\n if (source) {\r\n const existingData = source._data as GeoJSON.FeatureCollection\r\n const existingSites = existingData.features.filter(f =>\r\n f.properties?.type === 'site',\r\n )\r\n source.setData({\r\n type: 'FeatureCollection',\r\n features: existingSites,\r\n })\r\n }\r\n }\r\n\r\n // set up layer to add site\r\n React.useEffect(() => {\r\n if (!hasMapApi(map)) return\r\n // GeoJSON object to hold features\r\n const geojson: GeoJSON.FeatureCollection<GeoJSON.Geometry> = {\r\n type: 'FeatureCollection',\r\n features: [],\r\n }\r\n\r\n // Add source if it doesn't exist\r\n if (!map.getSource('add-geojson') && geojson) {\r\n map.addSource('add-geojson', {\r\n type: 'geojson',\r\n data: geojson,\r\n })\r\n }\r\n\r\n // Add layers for points during drawing\r\n if (!map.getLayer('add-draw-points')) {\r\n map.addLayer({\r\n id: 'add-draw-points',\r\n type: 'circle',\r\n source: 'add-geojson',\r\n paint: {\r\n 'circle-radius': 5,\r\n 'circle-color': '#fff',\r\n 'circle-stroke-width': 1,\r\n 'circle-stroke-color': '#000',\r\n },\r\n filter: ['all',\r\n ['in', '$type', 'Point'],\r\n ['==', 'isDrawPoint', true],\r\n ],\r\n })\r\n }\r\n\r\n // Add layers for lines during drawing\r\n if (!map.getLayer('add-draw-lines')) {\r\n map.addLayer({\r\n id: 'add-draw-lines',\r\n type: 'line',\r\n source: 'add-geojson',\r\n layout: {\r\n 'line-cap': 'round',\r\n 'line-join': 'round',\r\n },\r\n paint: {\r\n 'line-color': '#fff',\r\n 'line-width': 2.5,\r\n 'line-dasharray': [2, 2],\r\n },\r\n filter: ['all',\r\n ['in', '$type', 'LineString'],\r\n ['==', 'isDrawing', true],\r\n ],\r\n })\r\n }\r\n\r\n // Add layer for sites (2D polygons)\r\n if (!map.getLayer('add-sites')) {\r\n map.addLayer({\r\n id: 'add-sites',\r\n type: 'fill',\r\n source: 'add-geojson',\r\n paint: {\r\n 'fill-color': '#3b82f6',\r\n 'fill-opacity': 0.5,\r\n },\r\n filter: ['all',\r\n ['in', '$type', 'Polygon'],\r\n ['==', 'type', 'site'],\r\n ],\r\n })\r\n }\r\n\r\n // Layer for site outlines\r\n if (!map.getLayer('add-site-outline')) {\r\n map.addLayer({\r\n id: 'add-site-outline',\r\n type: 'line',\r\n source: 'add-geojson',\r\n layout: {\r\n 'line-cap': 'round',\r\n 'line-join': 'round',\r\n },\r\n paint: {\r\n 'line-color': '#1c4587',\r\n 'line-width': 2,\r\n },\r\n filter: ['all',\r\n ['in', '$type', 'Polygon'],\r\n ['==', 'type', 'site'],\r\n ],\r\n })\r\n }\r\n // Add labels for features\r\n if (!map.getLayer('add-labels')) {\r\n map.addLayer({\r\n id: 'add-labels',\r\n type: 'symbol',\r\n source: 'add-geojson',\r\n layout: {\r\n 'text-field': ['get', 'name'],\r\n 'text-font': ['Inter'],\r\n 'text-size': 12,\r\n 'text-offset': [0, -1.5],\r\n 'text-anchor': 'center',\r\n },\r\n paint: {\r\n 'text-color': '#fff',\r\n 'text-halo-color': '#000',\r\n 'text-halo-width': 1,\r\n },\r\n })\r\n }\r\n }, [map])\r\n\r\n React.useEffect(() => {\r\n if (!hasMapApi(map)) return\r\n\r\n const clickHandler = (e: maplibregl.MapMouseEvent): void => {\r\n if (!isDrawing) return\r\n\r\n const clickPoint: [number, number] = [e.lngLat.lng, e.lngLat.lat]\r\n\r\n // Check if clicking near an existing point to close the polygon\r\n const nearPointIndex = isPointNearExisting(clickPoint, drawingPoints)\r\n\r\n if (nearPointIndex !== -1 && drawingPoints.length >= 3) {\r\n // Close the polygon by creating a site\r\n addSite(drawingPoints)\r\n stopDrawing()\r\n setSiteName('')\r\n }\r\n else {\r\n // Add new point\r\n const newPoints = [...drawingPoints, clickPoint]\r\n setDrawingPoints(newPoints)\r\n updateDrawingFeatures(newPoints)\r\n }\r\n }\r\n\r\n const mouseMoveHandler = (e: maplibregl.MapMouseEvent): void => {\r\n if (isDrawing) setCursor('crosshair')\r\n else setCursor(null)\r\n }\r\n\r\n // Add event listeners\r\n map.on('click', clickHandler)\r\n map.on('mousemove', mouseMoveHandler)\r\n\r\n return () => {\r\n map.off('click', clickHandler)\r\n map.off('mousemove', mouseMoveHandler)\r\n }\r\n }, [map, isDrawing, drawingPoints, siteName])\r\n\r\n // When drawing is active, hide the dialog so the map is fully interactive.\r\n // The component stays mounted so useEffect listeners remain active.\r\n if (isDrawing) return null\r\n\r\n return (\r\n <AddItemDialog\r\n open={isOpen}\r\n onOpenChange={open => !open && onCancel?.()}\r\n title={t('title')}\r\n icon={BoxSelect}\r\n >\r\n <div className=\"flex flex-col gap-2 pointer-events-auto\">\r\n <div className=\"flex flex-col gap-2\">\r\n <Label htmlFor=\"site-name\">{t('nameLabel')}</Label>\r\n <Input\r\n id=\"site-name\"\r\n ref={inputRef}\r\n type=\"text\"\r\n placeholder={t('namePlaceholder')}\r\n value={siteName}\r\n onChange={e => setSiteName(e.target.value)}\r\n />\r\n </div>\r\n\r\n <div className=\"flex gap-2\">\r\n <Button onClick={startDrawing}>\r\n {t('startDrawing')}\r\n </Button>\r\n </div>\r\n\r\n <p className=\"text-xs text-muted-foreground\">\r\n {t('startDrawingClick')}\r\n </p>\r\n </div>\r\n </AddItemDialog>\r\n )\r\n}"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../src/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.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 { Input, Label } from '../../../../../../ui/'\r\nimport * as React from \"react\";\r\nimport { MapContext, useMapSitesContext } from '../../../../../../../store'\r\nimport { CursorType } from '../../../../../../../types/global'\r\nimport { Button } from '../../../../../../ui/Button'\r\nimport { AddItemDialog } from '../../../../../../ui/AddItemDialog'\r\nimport { BoxSelect } from 'lucide-react'\r\nimport { useTranslations } from 'next-intl'\r\nimport { toast } from 'sonner'\r\nimport { useCreateSite } from '../../../../../../../hooks/sites/sites'\r\nimport { useBuildings } from '../../../../../../../hooks/buildings/buildings'\r\nimport type { Building } from '../../../../../../../types/dbTypes'\r\nimport { persistDrawnSite } from './persistSite'\r\nimport { polygonCentroid, filterPointsInRing } from '../../../MapLayers/src/SiteLayer/siteGeometry'\r\nimport { AssociateBuildingsDialog } from './AssociateBuildingsDialog'\r\n\r\ninterface SiteAdderProps {\r\n isOpen?: boolean\r\n onCancel?: () => void\r\n}\r\n\r\n// A single, stable id so the instruction toast updates in place instead of stacking.\r\nconst DRAW_TOAST_ID = 'site-adder-draw'\r\n\r\nexport const SiteAdder = ({ isOpen = false, onCancel }: SiteAdderProps) => {\r\n // Translation\r\n const t = useTranslations('SiteAdder')\r\n\r\n // Translate with an English fallback. The translation messages for this\r\n // library live in the host app, so newer keys may not exist there yet —\r\n // `tf` keeps the tool usable (and translatable) in every host.\r\n const tf = React.useCallback(\r\n (key: string, fallback: string, values?: Record<string, string | number>) =>\r\n (t.has(key) ? t(key, values) : fallback),\r\n [t],\r\n )\r\n\r\n const [siteName, setSiteName] = React.useState<string>('')\r\n const [isDrawing, setIsDrawing] = React.useState<boolean>(false)\r\n const [drawingPoints, setDrawingPoints] = React.useState<[number, number][]>([])\r\n const { state: mapState } = React.useContext(MapContext)\r\n const { map } = mapState.map!\r\n const inputRef = React.useRef<HTMLInputElement>(null)\r\n\r\n // Persistence: create the DB site + upload its polygon geojson to minio, then\r\n // hand the polygon to the SiteLayer (via the MapSites store) for rendering.\r\n const { createSite } = useCreateSite()\r\n const { dispatch: mapSitesDispatch } = useMapSitesContext()\r\n\r\n // Database buildings (for detecting which fall inside a freshly drawn site).\r\n const { buildings, isLoading: buildingsLoading } = useBuildings()\r\n\r\n // A site that was just created and whose inside-buildings still need to be\r\n // detected. Held in state so detection re-runs once the buildings list loads.\r\n const [pendingDetection, setPendingDetection] = React.useState<\r\n { siteId: number, siteName: string, ring: [number, number][] } | null\r\n >(null)\r\n\r\n // When buildings are detected inside a freshly drawn site, prompt the user to\r\n // associate them (multi-select). Independent of the add-site dialog.\r\n const [associateState, setAssociateState] = React.useState<\r\n { siteId: number, siteName: string, buildings: Building[] } | null\r\n >(null)\r\n // A ref (not state) so all event-handler closures observe the live value and\r\n // a rapid second finish (double Enter / click + toast \"Finish\") can't create\r\n // a duplicate site before the first persist resolves.\r\n const isPersistingRef = React.useRef(false)\r\n\r\n // The committed points are mirrored in a ref so map / keyboard / toast\r\n // handlers always read the latest geometry without having to re-subscribe\r\n // on every click (which would otherwise drop fast double clicks).\r\n const drawingPointsRef = React.useRef<[number, number][]>([])\r\n\r\n // Added this to fix the issue of map not available.\r\n const hasMapApi = (\r\n mapInstance: unknown,\r\n ): mapInstance is maplibregl.Map => (\r\n Boolean(mapInstance)\r\n && typeof (mapInstance as maplibregl.Map).getSource === 'function'\r\n && typeof (mapInstance as maplibregl.Map).getLayer === 'function'\r\n && typeof (mapInstance as maplibregl.Map).addSource === 'function'\r\n && typeof (mapInstance as maplibregl.Map).addLayer === 'function'\r\n && typeof (mapInstance as maplibregl.Map).on === 'function'\r\n && typeof (mapInstance as maplibregl.Map).off === 'function'\r\n )\r\n\r\n const setCursor = (cursor: CursorType) => {\r\n if (!map) return\r\n if (map.getCanvas().style.cursor === cursor) return\r\n map.getCanvas().style.cursor = cursor\r\n }\r\n\r\n const getSource = () =>\r\n (map?.getSource('add-geojson') as maplibregl.GeoJSONSource | undefined)\r\n\r\n // Render the in-progress drawing: corner points, the connecting outline and\r\n // (once it can be closed) a translucent preview of the resulting polygon.\r\n // Created sites are rendered by SiteLayer, not here.\r\n const updateDrawingFeatures = (points: [number, number][]) => {\r\n const source = getSource()\r\n if (!source) return\r\n\r\n const closable = points.length >= 3\r\n const features: GeoJSON.Feature[] = []\r\n\r\n points.forEach((point, index) => {\r\n features.push({\r\n type: 'Feature',\r\n geometry: { type: 'Point', coordinates: point },\r\n properties: {\r\n isDrawPoint: true,\r\n pointIndex: index,\r\n isFirstPoint: index === 0,\r\n // The first point turns green once a click on it would close the site.\r\n closable: closable && index === 0,\r\n },\r\n })\r\n })\r\n\r\n if (points.length > 1) {\r\n features.push({\r\n type: 'Feature',\r\n geometry: {\r\n type: 'LineString',\r\n // Close the loop visually so the user sees the final shape.\r\n coordinates: closable ? [...points, points[0]] : points,\r\n },\r\n properties: { isDrawing: true },\r\n })\r\n }\r\n\r\n if (closable) {\r\n features.push({\r\n type: 'Feature',\r\n geometry: { type: 'Polygon', coordinates: [[...points, points[0]]] },\r\n properties: { isDrawPreview: true },\r\n })\r\n }\r\n\r\n source.setData({ type: 'FeatureCollection', features })\r\n }\r\n\r\n const commitPoints = (points: [number, number][]) => {\r\n drawingPointsRef.current = points\r\n setDrawingPoints(points)\r\n updateDrawingFeatures(points)\r\n }\r\n\r\n // Returns true when a click is close enough (in screen pixels, so it works at\r\n // any zoom level) to the first point to close the polygon.\r\n const isNearFirstPoint = (\r\n clickPoint: [number, number],\r\n points: [number, number][],\r\n pixelThreshold = 14,\r\n ): boolean => {\r\n if (!map || points.length < 3) return false\r\n const first = map.project(points[0])\r\n const click = map.project(clickPoint)\r\n return Math.hypot(first.x - click.x, first.y - click.y) <= pixelThreshold\r\n }\r\n\r\n // Remove the in-progress drawing features from the map.\r\n const clearDrawingFeatures = () => {\r\n const source = getSource()\r\n if (!source) return\r\n source.setData({ type: 'FeatureCollection', features: [] })\r\n }\r\n\r\n const resetDrawingState = () => {\r\n drawingPointsRef.current = []\r\n setDrawingPoints([])\r\n setIsDrawing(false)\r\n setCursor('')\r\n toast.dismiss(DRAW_TOAST_ID)\r\n }\r\n\r\n const startDrawing = () => {\r\n if (!hasMapApi(map) || !map.getSource('add-geojson')) {\r\n toast.error(tf('mapNotReady', 'The map is still loading. Please try again in a moment.'))\r\n return\r\n }\r\n drawingPointsRef.current = []\r\n setDrawingPoints([])\r\n setIsDrawing(true)\r\n setCursor('crosshair')\r\n }\r\n\r\n const undoLastPoint = () => {\r\n const points = drawingPointsRef.current\r\n if (points.length === 0) return\r\n commitPoints(points.slice(0, -1))\r\n }\r\n\r\n const cancelDrawing = () => {\r\n clearDrawingFeatures()\r\n resetDrawingState()\r\n toast.info(tf('drawCancelled', 'Site drawing cancelled.'))\r\n }\r\n\r\n const finishDrawing = async () => {\r\n if (isPersistingRef.current) return\r\n const points = drawingPointsRef.current\r\n if (points.length < 3) {\r\n toast.warning(tf('needThreePoints', 'Add at least 3 points before closing the site.'))\r\n return\r\n }\r\n const name = siteName.trim() || tf('unnamedSite', 'Unnamed Site')\r\n const ring = [...points]\r\n const centroid = polygonCentroid(ring)\r\n\r\n // Set synchronously before the first await so a second invocation bails.\r\n isPersistingRef.current = true\r\n const toastId = toast.loading(tf('savingSite', 'Saving site…'))\r\n try {\r\n const { siteId, assetId, fileId } = await persistDrawnSite({ name, ring, centroid, createSite })\r\n // Hand the polygon to SiteLayer for clickable, persistent rendering.\r\n mapSitesDispatch({\r\n type: 'SHOW_SITE',\r\n payload: { site: { id: siteId, name, ring, assetId, fileId } },\r\n })\r\n toast.success(tf('siteCreated', `Site \"${name}\" created.`, { name }), { id: toastId })\r\n clearDrawingFeatures()\r\n resetDrawingState()\r\n setSiteName('')\r\n isPersistingRef.current = false\r\n\r\n // Detect buildings inside the polygon (deferred to the effect below so it\r\n // still works if the buildings list hasn't finished loading yet).\r\n setPendingDetection({ siteId, siteName: name, ring })\r\n\r\n // Don't reopen the Add Site dialog — creation is finished.\r\n onCancel?.()\r\n }\r\n catch {\r\n toast.error(tf('siteCreateFailed', 'Failed to create the site. Please try again.'), { id: toastId })\r\n isPersistingRef.current = false\r\n // Keep the drawing so the user can retry finishing.\r\n }\r\n }\r\n\r\n // set up layer to add site\r\n React.useEffect(() => {\r\n if (!hasMapApi(map)) return\r\n // GeoJSON object to hold features\r\n const geojson: GeoJSON.FeatureCollection<GeoJSON.Geometry> = {\r\n type: 'FeatureCollection',\r\n features: [],\r\n }\r\n\r\n // Add source if it doesn't exist\r\n if (!map.getSource('add-geojson') && geojson) {\r\n map.addSource('add-geojson', {\r\n type: 'geojson',\r\n data: geojson,\r\n })\r\n }\r\n\r\n // Translucent preview of the polygon currently being drawn (added first so\r\n // the outline and points render on top of it).\r\n if (!map.getLayer('add-draw-fill')) {\r\n map.addLayer({\r\n id: 'add-draw-fill',\r\n type: 'fill',\r\n source: 'add-geojson',\r\n paint: {\r\n 'fill-color': '#3b82f6',\r\n 'fill-opacity': 0.2,\r\n },\r\n filter: ['all',\r\n ['in', '$type', 'Polygon'],\r\n ['==', 'isDrawPreview', true],\r\n ],\r\n })\r\n }\r\n\r\n // Add layers for points during drawing\r\n if (!map.getLayer('add-draw-points')) {\r\n map.addLayer({\r\n id: 'add-draw-points',\r\n type: 'circle',\r\n source: 'add-geojson',\r\n paint: {\r\n // The first point is emphasised (and turns green once closing is\r\n // possible) so it reads as \"click here to close\".\r\n 'circle-radius': ['case', ['==', ['get', 'closable'], true], 8, 5],\r\n 'circle-color': ['case', ['==', ['get', 'closable'], true], '#22c55e', '#ffffff'],\r\n 'circle-stroke-width': 2,\r\n 'circle-stroke-color': ['case', ['==', ['get', 'closable'], true], '#15803d', '#000000'],\r\n },\r\n filter: ['all',\r\n ['in', '$type', 'Point'],\r\n ['==', 'isDrawPoint', true],\r\n ],\r\n })\r\n }\r\n\r\n // Add layers for lines during drawing\r\n if (!map.getLayer('add-draw-lines')) {\r\n map.addLayer({\r\n id: 'add-draw-lines',\r\n type: 'line',\r\n source: 'add-geojson',\r\n layout: {\r\n 'line-cap': 'round',\r\n 'line-join': 'round',\r\n },\r\n paint: {\r\n 'line-color': '#fff',\r\n 'line-width': 2.5,\r\n 'line-dasharray': [2, 2],\r\n },\r\n filter: ['all',\r\n ['in', '$type', 'LineString'],\r\n ['==', 'isDrawing', true],\r\n ],\r\n })\r\n }\r\n\r\n }, [map])\r\n\r\n // Map interaction while drawing: click to add a corner, click the first\r\n // corner to close. Bound only while drawing so normal map use is unaffected.\r\n React.useEffect(() => {\r\n if (!hasMapApi(map) || !isDrawing) return\r\n\r\n const clickHandler = (e: maplibregl.MapMouseEvent): void => {\r\n const clickPoint: [number, number] = [e.lngLat.lng, e.lngLat.lat]\r\n\r\n // Click near the first point to close the polygon.\r\n if (isNearFirstPoint(clickPoint, drawingPointsRef.current)) {\r\n finishDrawing()\r\n return\r\n }\r\n\r\n commitPoints([...drawingPointsRef.current, clickPoint])\r\n }\r\n\r\n const mouseMoveHandler = (): void => setCursor('crosshair')\r\n\r\n map.on('click', clickHandler)\r\n map.on('mousemove', mouseMoveHandler)\r\n\r\n return () => {\r\n map.off('click', clickHandler)\r\n map.off('mousemove', mouseMoveHandler)\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [map, isDrawing])\r\n\r\n // Keyboard shortcuts while drawing: Enter finishes, Esc cancels, Backspace undoes.\r\n React.useEffect(() => {\r\n if (!isDrawing) return\r\n const onKey = (e: KeyboardEvent) => {\r\n if (e.key === 'Enter') { e.preventDefault(); finishDrawing() }\r\n else if (e.key === 'Escape') { e.preventDefault(); cancelDrawing() }\r\n else if (e.key === 'Backspace') { e.preventDefault(); undoLastPoint() }\r\n }\r\n window.addEventListener('keydown', onKey)\r\n return () => window.removeEventListener('keydown', onKey)\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [isDrawing])\r\n\r\n // Persistent instruction / feedback toast that updates as points are added.\r\n React.useEffect(() => {\r\n if (!isDrawing) {\r\n toast.dismiss(DRAW_TOAST_ID)\r\n return\r\n }\r\n\r\n const count = drawingPoints.length\r\n const canFinish = count >= 3\r\n\r\n const title = canFinish\r\n ? tf('drawReadyTitle', 'Ready to finish your site')\r\n : tf('drawTitle', 'Draw your site')\r\n\r\n const description = canFinish\r\n ? tf(\r\n 'drawReadyHint',\r\n `${count} points placed. Click the first point, press Enter, or tap Finish to close it. Backspace removes the last point, Esc cancels.`,\r\n { count },\r\n )\r\n : tf(\r\n 'drawHint',\r\n `Click on the map to add each corner (${count} so far). Add at least 3 to form a site. Esc cancels.`,\r\n { count },\r\n )\r\n\r\n toast.info(title, {\r\n id: DRAW_TOAST_ID,\r\n description,\r\n duration: Infinity,\r\n action: canFinish\r\n ? { label: tf('finish', 'Finish'), onClick: () => finishDrawing() }\r\n : undefined,\r\n cancel: { label: tf('cancel', 'Cancel'), onClick: () => cancelDrawing() },\r\n })\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [isDrawing, drawingPoints.length])\r\n\r\n // If the tool is closed (e.g. the user switches tools) mid-draw, clean up.\r\n React.useEffect(() => {\r\n if (!isOpen && isDrawing) {\r\n clearDrawingFeatures()\r\n resetDrawingState()\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [isOpen, isDrawing])\r\n\r\n // Dismiss the instruction toast if the component unmounts while drawing.\r\n React.useEffect(() => () => { toast.dismiss(DRAW_TOAST_ID) }, [])\r\n\r\n // After a site is created, detect the database buildings inside it and prompt\r\n // to associate them. Waits for the buildings list to load before deciding, so\r\n // a quick draw on a cold cache doesn't silently skip the prompt.\r\n React.useEffect(() => {\r\n if (!pendingDetection) return\r\n if (buildingsLoading && buildings.length === 0) return\r\n const inside = filterPointsInRing(pendingDetection.ring, buildings, b =>\r\n (typeof b.buildingLongitude === 'number' && typeof b.buildingLatitude === 'number')\r\n ? [b.buildingLongitude, b.buildingLatitude]\r\n : null)\r\n if (inside.length > 0) {\r\n setAssociateState({\r\n siteId: pendingDetection.siteId,\r\n siteName: pendingDetection.siteName,\r\n buildings: inside,\r\n })\r\n }\r\n setPendingDetection(null)\r\n }, [pendingDetection, buildings, buildingsLoading])\r\n\r\n // When drawing is active, hide the dialog so the map is fully interactive.\r\n // The component stays mounted so useEffect listeners remain active and the\r\n // instruction toast guides the user through finishing the polygon.\r\n if (isDrawing) return null\r\n\r\n return (\r\n <>\r\n {associateState && (\r\n <AssociateBuildingsDialog\r\n siteId={associateState.siteId}\r\n siteName={associateState.siteName}\r\n buildings={associateState.buildings}\r\n onClose={() => setAssociateState(null)}\r\n />\r\n )}\r\n <AddItemDialog\r\n open={isOpen}\r\n onOpenChange={open => !open && onCancel?.()}\r\n title={t('title')}\r\n icon={BoxSelect}\r\n >\r\n <div className=\"flex flex-col gap-3 pointer-events-auto\">\r\n <div className=\"flex flex-col gap-2\">\r\n <Label htmlFor=\"site-name\">{t('nameLabel')}</Label>\r\n <Input\r\n id=\"site-name\"\r\n ref={inputRef}\r\n type=\"text\"\r\n placeholder={t('namePlaceholder')}\r\n value={siteName}\r\n onChange={e => setSiteName(e.target.value)}\r\n />\r\n </div>\r\n\r\n <ol className=\"list-decimal pl-4 text-xs text-muted-foreground space-y-1\">\r\n <li>{tf('step1', 'Name your site (optional), then start drawing.')}</li>\r\n <li>{tf('step2', 'Click on the map to drop each corner.')}</li>\r\n <li>{tf('step3', 'Click the first corner (or press Enter) to close it.')}</li>\r\n </ol>\r\n\r\n <div className=\"flex gap-2\">\r\n <Button onClick={startDrawing}>\r\n <BoxSelect className=\"size-4\" />\r\n {t('startDrawing')}\r\n </Button>\r\n </div>\r\n </div>\r\n </AddItemDialog>\r\n </>\r\n )\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Ring } from '../../../MapLayers/src/SiteLayer/siteGeometry';
|
|
2
|
+
/** Tag stored on the File row so the site's boundary geojson can be found later. */
|
|
3
|
+
export declare const SITE_GEOMETRY_TAG = "site-geometry";
|
|
4
|
+
export interface PersistedSite {
|
|
5
|
+
siteId: number;
|
|
6
|
+
assetId: string;
|
|
7
|
+
fileId?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Persist a freshly drawn site:
|
|
11
|
+
* 1. create the DB Site (via the injected `createSite` hook trigger),
|
|
12
|
+
* 2. upload the polygon GeoJSON to minio (presigned PUT),
|
|
13
|
+
* 3. create a File row attached to the site that references the minio object.
|
|
14
|
+
*
|
|
15
|
+
* The polygon geometry lives in minio (there is no geometry column on Site);
|
|
16
|
+
* the File row links it to the site via `attachedFilesSiteId` + the geometry tag.
|
|
17
|
+
*/
|
|
18
|
+
export declare const persistDrawnSite: (params: {
|
|
19
|
+
name: string;
|
|
20
|
+
ring: Ring;
|
|
21
|
+
centroid: [number, number];
|
|
22
|
+
createSite: (arg: any) => Promise<any>;
|
|
23
|
+
}) => Promise<PersistedSite>;
|
|
24
|
+
//# sourceMappingURL=persistSite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistSite.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.ts"],"names":[],"mappings":"AAIA,OAAO,EAA2B,KAAK,IAAI,EAAE,MAAM,+CAA+C,CAAA;AAElG,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,kBAAkB,CAAA;AAEhD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAU,QAAQ;IAC7C,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,IAAI,CAAA;IACV,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC1B,UAAU,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;CACvC,KAAG,OAAO,CAAC,aAAa,CA2CxB,CAAA"}
|