@collabdt/core 0.0.57 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -43
- package/dist/core/components/AppSidebarContent.d.ts.map +1 -1
- package/dist/core/components/AppSidebarContent.js +21 -27
- package/dist/core/components/AppSidebarContent.js.map +1 -1
- package/dist/core/components/authentication/AuthPage.d.ts.map +1 -1
- package/dist/core/components/authentication/AuthPage.js +32 -7
- package/dist/core/components/authentication/AuthPage.js.map +1 -1
- package/dist/core/components/authentication/SignInAuthHeader.d.ts +7 -0
- package/dist/core/components/authentication/SignInAuthHeader.d.ts.map +1 -0
- package/dist/core/components/authentication/SignInAuthHeader.js +32 -0
- package/dist/core/components/authentication/SignInAuthHeader.js.map +1 -0
- package/dist/core/components/authentication/Signin.d.ts.map +1 -1
- package/dist/core/components/authentication/Signin.js +308 -10
- package/dist/core/components/authentication/Signin.js.map +1 -1
- package/dist/core/components/settings/index.js +3 -3
- package/dist/core/components/settings/index.js.map +1 -1
- package/dist/core/components/settings/src/OrganizationSettingsPanel.d.ts.map +1 -1
- package/dist/core/components/settings/src/OrganizationSettingsPanel.js +5 -4
- package/dist/core/components/settings/src/OrganizationSettingsPanel.js.map +1 -1
- package/dist/core/components/settings/src/SettingsSidebar.d.ts.map +1 -1
- package/dist/core/components/settings/src/SettingsSidebar.js +12 -2
- package/dist/core/components/settings/src/SettingsSidebar.js.map +1 -1
- package/dist/core/components/support/BugReportDialog.d.ts.map +1 -1
- package/dist/core/components/support/BugReportDialog.js +122 -9
- package/dist/core/components/support/BugReportDialog.js.map +1 -1
- package/dist/core/components/support/FeatureRequestDialog.d.ts.map +1 -1
- package/dist/core/components/support/FeatureRequestDialog.js +61 -2
- package/dist/core/components/support/FeatureRequestDialog.js.map +1 -1
- package/dist/core/components/ui/AppSidebar.d.ts.map +1 -1
- package/dist/core/components/ui/AppSidebar.js +52 -9
- package/dist/core/components/ui/AppSidebar.js.map +1 -1
- package/dist/core/components/ui/DataTable.js +3 -3
- package/dist/core/components/ui/DataTable.js.map +1 -1
- package/dist/core/components/ui/Dialog.js +1 -1
- package/dist/core/components/ui/Dialog.js.map +1 -1
- package/dist/core/components/ui/InfoSidebar/Container.js +1 -1
- package/dist/core/components/ui/InfoSidebar/Container.js.map +1 -1
- package/dist/core/components/ui/Sensors/CollapsibleSensorItem.d.ts.map +1 -1
- package/dist/core/components/ui/Sensors/CollapsibleSensorItem.js +2 -2
- package/dist/core/components/ui/Sensors/CollapsibleSensorItem.js.map +1 -1
- package/dist/core/components/ui/Sensors/SensorsSection.d.ts.map +1 -1
- package/dist/core/components/ui/Sensors/SensorsSection.js +2 -1
- package/dist/core/components/ui/Sensors/SensorsSection.js.map +1 -1
- package/dist/core/components/ui/Sensors/sensorUtils.d.ts +9 -0
- package/dist/core/components/ui/Sensors/sensorUtils.d.ts.map +1 -1
- package/dist/core/components/ui/Sensors/sensorUtils.js +6 -0
- package/dist/core/components/ui/Sensors/sensorUtils.js.map +1 -1
- package/dist/core/components/ui/Sidebar.d.ts +4 -0
- package/dist/core/components/ui/Sidebar.d.ts.map +1 -1
- package/dist/core/components/ui/Sidebar.js +11 -5
- package/dist/core/components/ui/Sidebar.js.map +1 -1
- package/dist/core/components/ui/Table.js +2 -2
- package/dist/core/components/ui/Table.js.map +1 -1
- package/dist/core/components/viewers/Data/DataMenu.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/DataMenu.js +17 -9
- package/dist/core/components/viewers/Data/DataMenu.js.map +1 -1
- package/dist/core/components/viewers/Data/HeaderButtons.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/HeaderButtons.js +5 -12
- package/dist/core/components/viewers/Data/HeaderButtons.js.map +1 -1
- package/dist/core/components/viewers/Data/MoreOptions.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/MoreOptions.js +14 -27
- package/dist/core/components/viewers/Data/MoreOptions.js.map +1 -1
- package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.js +26 -0
- package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.js.map +1 -1
- package/dist/core/components/viewers/Data/buildingDetails/FieldRenderer.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/buildingDetails/FieldRenderer.js +13 -18
- package/dist/core/components/viewers/Data/buildingDetails/FieldRenderer.js.map +1 -1
- package/dist/core/components/viewers/Data/details/DetailHeader.js +14 -14
- package/dist/core/components/viewers/Data/details/DetailHeader.js.map +1 -1
- package/dist/core/components/viewers/Data/details/FieldValue.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/details/FieldValue.js +13 -3
- package/dist/core/components/viewers/Data/details/FieldValue.js.map +1 -1
- package/dist/core/components/viewers/Data/details/TabSidebar.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/details/TabSidebar.js +22 -0
- package/dist/core/components/viewers/Data/details/TabSidebar.js.map +1 -1
- package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.js +19 -0
- package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.js.map +1 -1
- package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.d.ts +2 -0
- package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.js +84 -3
- package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.js.map +1 -1
- package/dist/core/components/viewers/Data/siteDetails/SiteDetails.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/siteDetails/SiteDetails.js +42 -2
- package/dist/core/components/viewers/Data/siteDetails/SiteDetails.js.map +1 -1
- package/dist/core/components/viewers/Data/utils/Columns.js +2 -2
- package/dist/core/components/viewers/Data/utils/Columns.js.map +1 -1
- package/dist/core/components/viewers/Data/utils/positionParser.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/utils/positionParser.js +6 -5
- package/dist/core/components/viewers/Data/utils/positionParser.js.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.js +8 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.js.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.js +7 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.js.map +1 -1
- package/dist/core/components/viewers/map/datasets/RowActions.js +6 -6
- package/dist/core/components/viewers/map/datasets/RowActions.js.map +1 -1
- package/dist/core/components/viewers/map/datasets/data.js +1 -1
- package/dist/core/components/viewers/map/datasets/data.js.map +1 -1
- package/dist/core/components/viewers/map/datasets/index.d.ts.map +1 -1
- package/dist/core/components/viewers/map/datasets/index.js +28 -9
- package/dist/core/components/viewers/map/datasets/index.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/index.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/index.js +2 -0
- package/dist/core/components/viewers/map/src/MapLayers/index.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.d.ts +19 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.js +163 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.js.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.d.ts +3 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.js +310 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.js.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.d.ts +30 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.js +122 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.js.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.d.ts +13 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.js +58 -0
- package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.js.map +1 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.d.ts +18 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.js +102 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.js.map +1 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.js +243 -176
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.js.map +1 -1
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.d.ts +24 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.d.ts.map +1 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.js +46 -0
- package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.js.map +1 -0
- package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.d.ts +1 -0
- package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.d.ts.map +1 -1
- package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.js +1 -0
- package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.js.map +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.d.ts.map +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.js +54 -31
- package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.js.map +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/index.js +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/index.js.map +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.d.ts +2 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.d.ts.map +1 -1
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.js +5 -4
- package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.js.map +1 -1
- package/dist/core/hooks/sites/createSiteHooks.d.ts.map +1 -1
- package/dist/core/hooks/sites/createSiteHooks.js +0 -1
- package/dist/core/hooks/sites/createSiteHooks.js.map +1 -1
- package/dist/core/hooks/useCaptureScreenshot.d.ts.map +1 -1
- package/dist/core/hooks/useCaptureScreenshot.js +111 -33
- package/dist/core/hooks/useCaptureScreenshot.js.map +1 -1
- package/dist/core/hooks/useCurrentViewerPosition.d.ts +18 -0
- package/dist/core/hooks/useCurrentViewerPosition.d.ts.map +1 -0
- package/dist/core/hooks/useCurrentViewerPosition.js +66 -0
- package/dist/core/hooks/useCurrentViewerPosition.js.map +1 -0
- package/dist/core/hooks/useShareUrl.d.ts +2 -0
- package/dist/core/hooks/useShareUrl.d.ts.map +1 -0
- package/dist/core/hooks/useShareUrl.js +74 -0
- package/dist/core/hooks/useShareUrl.js.map +1 -0
- package/dist/core/store/CombineProviders.d.ts.map +1 -1
- package/dist/core/store/CombineProviders.js +2 -0
- package/dist/core/store/CombineProviders.js.map +1 -1
- package/dist/core/store/MapSites/context.d.ts +16 -0
- package/dist/core/store/MapSites/context.d.ts.map +1 -0
- package/dist/core/store/MapSites/context.js +29 -0
- package/dist/core/store/MapSites/context.js.map +1 -0
- package/dist/core/store/MapSites/reducer.d.ts +52 -0
- package/dist/core/store/MapSites/reducer.d.ts.map +1 -0
- package/dist/core/store/MapSites/reducer.js +70 -0
- package/dist/core/store/MapSites/reducer.js.map +1 -0
- package/dist/core/store/index.d.ts +1 -0
- package/dist/core/store/index.d.ts.map +1 -1
- package/dist/core/store/index.js +1 -0
- package/dist/core/store/index.js.map +1 -1
- package/dist/core/styles/globals.css +46 -33
- package/package.json +1 -1
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"}
|
|
@@ -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
|