@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
|
@@ -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"}
|
package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { Building2 } from "lucide-react";
|
|
7
|
+
import { AddItemDialog } from "../../../../../../ui/AddItemDialog";
|
|
8
|
+
import { Button } from "../../../../../../ui/Button";
|
|
9
|
+
import { Checkbox } from "../../../../../../ui/Checkbox";
|
|
10
|
+
import { LoadingSpinner } from "../../../../../../ui/LoadingSpinner";
|
|
11
|
+
import { useSite } from "../../../../../../../hooks/sites/sites";
|
|
12
|
+
const AssociateBuildingsDialog = ({
|
|
13
|
+
siteId,
|
|
14
|
+
siteName,
|
|
15
|
+
buildings,
|
|
16
|
+
onClose,
|
|
17
|
+
onAssociated
|
|
18
|
+
}) => {
|
|
19
|
+
const t = useTranslations("AssociateBuildings");
|
|
20
|
+
const tf = (key, fallback, values) => t.has(key) ? t(key, values) : fallback;
|
|
21
|
+
const { updateSite } = useSite(String(siteId));
|
|
22
|
+
const [selected, setSelected] = React.useState(
|
|
23
|
+
() => new Set(buildings.map((b) => b.id))
|
|
24
|
+
);
|
|
25
|
+
const [isSaving, setIsSaving] = React.useState(false);
|
|
26
|
+
const toggle = (id) => setSelected((prev) => {
|
|
27
|
+
const next = new Set(prev);
|
|
28
|
+
if (next.has(id)) next.delete(id);
|
|
29
|
+
else next.add(id);
|
|
30
|
+
return next;
|
|
31
|
+
});
|
|
32
|
+
const allSelected = selected.size === buildings.length;
|
|
33
|
+
const toggleAll = () => setSelected(allSelected ? /* @__PURE__ */ new Set() : new Set(buildings.map((b) => b.id)));
|
|
34
|
+
const handleAssociate = async () => {
|
|
35
|
+
if (selected.size === 0) {
|
|
36
|
+
onClose();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
setIsSaving(true);
|
|
40
|
+
try {
|
|
41
|
+
await updateSite({ siteBuildings: { connect: [...selected].map((id) => ({ id })) } });
|
|
42
|
+
toast.success(tf("associated", `${selected.size} building(s) associated with "${siteName}".`, {
|
|
43
|
+
count: selected.size,
|
|
44
|
+
name: siteName
|
|
45
|
+
}));
|
|
46
|
+
onAssociated == null ? void 0 : onAssociated(buildings.filter((b) => selected.has(b.id)));
|
|
47
|
+
onClose();
|
|
48
|
+
} catch (e) {
|
|
49
|
+
toast.error(tf("associateFailed", "Failed to associate the buildings. Please try again."));
|
|
50
|
+
setIsSaving(false);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
return /* @__PURE__ */ jsx(
|
|
54
|
+
AddItemDialog,
|
|
55
|
+
{
|
|
56
|
+
open: true,
|
|
57
|
+
onOpenChange: (open) => !open && !isSaving && onClose(),
|
|
58
|
+
title: tf("title", "Buildings found inside this site"),
|
|
59
|
+
description: tf("subtitle", `These buildings fall within "${siteName}". Choose which to associate with it.`, { name: siteName }),
|
|
60
|
+
icon: Building2,
|
|
61
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 pointer-events-auto min-w-0", children: [
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
"button",
|
|
64
|
+
{
|
|
65
|
+
type: "button",
|
|
66
|
+
onClick: toggleAll,
|
|
67
|
+
className: "self-start text-xs text-primary hover:underline",
|
|
68
|
+
children: allSelected ? tf("deselectAll", "Deselect all") : tf("selectAll", "Select all")
|
|
69
|
+
}
|
|
70
|
+
),
|
|
71
|
+
/* @__PURE__ */ jsx("div", { className: "max-h-64 overflow-y-auto flex flex-col gap-1 rounded-md border p-1", children: buildings.map((b) => /* @__PURE__ */ jsxs(
|
|
72
|
+
"label",
|
|
73
|
+
{
|
|
74
|
+
className: "flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-accent cursor-pointer min-w-0",
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ jsx(Checkbox, { className: "shrink-0", checked: selected.has(b.id), onCheckedChange: () => toggle(b.id) }),
|
|
77
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col min-w-0", children: [
|
|
78
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm truncate", children: b.buildingName || tf("unnamed", "Unnamed building") }),
|
|
79
|
+
b.buildingAddress && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground truncate", children: b.buildingAddress })
|
|
80
|
+
] })
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
b.id
|
|
84
|
+
)) }),
|
|
85
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
86
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, disabled: isSaving, children: tf("skip", "Skip") }),
|
|
87
|
+
/* @__PURE__ */ jsxs(Button, { onClick: handleAssociate, disabled: isSaving || selected.size === 0, children: [
|
|
88
|
+
isSaving && /* @__PURE__ */ jsx(LoadingSpinner, {}),
|
|
89
|
+
tf("associate", "Associate"),
|
|
90
|
+
" (",
|
|
91
|
+
selected.size,
|
|
92
|
+
")"
|
|
93
|
+
] })
|
|
94
|
+
] })
|
|
95
|
+
] })
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
export {
|
|
100
|
+
AssociateBuildingsDialog
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=AssociateBuildingsDialog.js.map
|
package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../src/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.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 { toast } from 'sonner'\r\nimport { useTranslations } from 'next-intl'\r\nimport { Building2 } from 'lucide-react'\r\nimport { AddItemDialog } from '../../../../../../ui/AddItemDialog'\r\nimport { Button } from '../../../../../../ui/Button'\r\nimport { Checkbox } from '../../../../../../ui/Checkbox'\r\nimport { LoadingSpinner } from '../../../../../../ui/LoadingSpinner'\r\nimport { useSite } from '../../../../../../../hooks/sites/sites'\r\nimport type { Building } from '../../../../../../../types/dbTypes'\r\n\r\ninterface AssociateBuildingsDialogProps {\r\n siteId: number\r\n siteName: string\r\n buildings: Building[]\r\n onClose: () => void\r\n /** Called with the associated buildings after a successful connect. */\r\n onAssociated?: (buildings: Building[]) => void\r\n}\r\n\r\n/**\r\n * Shown after a site polygon is drawn when database buildings are detected\r\n * inside it. The user picks (multi-select) which buildings to associate with\r\n * the new site; selected buildings are connected via `updateSite`.\r\n */\r\nexport const AssociateBuildingsDialog = ({\r\n siteId, siteName, buildings, onClose, onAssociated,\r\n}: AssociateBuildingsDialogProps) => {\r\n const t = useTranslations('AssociateBuildings')\r\n const tf = (key: string, fallback: string, values?: Record<string, string | number>) =>\r\n (t.has(key) ? t(key, values) : fallback)\r\n\r\n const { updateSite } = useSite(String(siteId))\r\n const [selected, setSelected] = React.useState<Set<number>>(\r\n () => new Set(buildings.map(b => b.id)),\r\n )\r\n const [isSaving, setIsSaving] = React.useState(false)\r\n\r\n const toggle = (id: number) => setSelected((prev) => {\r\n const next = new Set(prev)\r\n if (next.has(id)) next.delete(id)\r\n else next.add(id)\r\n return next\r\n })\r\n\r\n const allSelected = selected.size === buildings.length\r\n const toggleAll = () =>\r\n setSelected(allSelected ? new Set() : new Set(buildings.map(b => b.id)))\r\n\r\n const handleAssociate = async () => {\r\n if (selected.size === 0) { onClose(); return }\r\n setIsSaving(true)\r\n try {\r\n await updateSite({ siteBuildings: { connect: [...selected].map(id => ({ id })) } })\r\n toast.success(tf('associated', `${selected.size} building(s) associated with \"${siteName}\".`, {\r\n count: selected.size,\r\n name: siteName,\r\n }))\r\n onAssociated?.(buildings.filter(b => selected.has(b.id)))\r\n onClose()\r\n }\r\n catch {\r\n toast.error(tf('associateFailed', 'Failed to associate the buildings. Please try again.'))\r\n setIsSaving(false)\r\n }\r\n }\r\n\r\n return (\r\n <AddItemDialog\r\n open\r\n onOpenChange={open => !open && !isSaving && onClose()}\r\n title={tf('title', 'Buildings found inside this site')}\r\n description={tf('subtitle', `These buildings fall within \"${siteName}\". Choose which to associate with it.`, { name: siteName })}\r\n icon={Building2}\r\n >\r\n <div className=\"flex flex-col gap-3 pointer-events-auto min-w-0\">\r\n <button\r\n type=\"button\"\r\n onClick={toggleAll}\r\n className=\"self-start text-xs text-primary hover:underline\"\r\n >\r\n {allSelected ? tf('deselectAll', 'Deselect all') : tf('selectAll', 'Select all')}\r\n </button>\r\n\r\n <div className=\"max-h-64 overflow-y-auto flex flex-col gap-1 rounded-md border p-1\">\r\n {buildings.map(b => (\r\n <label\r\n key={b.id}\r\n className=\"flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-accent cursor-pointer min-w-0\"\r\n >\r\n <Checkbox className=\"shrink-0\" checked={selected.has(b.id)} onCheckedChange={() => toggle(b.id)} />\r\n <div className=\"flex flex-col min-w-0\">\r\n <span className=\"text-sm truncate\">{b.buildingName || tf('unnamed', 'Unnamed building')}</span>\r\n {b.buildingAddress && (\r\n <span className=\"text-xs text-muted-foreground truncate\">{b.buildingAddress}</span>\r\n )}\r\n </div>\r\n </label>\r\n ))}\r\n </div>\r\n\r\n <div className=\"flex justify-end gap-2\">\r\n <Button variant=\"outline\" onClick={onClose} disabled={isSaving}>\r\n {tf('skip', 'Skip')}\r\n </Button>\r\n <Button onClick={handleAssociate} disabled={isSaving || selected.size === 0}>\r\n {isSaving && <LoadingSpinner />}\r\n {tf('associate', 'Associate')} ({selected.size})\r\n </Button>\r\n </div>\r\n </div>\r\n </AddItemDialog>\r\n )\r\n}\r\n"],"mappings":";AAiFQ,cAeM,YAfN;AA5ER,YAAY,WAAW;AACvB,SAAS,aAAa;AACtB,SAAS,uBAAuB;AAChC,SAAS,iBAAiB;AAC1B,SAAS,qBAAqB;AAC9B,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AAiBjB,MAAM,2BAA2B,CAAC;AAAA,EACvC;AAAA,EAAQ;AAAA,EAAU;AAAA,EAAW;AAAA,EAAS;AACxC,MAAqC;AACnC,QAAM,IAAI,gBAAgB,oBAAoB;AAC9C,QAAM,KAAK,CAAC,KAAa,UAAkB,WACxC,EAAE,IAAI,GAAG,IAAI,EAAE,KAAK,MAAM,IAAI;AAEjC,QAAM,EAAE,WAAW,IAAI,QAAQ,OAAO,MAAM,CAAC;AAC7C,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM;AAAA,IACpC,MAAM,IAAI,IAAI,UAAU,IAAI,OAAK,EAAE,EAAE,CAAC;AAAA,EACxC;AACA,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,KAAK;AAEpD,QAAM,SAAS,CAAC,OAAe,YAAY,CAAC,SAAS;AACnD,UAAM,OAAO,IAAI,IAAI,IAAI;AACzB,QAAI,KAAK,IAAI,EAAE,EAAG,MAAK,OAAO,EAAE;AAAA,QAC3B,MAAK,IAAI,EAAE;AAChB,WAAO;AAAA,EACT,CAAC;AAED,QAAM,cAAc,SAAS,SAAS,UAAU;AAChD,QAAM,YAAY,MAChB,YAAY,cAAc,oBAAI,IAAI,IAAI,IAAI,IAAI,UAAU,IAAI,OAAK,EAAE,EAAE,CAAC,CAAC;AAEzE,QAAM,kBAAkB,YAAY;AAClC,QAAI,SAAS,SAAS,GAAG;AAAE,cAAQ;AAAG;AAAA,IAAO;AAC7C,gBAAY,IAAI;AAChB,QAAI;AACF,YAAM,WAAW,EAAE,eAAe,EAAE,SAAS,CAAC,GAAG,QAAQ,EAAE,IAAI,SAAO,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;AAClF,YAAM,QAAQ,GAAG,cAAc,GAAG,SAAS,IAAI,iCAAiC,QAAQ,MAAM;AAAA,QAC5F,OAAO,SAAS;AAAA,QAChB,MAAM;AAAA,MACR,CAAC,CAAC;AACF,mDAAe,UAAU,OAAO,OAAK,SAAS,IAAI,EAAE,EAAE,CAAC;AACvD,cAAQ;AAAA,IACV,SACM;AACJ,YAAM,MAAM,GAAG,mBAAmB,sDAAsD,CAAC;AACzF,kBAAY,KAAK;AAAA,IACnB;AAAA,EACF;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAI;AAAA,MACJ,cAAc,UAAQ,CAAC,QAAQ,CAAC,YAAY,QAAQ;AAAA,MACpD,OAAO,GAAG,SAAS,kCAAkC;AAAA,MACrD,aAAa,GAAG,YAAY,gCAAgC,QAAQ,yCAAyC,EAAE,MAAM,SAAS,CAAC;AAAA,MAC/H,MAAM;AAAA,MAEN,+BAAC,SAAI,WAAU,mDACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAU;AAAA,YAET,wBAAc,GAAG,eAAe,cAAc,IAAI,GAAG,aAAa,YAAY;AAAA;AAAA,QACjF;AAAA,QAEA,oBAAC,SAAI,WAAU,sEACZ,oBAAU,IAAI,OACb;AAAA,UAAC;AAAA;AAAA,YAEC,WAAU;AAAA,YAEV;AAAA,kCAAC,YAAS,WAAU,YAAW,SAAS,SAAS,IAAI,EAAE,EAAE,GAAG,iBAAiB,MAAM,OAAO,EAAE,EAAE,GAAG;AAAA,cACjG,qBAAC,SAAI,WAAU,yBACb;AAAA,oCAAC,UAAK,WAAU,oBAAoB,YAAE,gBAAgB,GAAG,WAAW,kBAAkB,GAAE;AAAA,gBACvF,EAAE,mBACD,oBAAC,UAAK,WAAU,0CAA0C,YAAE,iBAAgB;AAAA,iBAEhF;AAAA;AAAA;AAAA,UATK,EAAE;AAAA,QAUT,CACD,GACH;AAAA,QAEA,qBAAC,SAAI,WAAU,0BACb;AAAA,8BAAC,UAAO,SAAQ,WAAU,SAAS,SAAS,UAAU,UACnD,aAAG,QAAQ,MAAM,GACpB;AAAA,UACA,qBAAC,UAAO,SAAS,iBAAiB,UAAU,YAAY,SAAS,SAAS,GACvE;AAAA,wBAAY,oBAAC,kBAAe;AAAA,YAC5B,GAAG,aAAa,WAAW;AAAA,YAAE;AAAA,YAAG,SAAS;AAAA,YAAK;AAAA,aACjD;AAAA,WACF;AAAA,SACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SiteAdder.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"SiteAdder.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAe/B,UAAU,cAAc;IACtB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAKD,eAAO,MAAM,SAAS,GAAI,sBAA8B,cAAc,sBA0crE,CAAA"}
|