@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
|
@@ -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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { uploadToPresignedUrl } from "../AddFile/utils/uploadToPresignedURLS";
|
|
2
|
+
import { ringToFeatureCollection } from "../../../MapLayers/src/SiteLayer/siteGeometry";
|
|
3
|
+
const SITE_GEOMETRY_TAG = "site-geometry";
|
|
4
|
+
const persistDrawnSite = async (params) => {
|
|
5
|
+
var _a;
|
|
6
|
+
const { name, ring, centroid, createSite } = params;
|
|
7
|
+
const site = await createSite({
|
|
8
|
+
siteName: name,
|
|
9
|
+
siteLongitude: centroid[0],
|
|
10
|
+
siteLatitude: centroid[1]
|
|
11
|
+
});
|
|
12
|
+
const siteId = site == null ? void 0 : site.id;
|
|
13
|
+
if (!siteId) throw new Error("Site creation did not return an id");
|
|
14
|
+
const assetId = `${crypto.randomUUID()}.geojson`;
|
|
15
|
+
const fc = ringToFeatureCollection(ring, name);
|
|
16
|
+
const file = new File([JSON.stringify(fc)], `${name || "site"}.geojson`, {
|
|
17
|
+
type: "application/geo+json"
|
|
18
|
+
});
|
|
19
|
+
const presRes = await fetch(`/api/presigned-url-upload?asset=${encodeURIComponent(assetId)}`);
|
|
20
|
+
if (!presRes.ok) throw new Error("Failed to get an upload URL");
|
|
21
|
+
const { presignedUrl } = await presRes.json();
|
|
22
|
+
await uploadToPresignedUrl(presignedUrl, file);
|
|
23
|
+
const fileRes = await fetch(`/api/files/site/${siteId}`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/json" },
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
type: "map-file",
|
|
28
|
+
name: `${name || "site"}.geojson`,
|
|
29
|
+
assetId,
|
|
30
|
+
mimeType: "application/geo+json",
|
|
31
|
+
extension: "geojson",
|
|
32
|
+
sizeBytes: file.size,
|
|
33
|
+
tag: SITE_GEOMETRY_TAG,
|
|
34
|
+
isVisible: false
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
if (!fileRes.ok) throw new Error("Failed to attach the geometry file to the site");
|
|
38
|
+
const data = await fileRes.json().catch(() => null);
|
|
39
|
+
const fileId = (_a = data == null ? void 0 : data.newFile) == null ? void 0 : _a.id;
|
|
40
|
+
return { siteId, assetId, fileId };
|
|
41
|
+
};
|
|
42
|
+
export {
|
|
43
|
+
SITE_GEOMETRY_TAG,
|
|
44
|
+
persistDrawnSite
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=persistSite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../src/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport { uploadToPresignedUrl } from '../AddFile/utils/uploadToPresignedURLS'\r\nimport { ringToFeatureCollection, type Ring } from '../../../MapLayers/src/SiteLayer/siteGeometry'\r\n\r\n/** Tag stored on the File row so the site's boundary geojson can be found later. */\r\nexport const SITE_GEOMETRY_TAG = 'site-geometry'\r\n\r\nexport interface PersistedSite {\r\n siteId: number\r\n assetId: string\r\n fileId?: number\r\n}\r\n\r\n/**\r\n * Persist a freshly drawn site:\r\n * 1. create the DB Site (via the injected `createSite` hook trigger),\r\n * 2. upload the polygon GeoJSON to minio (presigned PUT),\r\n * 3. create a File row attached to the site that references the minio object.\r\n *\r\n * The polygon geometry lives in minio (there is no geometry column on Site);\r\n * the File row links it to the site via `attachedFilesSiteId` + the geometry tag.\r\n */\r\nexport const persistDrawnSite = async (params: {\r\n name: string\r\n ring: Ring\r\n centroid: [number, number]\r\n createSite: (arg: any) => Promise<any>\r\n}): Promise<PersistedSite> => {\r\n const { name, ring, centroid, createSite } = params\r\n\r\n const site = await createSite({\r\n siteName: name,\r\n siteLongitude: centroid[0],\r\n siteLatitude: centroid[1],\r\n })\r\n const siteId = site?.id\r\n if (!siteId) throw new Error('Site creation did not return an id')\r\n\r\n const assetId = `${crypto.randomUUID()}.geojson`\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\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 const fileRes = await fetch(`/api/files/site/${siteId}`, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({\r\n type: 'map-file',\r\n name: `${name || 'site'}.geojson`,\r\n assetId,\r\n mimeType: 'application/geo+json',\r\n extension: 'geojson',\r\n sizeBytes: file.size,\r\n tag: SITE_GEOMETRY_TAG,\r\n isVisible: false,\r\n }),\r\n })\r\n // The DB link is required: without it the boundary is orphaned in minio and\r\n // can never be re-shown. Treat failure as fatal so the caller can retry.\r\n if (!fileRes.ok) throw new Error('Failed to attach the geometry file to the site')\r\n const data = await fileRes.json().catch(() => null)\r\n const fileId: number | undefined = data?.newFile?.id\r\n\r\n return { siteId, assetId, fileId }\r\n}\r\n"],"mappings":"AAGA,SAAS,4BAA4B;AACrC,SAAS,+BAA0C;AAG5C,MAAM,oBAAoB;AAiB1B,MAAM,mBAAmB,OAAO,WAKT;AA7B9B;AA8BE,QAAM,EAAE,MAAM,MAAM,UAAU,WAAW,IAAI;AAE7C,QAAM,OAAO,MAAM,WAAW;AAAA,IAC5B,UAAU;AAAA,IACV,eAAe,SAAS,CAAC;AAAA,IACzB,cAAc,SAAS,CAAC;AAAA,EAC1B,CAAC;AACD,QAAM,SAAS,6BAAM;AACrB,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,oCAAoC;AAEjE,QAAM,UAAU,GAAG,OAAO,WAAW,CAAC;AACtC,QAAM,KAAK,wBAAwB,MAAM,IAAI;AAC7C,QAAM,OAAO,IAAI,KAAK,CAAC,KAAK,UAAU,EAAE,CAAC,GAAG,GAAG,QAAQ,MAAM,YAAY;AAAA,IACvE,MAAM;AAAA,EACR,CAAC;AAED,QAAM,UAAU,MAAM,MAAM,mCAAmC,mBAAmB,OAAO,CAAC,EAAE;AAC5F,MAAI,CAAC,QAAQ,GAAI,OAAM,IAAI,MAAM,6BAA6B;AAC9D,QAAM,EAAE,aAAa,IAAI,MAAM,QAAQ,KAAK;AAC5C,QAAM,qBAAqB,cAAc,IAAI;AAE7C,QAAM,UAAU,MAAM,MAAM,mBAAmB,MAAM,IAAI;AAAA,IACvD,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU;AAAA,MACnB,MAAM;AAAA,MACN,MAAM,GAAG,QAAQ,MAAM;AAAA,MACvB;AAAA,MACA,UAAU;AAAA,MACV,WAAW;AAAA,MACX,WAAW,KAAK;AAAA,MAChB,KAAK;AAAA,MACL,WAAW;AAAA,IACb,CAAC;AAAA,EACH,CAAC;AAGD,MAAI,CAAC,QAAQ,GAAI,OAAM,IAAI,MAAM,gDAAgD;AACjF,QAAM,OAAO,MAAM,QAAQ,KAAK,EAAE,MAAM,MAAM,IAAI;AAClD,QAAM,UAA6B,kCAAM,YAAN,mBAAe;AAElD,SAAO,EAAE,QAAQ,SAAS,OAAO;AACnC;","names":[]}
|
|
@@ -5,6 +5,7 @@ import type maplibregl from 'maplibre-gl';
|
|
|
5
5
|
* Higher priority is clicked first.
|
|
6
6
|
*/
|
|
7
7
|
export declare enum MapLayerClickPriority {
|
|
8
|
+
SiteLayerClickPriority = 50,
|
|
8
9
|
BuildingLayersClickPriority = 100,
|
|
9
10
|
CommentLayersClickPriority = 200,
|
|
10
11
|
MartinLayerClickPriority = 300,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MapClickManager.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/components/viewers/map/utils/MapEventManager/MapClickManager.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,UAAU,MAAM,aAAa,CAAA;AAEzC;;;;GAIG;AACH,oBAAY,qBAAqB;
|
|
1
|
+
{"version":3,"file":"MapClickManager.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/components/viewers/map/utils/MapEventManager/MapClickManager.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,UAAU,MAAM,aAAa,CAAA;AAEzC;;;;GAIG;AACH,oBAAY,qBAAqB;IAG/B,sBAAsB,KAAK;IAC3B,2BAA2B,MAAM;IACjC,0BAA0B,MAAM;IAChC,wBAAwB,MAAM;IAC9B,0BAA0B,MAAM;IAChC,sBAAsB,MAAM;IAC5B,qBAAqB,MAAM;IAC3B,UAAU,OAAO;CAClB;AACD,MAAM,MAAM,aAAa,GAAG,CAC1B,KAAK,EAAE,UAAU,CAAC,aAAa,EAC/B,QAAQ,EAAE,UAAU,CAAC,iBAAiB,EAAE,KACrC,IAAI,CAAA;AAQT,qBAAa,eAAe;IAC1B,OAAO,CAAC,GAAG,CAAgB;IAC3B,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,mBAAmB,CAAqC;gBAEpD,GAAG,EAAE,UAAU,CAAC,GAAG;IAOxB,QAAQ,CACb,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,qBAAqB,EAC/B,QAAQ,EAAE,aAAa;IAOlB,UAAU,CAAC,OAAO,KAAA;IAKzB,OAAO,CAAC,cAAc;IA4BtB,OAAO;CAIR"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
var MapLayerClickPriority = /* @__PURE__ */ ((MapLayerClickPriority2) => {
|
|
2
|
+
MapLayerClickPriority2[MapLayerClickPriority2["SiteLayerClickPriority"] = 50] = "SiteLayerClickPriority";
|
|
2
3
|
MapLayerClickPriority2[MapLayerClickPriority2["BuildingLayersClickPriority"] = 100] = "BuildingLayersClickPriority";
|
|
3
4
|
MapLayerClickPriority2[MapLayerClickPriority2["CommentLayersClickPriority"] = 200] = "CommentLayersClickPriority";
|
|
4
5
|
MapLayerClickPriority2[MapLayerClickPriority2["MartinLayerClickPriority"] = 300] = "MartinLayerClickPriority";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../src/core/components/viewers/map/utils/MapEventManager/MapClickManager.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\n// MapClickManager.ts\r\nimport type maplibregl from 'maplibre-gl'\r\n\r\n/**\r\n * This is very important. Declare a new priority for your layer in this enum if you need to,\r\n * otherwise, just use the one that has been declared\r\n * Higher priority is clicked first.\r\n */\r\nexport enum MapLayerClickPriority {\r\n BuildingLayersClickPriority = 100,\r\n CommentLayersClickPriority = 200,\r\n MartinLayerClickPriority = 300,\r\n OpenDataLayerClickPriority = 350,\r\n FileLayerCLickPriority = 400,\r\n BimModelLayerPriority = 600,\r\n ActiveTool = 1000, // SPECIAL ONE, MAKE SURE TO DISABLE THE TOOL WHEN YOU ARE FINISH.\r\n}\r\nexport type ClickCallback = (\r\n event: maplibregl.MapMouseEvent,\r\n features: maplibregl.MapGeoJSONFeature[],\r\n) => void\r\n\r\ninterface LayerClickHandler {\r\n layerId: string\r\n priority: number\r\n callback: ClickCallback\r\n}\r\n\r\nexport class MapClickManager {\r\n private map: maplibregl.Map // the instance of the map\r\n private clickHandlers: LayerClickHandler[] // all click handlers for layers that has been registered\r\n private boundedClickHandler: (e: maplibregl.MapMouseEvent) => {} // clickHandler has \"this\" object binded to it, so inside the handleClick private function, we can still access this.map.\r\n\r\n constructor(map: maplibregl.Map) {\r\n this.map = map\r\n this.boundedClickHandler = this.handleMapClick.bind(this)\r\n this.clickHandlers = []\r\n this.map.on('click', this.boundedClickHandler)\r\n }\r\n\r\n public register(\r\n layerId: string,\r\n priority: MapLayerClickPriority,\r\n callback: ClickCallback,\r\n ) {\r\n this.clickHandlers.push({ layerId, priority, callback })\r\n this.clickHandlers.sort((a, b) => b.priority - a.priority)\r\n // console.log(this.clickHandlers)\r\n }\r\n\r\n public unregister(layerId) {\r\n this.clickHandlers = this.clickHandlers.filter(handler => handler.layerId !== layerId)\r\n // console.log(this.clickHandlers)\r\n }\r\n\r\n private handleMapClick(e: maplibregl.MapMouseEvent) {\r\n // for active tools\r\n for (const handler of this.clickHandlers) {\r\n if (handler.priority === MapLayerClickPriority.ActiveTool) {\r\n handler.callback(e, [])\r\n console.log('call back return ')\r\n return\r\n }\r\n }\r\n\r\n // console.log(e.point)\r\n const features = this.map.queryRenderedFeatures(e.point)\r\n // console.log(features)\r\n\r\n if (!features) return\r\n\r\n for (let i = 0; i < this.clickHandlers.length; i++) {\r\n const currentHandler = this.clickHandlers[i]\r\n const hits = features.filter(f => f.layer?.id == currentHandler.layerId)\r\n\r\n if (hits.length > 0) {\r\n // console.log(currentHandler)\r\n currentHandler.callback(e, hits)\r\n break\r\n }\r\n }\r\n }\r\n\r\n destroy() {\r\n this.map.off('click', this.boundedClickHandler)\r\n this.clickHandlers = []\r\n }\r\n}\r\n"],"mappings":"AAWO,IAAK,wBAAL,kBAAKA,2BAAL;
|
|
1
|
+
{"version":3,"sources":["../../../../../../../src/core/components/viewers/map/utils/MapEventManager/MapClickManager.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\n// MapClickManager.ts\r\nimport type maplibregl from 'maplibre-gl'\r\n\r\n/**\r\n * This is very important. Declare a new priority for your layer in this enum if you need to,\r\n * otherwise, just use the one that has been declared\r\n * Higher priority is clicked first.\r\n */\r\nexport enum MapLayerClickPriority {\r\n // Lowest priority: a site polygon only handles a click when no other\r\n // interactive layer (buildings, comments, sensors, files, ...) was hit.\r\n SiteLayerClickPriority = 50,\r\n BuildingLayersClickPriority = 100,\r\n CommentLayersClickPriority = 200,\r\n MartinLayerClickPriority = 300,\r\n OpenDataLayerClickPriority = 350,\r\n FileLayerCLickPriority = 400,\r\n BimModelLayerPriority = 600,\r\n ActiveTool = 1000, // SPECIAL ONE, MAKE SURE TO DISABLE THE TOOL WHEN YOU ARE FINISH.\r\n}\r\nexport type ClickCallback = (\r\n event: maplibregl.MapMouseEvent,\r\n features: maplibregl.MapGeoJSONFeature[],\r\n) => void\r\n\r\ninterface LayerClickHandler {\r\n layerId: string\r\n priority: number\r\n callback: ClickCallback\r\n}\r\n\r\nexport class MapClickManager {\r\n private map: maplibregl.Map // the instance of the map\r\n private clickHandlers: LayerClickHandler[] // all click handlers for layers that has been registered\r\n private boundedClickHandler: (e: maplibregl.MapMouseEvent) => {} // clickHandler has \"this\" object binded to it, so inside the handleClick private function, we can still access this.map.\r\n\r\n constructor(map: maplibregl.Map) {\r\n this.map = map\r\n this.boundedClickHandler = this.handleMapClick.bind(this)\r\n this.clickHandlers = []\r\n this.map.on('click', this.boundedClickHandler)\r\n }\r\n\r\n public register(\r\n layerId: string,\r\n priority: MapLayerClickPriority,\r\n callback: ClickCallback,\r\n ) {\r\n this.clickHandlers.push({ layerId, priority, callback })\r\n this.clickHandlers.sort((a, b) => b.priority - a.priority)\r\n // console.log(this.clickHandlers)\r\n }\r\n\r\n public unregister(layerId) {\r\n this.clickHandlers = this.clickHandlers.filter(handler => handler.layerId !== layerId)\r\n // console.log(this.clickHandlers)\r\n }\r\n\r\n private handleMapClick(e: maplibregl.MapMouseEvent) {\r\n // for active tools\r\n for (const handler of this.clickHandlers) {\r\n if (handler.priority === MapLayerClickPriority.ActiveTool) {\r\n handler.callback(e, [])\r\n console.log('call back return ')\r\n return\r\n }\r\n }\r\n\r\n // console.log(e.point)\r\n const features = this.map.queryRenderedFeatures(e.point)\r\n // console.log(features)\r\n\r\n if (!features) return\r\n\r\n for (let i = 0; i < this.clickHandlers.length; i++) {\r\n const currentHandler = this.clickHandlers[i]\r\n const hits = features.filter(f => f.layer?.id == currentHandler.layerId)\r\n\r\n if (hits.length > 0) {\r\n // console.log(currentHandler)\r\n currentHandler.callback(e, hits)\r\n break\r\n }\r\n }\r\n }\r\n\r\n destroy() {\r\n this.map.off('click', this.boundedClickHandler)\r\n this.clickHandlers = []\r\n }\r\n}\r\n"],"mappings":"AAWO,IAAK,wBAAL,kBAAKA,2BAAL;AAGL,EAAAA,8CAAA,4BAAyB,MAAzB;AACA,EAAAA,8CAAA,iCAA8B,OAA9B;AACA,EAAAA,8CAAA,gCAA6B,OAA7B;AACA,EAAAA,8CAAA,8BAA2B,OAA3B;AACA,EAAAA,8CAAA,gCAA6B,OAA7B;AACA,EAAAA,8CAAA,4BAAyB,OAAzB;AACA,EAAAA,8CAAA,2BAAwB,OAAxB;AACA,EAAAA,8CAAA,gBAAa,OAAb;AAVU,SAAAA;AAAA,GAAA;AAuBL,MAAM,gBAAgB;AAAA;AAAA,EAK3B,YAAY,KAAqB;AAC/B,SAAK,MAAM;AACX,SAAK,sBAAsB,KAAK,eAAe,KAAK,IAAI;AACxD,SAAK,gBAAgB,CAAC;AACtB,SAAK,IAAI,GAAG,SAAS,KAAK,mBAAmB;AAAA,EAC/C;AAAA,EAEO,SACL,SACA,UACA,UACA;AACA,SAAK,cAAc,KAAK,EAAE,SAAS,UAAU,SAAS,CAAC;AACvD,SAAK,cAAc,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAAA,EAE3D;AAAA,EAEO,WAAW,SAAS;AACzB,SAAK,gBAAgB,KAAK,cAAc,OAAO,aAAW,QAAQ,YAAY,OAAO;AAAA,EAEvF;AAAA,EAEQ,eAAe,GAA6B;AAElD,eAAW,WAAW,KAAK,eAAe;AACxC,UAAI,QAAQ,aAAa,sBAAkC;AACzD,gBAAQ,SAAS,GAAG,CAAC,CAAC;AACtB,gBAAQ,IAAI,mBAAmB;AAC/B;AAAA,MACF;AAAA,IACF;AAGA,UAAM,WAAW,KAAK,IAAI,sBAAsB,EAAE,KAAK;AAGvD,QAAI,CAAC,SAAU;AAEf,aAAS,IAAI,GAAG,IAAI,KAAK,cAAc,QAAQ,KAAK;AAClD,YAAM,iBAAiB,KAAK,cAAc,CAAC;AAC3C,YAAM,OAAO,SAAS,OAAO,OAAE;AA/ErC;AA+EwC,wBAAE,UAAF,mBAAS,OAAM,eAAe;AAAA,OAAO;AAEvE,UAAI,KAAK,SAAS,GAAG;AAEnB,uBAAe,SAAS,GAAG,IAAI;AAC/B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU;AACR,SAAK,IAAI,IAAI,SAAS,KAAK,mBAAmB;AAC9C,SAAK,gBAAgB,CAAC;AAAA,EACxB;AACF;","names":["MapLayerClickPriority"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAA;AA0BzB,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,mBAAmB,GAAG,YAAY,CAAA;AAElH,wBAAgB,sBAAsB,CAAC,EAAE,gBAAgB,EAAE,EAAE;IAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,qBAsXzF"}
|
|
@@ -31,7 +31,6 @@ import { useFilesByBuildingId } from "../../../../../hooks/files/files";
|
|
|
31
31
|
import { cn } from "../../../../../utils/utils";
|
|
32
32
|
import { useSearchParams, useRouter } from "next/navigation";
|
|
33
33
|
import { useBuilding, useBuildings } from "../../../../../hooks/buildings/buildings";
|
|
34
|
-
import { toast } from "sonner";
|
|
35
34
|
function PointCloudLoadingState({ pointcloudApiUrl }) {
|
|
36
35
|
const t = useTranslations("PointCloudLoadingState");
|
|
37
36
|
const API_BASE = pointcloudApiUrl != null ? pointcloudApiUrl : "http://localhost:5101";
|
|
@@ -124,6 +123,42 @@ function PointCloudLoadingState({ pointcloudApiUrl }) {
|
|
|
124
123
|
setSearchResults([]);
|
|
125
124
|
router.push(url.toString());
|
|
126
125
|
};
|
|
126
|
+
async function createPointCloud(name, buildingId2) {
|
|
127
|
+
const res = await fetch("/api/point-cloud", {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "Content-Type": "application/json" },
|
|
130
|
+
body: JSON.stringify({ name, buildingId: buildingId2 })
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
const msg = await res.text();
|
|
134
|
+
throw new Error(`Create point-cloud failed: ${msg || res.status}`);
|
|
135
|
+
}
|
|
136
|
+
return res.json();
|
|
137
|
+
}
|
|
138
|
+
function uploadToPresignedUrl(url, file, onProgress) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
try {
|
|
141
|
+
const xhr = new XMLHttpRequest();
|
|
142
|
+
xhr.open("PUT", url, true);
|
|
143
|
+
if (onProgress) {
|
|
144
|
+
xhr.upload.onprogress = (evt) => {
|
|
145
|
+
if (evt.lengthComputable) {
|
|
146
|
+
const pct = Math.round(evt.loaded / evt.total * 100);
|
|
147
|
+
onProgress(pct);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
xhr.onload = () => {
|
|
152
|
+
if (xhr.status >= 200 && xhr.status < 300) resolve();
|
|
153
|
+
else reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
154
|
+
};
|
|
155
|
+
xhr.onerror = () => reject(new Error("Network error during upload"));
|
|
156
|
+
xhr.send(file);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
reject(err);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
127
162
|
const handleFileChange = async (event) => {
|
|
128
163
|
const files2 = event.target.files;
|
|
129
164
|
if (!files2 || files2.length === 0) return;
|
|
@@ -134,39 +169,27 @@ function PointCloudLoadingState({ pointcloudApiUrl }) {
|
|
|
134
169
|
alert("Please select a LAZ or LAS file.");
|
|
135
170
|
return;
|
|
136
171
|
}
|
|
137
|
-
if (!(building == null ? void 0 : building.id)) {
|
|
138
|
-
alert("No building selected.");
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
172
|
setUploading(true);
|
|
142
173
|
try {
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
const { presignedUrl, key } = await presignedRes.json();
|
|
150
|
-
const uploadRes = await fetch(presignedUrl, {
|
|
151
|
-
method: "PUT",
|
|
152
|
-
body: file,
|
|
153
|
-
headers: { "Content-Type": "application/octet-stream" }
|
|
174
|
+
console.log("Creating point cloud entry...");
|
|
175
|
+
const { pointCloud, upload } = await createPointCloud(fileName, building.id);
|
|
176
|
+
console.log("Point cloud created:", pointCloud.id);
|
|
177
|
+
console.log("Uploading file...");
|
|
178
|
+
await uploadToPresignedUrl(upload.uploadUrl, file, (pct) => {
|
|
179
|
+
console.log(`Upload progress: ${pct}%`);
|
|
154
180
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
});
|
|
168
|
-
if (!fileRes.ok) throw new Error("Failed to create file record");
|
|
169
|
-
toast.success("LAZ file uploaded successfully", { duration: 6e3 });
|
|
181
|
+
console.log("Upload complete!");
|
|
182
|
+
console.log("Starting conversion...");
|
|
183
|
+
const convertRes = await fetch(
|
|
184
|
+
`${API_BASE}/point-cloud/${encodeURIComponent(String(pointCloud.id))}/convert-to-potree`,
|
|
185
|
+
{
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: { "Content-Type": "application/json" },
|
|
188
|
+
body: JSON.stringify({ options: { sampling_method: "poisson" } })
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
if (!convertRes.ok) throw new Error("Failed to start conversion");
|
|
192
|
+
console.log("Conversion started!");
|
|
170
193
|
window.location.reload();
|
|
171
194
|
} catch (error) {
|
|
172
195
|
console.error("Error uploading point cloud:", error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../src/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.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 React from 'react'\r\nimport { Button } from '../../../../ui/Button'\r\nimport { Card, CardContent, CardHeader } from '../../../../ui/Card'\r\nimport { Input } from '../../../../ui/Input'\r\nimport { LoadingSpinner } from '../../../../ui/LoadingSpinner'\r\nimport * as LR from 'lucide-react'\r\nimport { BuildingsContext, usePermissions } from '../../../../../store'\r\n// import { useAppConfigContext } from '../../../../../store/AppConfig/context'\r\nimport { useTranslations } from 'next-intl'\r\nimport { useFilesByBuildingId } from '../../../../../hooks/files/files'\r\nimport type { DbFile } from '../../../../../types/dbTypes'\r\nimport { cn } from '../../../../../utils/utils'\r\nimport { useSearchParams, useRouter } from 'next/navigation'\r\nimport { useBuilding, useBuildings } from '../../../../../hooks/buildings/buildings'\r\nimport { SquareArrowOutUpRight } from 'lucide-react'\r\nimport { toast } from 'sonner'\r\n\r\ntype PresignedUploadResponse = {\r\n presignedUrl: string\r\n key: string\r\n}\r\n\r\nexport type pointCloudViewerState = 'opening' | 'loading' | 'ready' | 'error' | 'noPointCloudFiles' | 'noBuilding'\r\n\r\nexport function PointCloudLoadingState({ pointcloudApiUrl }: { pointcloudApiUrl?: string }) {\r\n const t = useTranslations('PointCloudLoadingState')\r\n const API_BASE = pointcloudApiUrl ?? 'http://localhost:5101'\r\n // Permissions\r\n const { ability } = usePermissions()\r\n\r\n const { state: buildingState, dispatch: buildingDispatch } = React.useContext(BuildingsContext)\r\n const { building: storeBuilding } = buildingState.buildings\r\n\r\n const searchParams = useSearchParams()\r\n const buildingId = searchParams.get(\"buildingId\")\r\n const { building: urlBuilding, isLoading: buildingLoading, isError: buildingError } = useBuilding(buildingId ? Number(buildingId) : 0)\r\n const { buildings, isLoading: buildingsLoading } = useBuildings()\r\n\r\n // Determine the current building to use\r\n const building = storeBuilding || urlBuilding\r\n\r\n React.useEffect(() => {\r\n if (!storeBuilding && urlBuilding) {\r\n buildingDispatch({ type: \"SET-CURRENT-BUILDING\", payload: { building: urlBuilding } })\r\n }\r\n }, [urlBuilding, storeBuilding, buildingDispatch])\r\n\r\n const router = useRouter()\r\n const fileInputRef = React.useRef<HTMLInputElement>(null)\r\n\r\n const [currentState, setCurrentState] = React.useState<pointCloudViewerState>('opening')\r\n const [uploading, setUploading] = React.useState(false)\r\n const [searchTerm, setSearchTerm] = React.useState('')\r\n const [searchResults, setSearchResults] = React.useState<any[]>([])\r\n const [selectedBuilding, setSelectedBuilding] = React.useState<any>(null)\r\n \r\n const { files, isLoading: filesLoading } = useFilesByBuildingId(building?.id)\r\n const pointCloudFiles: DbFile[] = React.useMemo(() => (\r\n files.filter(file => {\r\n const ext = file.extension?.toLowerCase()\r\n if (!ext) return false\r\n return ext === \"laz\" || ext === \"las\"\r\n })\r\n ), [files])\r\n\r\n // Fuzzy search function for buildings\r\n const fuzzySearchBuildings = React.useCallback((buildings: any[], searchTerm: string) => {\r\n if (!searchTerm.trim()) return buildings.slice(0, 10)\r\n \r\n const fuzzySearchScore = (search: string, text: string): number => {\r\n if (!search || !text) return 0\r\n const searchLower = search.toLowerCase()\r\n const textLower = text.toLowerCase()\r\n \r\n if (textLower === searchLower) return 1000\r\n if (textLower.startsWith(searchLower)) return 800\r\n if (textLower.includes(searchLower)) return 600\r\n \r\n return 0\r\n }\r\n\r\n return buildings\r\n .map((building) => {\r\n const address = building.buildingAddress || ''\r\n const name = building.buildingName || ''\r\n const id = building.id.toString()\r\n \r\n const addressScore = fuzzySearchScore(searchTerm, address)\r\n const nameScore = fuzzySearchScore(searchTerm, name)\r\n const idScore = fuzzySearchScore(searchTerm, id)\r\n \r\n const maxScore = Math.max(addressScore, nameScore, idScore)\r\n return { ...building, searchScore: maxScore }\r\n })\r\n .filter(building => building.searchScore > 0)\r\n .sort((a, b) => b.searchScore - a.searchScore)\r\n .slice(0, 10)\r\n }, [])\r\n\r\n // Handle search\r\n const handleSearch = React.useCallback((term: string) => {\r\n setSearchTerm(term)\r\n if (buildings) {\r\n setSearchResults(fuzzySearchBuildings(buildings, term))\r\n }\r\n }, [buildings, fuzzySearchBuildings])\r\n\r\n const shouldShow = (currentState === 'opening' || currentState === 'noPointCloudFiles' || currentState === 'noBuilding')\r\n\r\n // Handle initial state transitions\r\n React.useEffect(() => {\r\n // If building is loading, stay in opening state\r\n if (buildingLoading) {\r\n setCurrentState('opening')\r\n return\r\n }\r\n\r\n // If building failed to load or no buildingId provided, show noBuilding state\r\n if (buildingError || (!buildingId && !building)) {\r\n setCurrentState('noBuilding')\r\n return\r\n }\r\n\r\n // If we have a building but files are still loading, stay in opening state\r\n if (building && filesLoading) {\r\n setCurrentState('opening')\r\n return\r\n }\r\n\r\n // If we have a building but no point cloud files, show noPointCloudFiles state\r\n if (building && pointCloudFiles.length === 0) {\r\n setCurrentState('noPointCloudFiles')\r\n return\r\n }\r\n\r\n // We have point cloud files, hide the loading state\r\n if (building && pointCloudFiles.length > 0) {\r\n setCurrentState('ready')\r\n }\r\n }, [building, buildingLoading, buildingError, buildingId, filesLoading, pointCloudFiles.length])\r\n\r\n // Don't render if not visible\r\n if (!shouldShow) {\r\n return null\r\n }\r\n\r\n const handleBuildingSelect = (building: any) => {\r\n const url = new URL(window.location.href)\r\n url.searchParams.set('buildingId', building.id.toString())\r\n buildingDispatch({ type: 'SET-CURRENT-BUILDING', payload: { building } })\r\n setSelectedBuilding(building)\r\n setSearchTerm('')\r\n setSearchResults([])\r\n router.push(url.toString())\r\n }\r\n\r\n const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {\r\n const files = event.target.files\r\n if (!files || files.length === 0) return\r\n\r\n const file = files[0]\r\n const fileName = file.name\r\n const fileExt = fileName.toLowerCase()\r\n\r\n if (!fileExt.endsWith('.laz') && !fileExt.endsWith('.las')) {\r\n alert('Please select a LAZ or LAS file.')\r\n return\r\n }\r\n\r\n if (!building?.id) {\r\n alert('No building selected.')\r\n return\r\n }\r\n\r\n setUploading(true)\r\n\r\n try {\r\n const assetId = crypto.randomUUID()\r\n const assetName = `${assetId}.laz`\r\n\r\n // 1. Get presigned upload URL\r\n const presignedRes = await fetch(\r\n `/api/presigned-url-upload?asset=${encodeURIComponent(assetName)}&bucket=pointclouds-demo`\r\n )\r\n if (!presignedRes.ok) throw new Error('Failed to get upload URL')\r\n const { presignedUrl, key }: PresignedUploadResponse = await presignedRes.json()\r\n\r\n // 2. Upload LAZ directly to MinIO\r\n const uploadRes = await fetch(presignedUrl, {\r\n method: 'PUT',\r\n body: file,\r\n headers: { 'Content-Type': 'application/octet-stream' },\r\n })\r\n if (!uploadRes.ok) throw new Error(`Upload failed: ${uploadRes.status}`)\r\n\r\n // 3. Create file record attached to the building\r\n const fileRes = await fetch(`/api/files/building/${building.id}`, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({\r\n type: 'point-cloud-file',\r\n name: fileName,\r\n assetId,\r\n lazFileKey: key,\r\n bucket: 'pointclouds-demo',\r\n extension: fileExt.endsWith('.las') ? 'las' : 'laz',\r\n }),\r\n })\r\n if (!fileRes.ok) throw new Error('Failed to create file record')\r\n\r\n toast.success('LAZ file uploaded successfully', { duration: 6000 })\r\n window.location.reload()\r\n }\r\n catch (error) {\r\n console.error('Error uploading point cloud:', error)\r\n alert(\r\n error instanceof Error\r\n ? `Failed to upload: ${error.message}`\r\n : 'Failed to upload point cloud'\r\n )\r\n setUploading(false)\r\n }\r\n }\r\n\r\n const handleUploadClick = () => {\r\n fileInputRef.current?.click()\r\n }\r\n\r\n return (\r\n <>\r\n <div className=\"absolute z-10 inset-0 bg-background/50 backdrop-blur-[1px] flex items-center justify-center pointer-events-none\">\r\n <Card className={cn(\"w-[536px] pointer-events-auto flex flex-col justify-start items-center gap-2\")}>\r\n <CardHeader className=\"flex justify-center items-center\">\r\n <div className=\"w-12 h-12 p-2 bg-card rounded-md shadow-sm border border-border flex justify-center items-center\">\r\n {currentState === 'noBuilding' ? (\r\n <LR.Building2 className={cn(\"w-4 h-5 text-foreground\", buildingsLoading && \"animate-pulse\")} strokeWidth={2} />\r\n ) : (\r\n <LR.Grip className={cn(\"w-4 h-5 text-foreground\", currentState === 'opening' && \"animate-pulse\")} strokeWidth={2} />\r\n )}\r\n </div>\r\n </CardHeader>\r\n\r\n <CardContent className=\"self-stretch flex flex-col justify-start items-center gap-6 pt-0\">\r\n <div className=\"self-stretch flex flex-col justify-start items-center gap-2\">\r\n <div className=\"self-stretch text-center text-foreground text-xl font-semibold font-['Inter'] leading-7 flex items-center justify-center gap-2\">\r\n {currentState === 'opening' ? (\r\n <>\r\n {t('openingText')}\r\n <LoadingSpinner size={20} />\r\n </>\r\n ) : currentState === 'noBuilding' ? (\r\n <>\r\n {t('noBuilding')}\r\n </>\r\n ) : uploading ? (\r\n <>\r\n {t('uploading')}\r\n <LoadingSpinner size={20} />\r\n </>\r\n ) : (\r\n t('noPointCloudLoaded')\r\n )}\r\n </div>\r\n {(currentState === 'noPointCloudFiles' || currentState === 'noBuilding') && !uploading && (\r\n <div className=\"self-stretch text-center text-muted-foreground text-sm font-normal font-['Inter'] leading-tight\">\r\n {currentState === 'noBuilding' ? t('selectBuildingHelpText') : t('helpText')}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {currentState === 'noBuilding' && (\r\n <div className=\"self-stretch flex flex-col gap-3 justify-centre items-center\">\r\n {buildingsLoading ? (\r\n <div className=\"flex items-center justify-center gap-2 text-muted-foreground\">\r\n <LoadingSpinner size={16} />\r\n <span>{t('loadingBuildings') || 'Loading buildings...'}</span>\r\n </div>\r\n ) : buildings && buildings.length > 0 ? (\r\n <div className=\"w-full relative\">\r\n {selectedBuilding ? (\r\n <div className=\"relative flex justify-end items-center\">\r\n <Input\r\n type=\"text\"\r\n value={\r\n 'Building: '\r\n + (selectedBuilding.buildingName && selectedBuilding.buildingAddress\r\n ? `${selectedBuilding.buildingName}, ${selectedBuilding.buildingAddress}`\r\n : selectedBuilding.buildingName || selectedBuilding.buildingAddress || `Building ${selectedBuilding.id}`)\r\n }\r\n readOnly\r\n className=\"w-full\"\r\n />\r\n <LR.X \r\n className=\"absolute right-2 cursor-pointer w-4 h-4 text-muted-foreground hover:text-foreground\" \r\n onClick={() => {\r\n setSelectedBuilding(null)\r\n setSearchTerm('')\r\n setSearchResults([])\r\n }} \r\n />\r\n </div>\r\n ) : (\r\n <div className=\"relative\">\r\n <LR.Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4\" />\r\n <Input\r\n placeholder={t('searchBuildings') || 'Search buildings...'}\r\n value={searchTerm}\r\n onChange={(e) => handleSearch(e.target.value)}\r\n className=\"w-full pl-10\"\r\n disabled={!ability.can('read', 'File')}\r\n />\r\n </div>\r\n )}\r\n \r\n {/* Search results dropdown */}\r\n {searchResults.length > 0 && searchTerm !== '' && !selectedBuilding && (\r\n <div className=\"absolute top-full left-0 right-0 mt-1 bg-background border border-border rounded-md shadow-lg max-h-60 overflow-y-auto z-20\">\r\n {searchResults.map(building => (\r\n <div\r\n key={building.id}\r\n className=\"px-3 py-2 text-sm cursor-pointer hover:bg-muted transition-colors flex gap-2 items-center\"\r\n onClick={() => handleBuildingSelect(building)}\r\n >\r\n <LR.SquareArrowOutUpRight className=\"w-3 h-3\" />\r\n {building.buildingName\r\n ? `${building.buildingName}, ${building.buildingAddress}`\r\n : building.buildingAddress || `Building ${building.id}`}\r\n </div>\r\n ))}\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"text-muted-foreground text-center\">\r\n {t('noBuildings') || 'No buildings available'}\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n\r\n {currentState === 'noPointCloudFiles' && (\r\n <div className=\"self-stretch flex justify-center items-center gap-3\">\r\n <Input\r\n ref={fileInputRef}\r\n type=\"file\"\r\n accept=\".laz,.las\"\r\n onChange={handleFileChange}\r\n className=\"hidden\"\r\n />\r\n <Button\r\n onClick={handleUploadClick}\r\n size=\"sm\"\r\n className=\"gap-2\"\r\n disabled={uploading || !ability.can('create', 'File')}\r\n >\r\n <LR.Upload className=\"w-4 h-4\" />\r\n {uploading ? t('uploading') : t('uploadButton')}\r\n </Button>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </>\r\n )\r\n}\r\n"],"mappings":"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","names":["buildings","searchTerm","building","files"]}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../src/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.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 React from 'react'\r\nimport { Button } from '../../../../ui/Button'\r\nimport { Card, CardContent, CardHeader } from '../../../../ui/Card'\r\nimport { Input } from '../../../../ui/Input'\r\nimport { LoadingSpinner } from '../../../../ui/LoadingSpinner'\r\nimport * as LR from 'lucide-react'\r\nimport { BuildingsContext, usePermissions } from '../../../../../store'\r\n// import { useAppConfigContext } from '../../../../../store/AppConfig/context'\r\nimport { useTranslations } from 'next-intl'\r\nimport { useFilesByBuildingId } from '../../../../../hooks/files/files'\r\nimport type { DbFile } from '../../../../../types/dbTypes'\r\nimport { cn } from '../../../../../utils/utils'\r\nimport { useSearchParams, useRouter } from 'next/navigation'\r\nimport { useBuilding, useBuildings } from '../../../../../hooks/buildings/buildings'\r\nimport { SquareArrowOutUpRight } from 'lucide-react'\r\n\r\ntype CreatePointCloudResponse = {\r\n pointCloud: {\r\n id: string\r\n name: string\r\n }\r\n upload: {\r\n uploadUrl: string\r\n }\r\n}\r\n\r\nexport type pointCloudViewerState = 'opening' | 'loading' | 'ready' | 'error' | 'noPointCloudFiles' | 'noBuilding'\r\n\r\nexport function PointCloudLoadingState({ pointcloudApiUrl }: { pointcloudApiUrl?: string }) {\r\n const t = useTranslations('PointCloudLoadingState')\r\n const API_BASE = pointcloudApiUrl ?? 'http://localhost:5101'\r\n // Permissions\r\n const { ability } = usePermissions()\r\n\r\n const { state: buildingState, dispatch: buildingDispatch } = React.useContext(BuildingsContext)\r\n const { building: storeBuilding } = buildingState.buildings\r\n\r\n const searchParams = useSearchParams()\r\n const buildingId = searchParams.get(\"buildingId\")\r\n const { building: urlBuilding, isLoading: buildingLoading, isError: buildingError } = useBuilding(buildingId ? Number(buildingId) : 0)\r\n const { buildings, isLoading: buildingsLoading } = useBuildings()\r\n\r\n // Determine the current building to use\r\n const building = storeBuilding || urlBuilding\r\n\r\n React.useEffect(() => {\r\n if (!storeBuilding && urlBuilding) {\r\n buildingDispatch({ type: \"SET-CURRENT-BUILDING\", payload: { building: urlBuilding } })\r\n }\r\n }, [urlBuilding, storeBuilding, buildingDispatch])\r\n\r\n const router = useRouter()\r\n const fileInputRef = React.useRef<HTMLInputElement>(null)\r\n\r\n const [currentState, setCurrentState] = React.useState<pointCloudViewerState>('opening')\r\n const [uploading, setUploading] = React.useState(false)\r\n const [searchTerm, setSearchTerm] = React.useState('')\r\n const [searchResults, setSearchResults] = React.useState<any[]>([])\r\n const [selectedBuilding, setSelectedBuilding] = React.useState<any>(null)\r\n \r\n const { files, isLoading: filesLoading } = useFilesByBuildingId(building?.id)\r\n const pointCloudFiles: DbFile[] = React.useMemo(() => (\r\n files.filter(file => {\r\n const ext = file.extension?.toLowerCase()\r\n if (!ext) return false\r\n return ext === \"laz\" || ext === \"las\"\r\n })\r\n ), [files])\r\n\r\n // Fuzzy search function for buildings\r\n const fuzzySearchBuildings = React.useCallback((buildings: any[], searchTerm: string) => {\r\n if (!searchTerm.trim()) return buildings.slice(0, 10)\r\n \r\n const fuzzySearchScore = (search: string, text: string): number => {\r\n if (!search || !text) return 0\r\n const searchLower = search.toLowerCase()\r\n const textLower = text.toLowerCase()\r\n \r\n if (textLower === searchLower) return 1000\r\n if (textLower.startsWith(searchLower)) return 800\r\n if (textLower.includes(searchLower)) return 600\r\n \r\n return 0\r\n }\r\n\r\n return buildings\r\n .map((building) => {\r\n const address = building.buildingAddress || ''\r\n const name = building.buildingName || ''\r\n const id = building.id.toString()\r\n \r\n const addressScore = fuzzySearchScore(searchTerm, address)\r\n const nameScore = fuzzySearchScore(searchTerm, name)\r\n const idScore = fuzzySearchScore(searchTerm, id)\r\n \r\n const maxScore = Math.max(addressScore, nameScore, idScore)\r\n return { ...building, searchScore: maxScore }\r\n })\r\n .filter(building => building.searchScore > 0)\r\n .sort((a, b) => b.searchScore - a.searchScore)\r\n .slice(0, 10)\r\n }, [])\r\n\r\n // Handle search\r\n const handleSearch = React.useCallback((term: string) => {\r\n setSearchTerm(term)\r\n if (buildings) {\r\n setSearchResults(fuzzySearchBuildings(buildings, term))\r\n }\r\n }, [buildings, fuzzySearchBuildings])\r\n\r\n const shouldShow = (currentState === 'opening' || currentState === 'noPointCloudFiles' || currentState === 'noBuilding')\r\n\r\n // Handle initial state transitions\r\n React.useEffect(() => {\r\n // If building is loading, stay in opening state\r\n if (buildingLoading) {\r\n setCurrentState('opening')\r\n return\r\n }\r\n\r\n // If building failed to load or no buildingId provided, show noBuilding state\r\n if (buildingError || (!buildingId && !building)) {\r\n setCurrentState('noBuilding')\r\n return\r\n }\r\n\r\n // If we have a building but files are still loading, stay in opening state\r\n if (building && filesLoading) {\r\n setCurrentState('opening')\r\n return\r\n }\r\n\r\n // If we have a building but no point cloud files, show noPointCloudFiles state\r\n if (building && pointCloudFiles.length === 0) {\r\n setCurrentState('noPointCloudFiles')\r\n return\r\n }\r\n\r\n // We have point cloud files, hide the loading state\r\n if (building && pointCloudFiles.length > 0) {\r\n setCurrentState('ready')\r\n }\r\n }, [building, buildingLoading, buildingError, buildingId, filesLoading, pointCloudFiles.length])\r\n\r\n // Don't render if not visible\r\n if (!shouldShow) {\r\n return null\r\n }\r\n\r\n const handleBuildingSelect = (building: any) => {\r\n const url = new URL(window.location.href)\r\n url.searchParams.set('buildingId', building.id.toString())\r\n buildingDispatch({ type: 'SET-CURRENT-BUILDING', payload: { building } })\r\n setSelectedBuilding(building)\r\n setSearchTerm('')\r\n setSearchResults([])\r\n router.push(url.toString())\r\n }\r\n\r\n // Create point cloud entry\r\n async function createPointCloud(name: string, buildingId: number): Promise<CreatePointCloudResponse> {\r\n const res = await fetch('/api/point-cloud', {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ name, buildingId }),\r\n })\r\n\r\n if (!res.ok) {\r\n const msg = await res.text()\r\n throw new Error(`Create point-cloud failed: ${msg || res.status}`)\r\n }\r\n\r\n return res.json()\r\n }\r\n\r\n // Upload to presigned URL\r\n function uploadToPresignedUrl(\r\n url: string,\r\n file: File,\r\n onProgress?: (pct: number) => void\r\n ): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n try {\r\n const xhr = new XMLHttpRequest()\r\n xhr.open('PUT', url, true)\r\n\r\n if (onProgress) {\r\n xhr.upload.onprogress = (evt) => {\r\n if (evt.lengthComputable) {\r\n const pct = Math.round((evt.loaded / evt.total) * 100)\r\n onProgress(pct)\r\n }\r\n }\r\n }\r\n\r\n xhr.onload = () => {\r\n if (xhr.status >= 200 && xhr.status < 300) resolve()\r\n else reject(new Error(`Upload failed with status ${xhr.status}`))\r\n }\r\n\r\n xhr.onerror = () => reject(new Error('Network error during upload'))\r\n xhr.send(file)\r\n } catch (err) {\r\n reject(err)\r\n }\r\n })\r\n }\r\n\r\n const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {\r\n const files = event.target.files\r\n if (!files || files.length === 0) return\r\n\r\n const file = files[0]\r\n const fileName = file.name\r\n const fileExt = fileName.toLowerCase()\r\n\r\n // Validate file extension\r\n if (!fileExt.endsWith('.laz') && !fileExt.endsWith('.las')) {\r\n alert('Please select a LAZ or LAS file.')\r\n return\r\n }\r\n\r\n setUploading(true)\r\n\r\n try {\r\n console.log('Creating point cloud entry...')\r\n const { pointCloud, upload } = await createPointCloud(fileName, building.id)\r\n console.log('Point cloud created:', pointCloud.id)\r\n\r\n console.log('Uploading file...')\r\n await uploadToPresignedUrl(upload.uploadUrl, file, (pct) => {\r\n console.log(`Upload progress: ${pct}%`)\r\n })\r\n console.log('Upload complete!')\r\n\r\n console.log('Starting conversion...')\r\n const convertRes = await fetch(\r\n `${API_BASE}/point-cloud/${encodeURIComponent(String(pointCloud.id))}/convert-to-potree`,\r\n {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ options: { sampling_method: 'poisson' } }),\r\n }\r\n )\r\n if (!convertRes.ok) throw new Error('Failed to start conversion')\r\n console.log('Conversion started!')\r\n\r\n window.location.reload()\r\n }\r\n catch (error) {\r\n console.error('Error uploading point cloud:', error)\r\n alert(\r\n error instanceof Error\r\n ? `Failed to upload: ${error.message}`\r\n : 'Failed to upload point cloud'\r\n )\r\n setUploading(false)\r\n }\r\n }\r\n\r\n const handleUploadClick = () => {\r\n fileInputRef.current?.click()\r\n }\r\n\r\n return (\r\n <>\r\n <div className=\"absolute z-10 inset-0 bg-background/50 backdrop-blur-[1px] flex items-center justify-center pointer-events-none\">\r\n <Card className={cn(\"w-[536px] pointer-events-auto flex flex-col justify-start items-center gap-2\")}>\r\n <CardHeader className=\"flex justify-center items-center\">\r\n <div className=\"w-12 h-12 p-2 bg-card rounded-md shadow-sm border border-border flex justify-center items-center\">\r\n {currentState === 'noBuilding' ? (\r\n <LR.Building2 className={cn(\"w-4 h-5 text-foreground\", buildingsLoading && \"animate-pulse\")} strokeWidth={2} />\r\n ) : (\r\n <LR.Grip className={cn(\"w-4 h-5 text-foreground\", currentState === 'opening' && \"animate-pulse\")} strokeWidth={2} />\r\n )}\r\n </div>\r\n </CardHeader>\r\n\r\n <CardContent className=\"self-stretch flex flex-col justify-start items-center gap-6 pt-0\">\r\n <div className=\"self-stretch flex flex-col justify-start items-center gap-2\">\r\n <div className=\"self-stretch text-center text-foreground text-xl font-semibold font-['Inter'] leading-7 flex items-center justify-center gap-2\">\r\n {currentState === 'opening' ? (\r\n <>\r\n {t('openingText')}\r\n <LoadingSpinner size={20} />\r\n </>\r\n ) : currentState === 'noBuilding' ? (\r\n <>\r\n {t('noBuilding')}\r\n </>\r\n ) : uploading ? (\r\n <>\r\n {t('uploading')}\r\n <LoadingSpinner size={20} />\r\n </>\r\n ) : (\r\n t('noPointCloudLoaded')\r\n )}\r\n </div>\r\n {(currentState === 'noPointCloudFiles' || currentState === 'noBuilding') && !uploading && (\r\n <div className=\"self-stretch text-center text-muted-foreground text-sm font-normal font-['Inter'] leading-tight\">\r\n {currentState === 'noBuilding' ? t('selectBuildingHelpText') : t('helpText')}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {currentState === 'noBuilding' && (\r\n <div className=\"self-stretch flex flex-col gap-3 justify-centre items-center\">\r\n {buildingsLoading ? (\r\n <div className=\"flex items-center justify-center gap-2 text-muted-foreground\">\r\n <LoadingSpinner size={16} />\r\n <span>{t('loadingBuildings') || 'Loading buildings...'}</span>\r\n </div>\r\n ) : buildings && buildings.length > 0 ? (\r\n <div className=\"w-full relative\">\r\n {selectedBuilding ? (\r\n <div className=\"relative flex justify-end items-center\">\r\n <Input\r\n type=\"text\"\r\n value={\r\n 'Building: '\r\n + (selectedBuilding.buildingName && selectedBuilding.buildingAddress\r\n ? `${selectedBuilding.buildingName}, ${selectedBuilding.buildingAddress}`\r\n : selectedBuilding.buildingName || selectedBuilding.buildingAddress || `Building ${selectedBuilding.id}`)\r\n }\r\n readOnly\r\n className=\"w-full\"\r\n />\r\n <LR.X \r\n className=\"absolute right-2 cursor-pointer w-4 h-4 text-muted-foreground hover:text-foreground\" \r\n onClick={() => {\r\n setSelectedBuilding(null)\r\n setSearchTerm('')\r\n setSearchResults([])\r\n }} \r\n />\r\n </div>\r\n ) : (\r\n <div className=\"relative\">\r\n <LR.Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4\" />\r\n <Input\r\n placeholder={t('searchBuildings') || 'Search buildings...'}\r\n value={searchTerm}\r\n onChange={(e) => handleSearch(e.target.value)}\r\n className=\"w-full pl-10\"\r\n disabled={!ability.can('read', 'File')}\r\n />\r\n </div>\r\n )}\r\n \r\n {/* Search results dropdown */}\r\n {searchResults.length > 0 && searchTerm !== '' && !selectedBuilding && (\r\n <div className=\"absolute top-full left-0 right-0 mt-1 bg-background border border-border rounded-md shadow-lg max-h-60 overflow-y-auto z-20\">\r\n {searchResults.map(building => (\r\n <div\r\n key={building.id}\r\n className=\"px-3 py-2 text-sm cursor-pointer hover:bg-muted transition-colors flex gap-2 items-center\"\r\n onClick={() => handleBuildingSelect(building)}\r\n >\r\n <LR.SquareArrowOutUpRight className=\"w-3 h-3\" />\r\n {building.buildingName\r\n ? `${building.buildingName}, ${building.buildingAddress}`\r\n : building.buildingAddress || `Building ${building.id}`}\r\n </div>\r\n ))}\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"text-muted-foreground text-center\">\r\n {t('noBuildings') || 'No buildings available'}\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n\r\n {currentState === 'noPointCloudFiles' && (\r\n <div className=\"self-stretch flex justify-center items-center gap-3\">\r\n <Input\r\n ref={fileInputRef}\r\n type=\"file\"\r\n accept=\".laz,.las\"\r\n onChange={handleFileChange}\r\n className=\"hidden\"\r\n />\r\n <Button\r\n onClick={handleUploadClick}\r\n size=\"sm\"\r\n className=\"gap-2\"\r\n disabled={uploading || !ability.can('create', 'File')}\r\n >\r\n <LR.Upload className=\"w-4 h-4\" />\r\n {uploading ? t('uploading') : t('uploadButton')}\r\n </Button>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </>\r\n )\r\n}\r\n"],"mappings":"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","names":["buildings","searchTerm","building","buildingId","files"]}
|
|
@@ -25,7 +25,7 @@ function FileTab({ pointcloudApiUrl }) {
|
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
27
|
return /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col space-y-6 h-full py-4 overflow-hidden", children: [
|
|
28
|
-
/* @__PURE__ */ jsx(PointCloudsSection, { files: pointCloudFiles, pointcloudApiUrl }),
|
|
28
|
+
/* @__PURE__ */ jsx(PointCloudsSection, { files: pointCloudFiles, pointcloudApiUrl, buildingId }),
|
|
29
29
|
/* @__PURE__ */ jsx(FilesSection, { files: nonPointCloudFiles })
|
|
30
30
|
] });
|
|
31
31
|
}
|
package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../../src/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/index.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 { FilesSection } from './src/FilesSection'\r\nimport { PointCloudsSection } from './src/PointCloudSection'\r\nimport { BuildingsContext } from '../../../../../../../store'\r\nimport { useSearchParams } from 'next/navigation'\r\nimport { useFilesByBuildingId } from '../../../../../../../hooks/files/files'\r\nimport { DbFile } from '../../../../../../../types/dbTypes'\r\n\r\nexport function FileTab({ pointcloudApiUrl }: { pointcloudApiUrl?: string }) {\r\n const { state: buildingState } = React.useContext(BuildingsContext)\r\n const { building } = buildingState.buildings\r\n\r\n const urlBuildingId = useSearchParams().get('buildingId')\r\n // Resolve from the store building, else the URL param; undefined if neither is\r\n // ready yet (e.g. mid viewer-switch) — the hook handles undefined → no files.\r\n const buildingId = building?.id ?? (urlBuildingId ? Number(urlBuildingId) : undefined)\r\n\r\n const filesData: DbFile[] = useFilesByBuildingId(buildingId).files || []\r\n\r\n const pointCloudFiles: DbFile[] = []\r\n const nonPointCloudFiles: DbFile[] = []\r\n\r\n // single .map pass to populate both arrays\r\n filesData.map((file) => {\r\n const {extension, type} = file\r\n // const isPointCloud = extension.toLowerCase() === 'laz' || extension.toLowerCase() === 'las'\r\n const isPointCloud = type?.toLowerCase() === 'point-cloud-file'\r\n\r\n if (isPointCloud) {\r\n pointCloudFiles.push(file)\r\n } else {\r\n nonPointCloudFiles.push(file)\r\n }\r\n })\r\n\r\n return (\r\n <div className=\"flex-1 flex flex-col space-y-6 h-full py-4 overflow-hidden\">\r\n <PointCloudsSection files={pointCloudFiles} pointcloudApiUrl={pointcloudApiUrl} />\r\n <FilesSection files={nonPointCloudFiles} />\r\n </div>\r\n )\r\n}\r\n"],"mappings":";AAyCI,SACE,KADF;AApCJ,YAAY,WAAW;AACvB,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,uBAAuB;AAChC,SAAS,4BAA4B;AAG9B,SAAS,QAAQ,EAAE,iBAAiB,GAAkC;AAb7E;AAcE,QAAM,EAAE,OAAO,cAAc,IAAI,MAAM,WAAW,gBAAgB;AAClE,QAAM,EAAE,SAAS,IAAI,cAAc;AAEnC,QAAM,gBAAgB,gBAAgB,EAAE,IAAI,YAAY;AAGxD,QAAM,cAAa,0CAAU,OAAV,YAAiB,gBAAgB,OAAO,aAAa,IAAI;AAE5E,QAAM,YAAsB,qBAAqB,UAAU,EAAE,SAAS,CAAC;AAEvE,QAAM,kBAA4B,CAAC;AACnC,QAAM,qBAA+B,CAAC;AAGtC,YAAU,IAAI,CAAC,SAAS;AACtB,UAAM,EAAC,WAAW,KAAI,IAAI;AAE1B,UAAM,gBAAe,6BAAM,mBAAkB;AAE7C,QAAI,cAAc;AAChB,sBAAgB,KAAK,IAAI;AAAA,IAC3B,OAAO;AACL,yBAAmB,KAAK,IAAI;AAAA,IAC9B;AAAA,EACF,CAAC;AAED,SACE,qBAAC,SAAI,WAAU,8DACb;AAAA,wBAAC,sBAAmB,OAAO,iBAAiB,kBAAoC;AAAA,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../src/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/index.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 { FilesSection } from './src/FilesSection'\r\nimport { PointCloudsSection } from './src/PointCloudSection'\r\nimport { BuildingsContext } from '../../../../../../../store'\r\nimport { useSearchParams } from 'next/navigation'\r\nimport { useFilesByBuildingId } from '../../../../../../../hooks/files/files'\r\nimport { DbFile } from '../../../../../../../types/dbTypes'\r\n\r\nexport function FileTab({ pointcloudApiUrl }: { pointcloudApiUrl?: string }) {\r\n const { state: buildingState } = React.useContext(BuildingsContext)\r\n const { building } = buildingState.buildings\r\n\r\n const urlBuildingId = useSearchParams().get('buildingId')\r\n // Resolve from the store building, else the URL param; undefined if neither is\r\n // ready yet (e.g. mid viewer-switch) — the hook handles undefined → no files.\r\n const buildingId = building?.id ?? (urlBuildingId ? Number(urlBuildingId) : undefined)\r\n\r\n const filesData: DbFile[] = useFilesByBuildingId(buildingId).files || []\r\n\r\n const pointCloudFiles: DbFile[] = []\r\n const nonPointCloudFiles: DbFile[] = []\r\n\r\n // single .map pass to populate both arrays\r\n filesData.map((file) => {\r\n const {extension, type} = file\r\n // const isPointCloud = extension.toLowerCase() === 'laz' || extension.toLowerCase() === 'las'\r\n const isPointCloud = type?.toLowerCase() === 'point-cloud-file'\r\n\r\n if (isPointCloud) {\r\n pointCloudFiles.push(file)\r\n } else {\r\n nonPointCloudFiles.push(file)\r\n }\r\n })\r\n\r\n return (\r\n <div className=\"flex-1 flex flex-col space-y-6 h-full py-4 overflow-hidden\">\r\n <PointCloudsSection files={pointCloudFiles} pointcloudApiUrl={pointcloudApiUrl} buildingId={buildingId} />\r\n <FilesSection files={nonPointCloudFiles} />\r\n </div>\r\n )\r\n}\r\n"],"mappings":";AAyCI,SACE,KADF;AApCJ,YAAY,WAAW;AACvB,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,uBAAuB;AAChC,SAAS,4BAA4B;AAG9B,SAAS,QAAQ,EAAE,iBAAiB,GAAkC;AAb7E;AAcE,QAAM,EAAE,OAAO,cAAc,IAAI,MAAM,WAAW,gBAAgB;AAClE,QAAM,EAAE,SAAS,IAAI,cAAc;AAEnC,QAAM,gBAAgB,gBAAgB,EAAE,IAAI,YAAY;AAGxD,QAAM,cAAa,0CAAU,OAAV,YAAiB,gBAAgB,OAAO,aAAa,IAAI;AAE5E,QAAM,YAAsB,qBAAqB,UAAU,EAAE,SAAS,CAAC;AAEvE,QAAM,kBAA4B,CAAC;AACnC,QAAM,qBAA+B,CAAC;AAGtC,YAAU,IAAI,CAAC,SAAS;AACtB,UAAM,EAAC,WAAW,KAAI,IAAI;AAE1B,UAAM,gBAAe,6BAAM,mBAAkB;AAE7C,QAAI,cAAc;AAChB,sBAAgB,KAAK,IAAI;AAAA,IAC3B,OAAO;AACL,yBAAmB,KAAK,IAAI;AAAA,IAC9B;AAAA,EACF,CAAC;AAED,SACE,qBAAC,SAAI,WAAU,8DACb;AAAA,wBAAC,sBAAmB,OAAO,iBAAiB,kBAAoC,YAAwB;AAAA,IACxG,oBAAC,gBAAa,OAAO,oBAAoB;AAAA,KAC3C;AAEJ;","names":[]}
|
|
@@ -3,7 +3,8 @@ import { DbFile } from '../../../../../../../../types/dbTypes';
|
|
|
3
3
|
interface PointCloudsSectionProps {
|
|
4
4
|
files: DbFile[];
|
|
5
5
|
pointcloudApiUrl?: string;
|
|
6
|
+
buildingId?: number;
|
|
6
7
|
}
|
|
7
|
-
export declare function PointCloudsSection({ files, pointcloudApiUrl }: PointCloudsSectionProps): React.JSX.Element;
|
|
8
|
+
export declare function PointCloudsSection({ files, pointcloudApiUrl, buildingId }: PointCloudsSectionProps): React.JSX.Element;
|
|
8
9
|
export {};
|
|
9
10
|
//# sourceMappingURL=PointCloudSection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PointCloudSection.d.ts","sourceRoot":"","sources":["../../../../../../../../../../src/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAS9B,OAAO,EAAE,MAAM,EAAE,MAAM,uCAAuC,CAAA;AAoB9D,UAAU,uBAAuB;IAC/B,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"PointCloudSection.d.ts","sourceRoot":"","sources":["../../../../../../../../../../src/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAS9B,OAAO,EAAE,MAAM,EAAE,MAAM,uCAAuC,CAAA;AAoB9D,UAAU,uBAAuB;IAC/B,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,EAAE,uBAAuB,qBAqYlG"}
|
|
@@ -30,7 +30,7 @@ import ConfirmDialog from "../../../../../../../ConfirmDialog";
|
|
|
30
30
|
import { Button } from "../../../../../../../ui/Button";
|
|
31
31
|
import { PointCloudContext } from "../../../../../../../../store";
|
|
32
32
|
const POINT_CLOUD_OPTIONS = ["view", "delete", "info"];
|
|
33
|
-
function PointCloudsSection({ files, pointcloudApiUrl }) {
|
|
33
|
+
function PointCloudsSection({ files, pointcloudApiUrl, buildingId }) {
|
|
34
34
|
const t = useTranslations("PointCloudManagement");
|
|
35
35
|
const API_BASE = pointcloudApiUrl != null ? pointcloudApiUrl : "http://localhost:5101";
|
|
36
36
|
const { data: session } = useSession();
|
|
@@ -128,11 +128,11 @@ function PointCloudsSection({ files, pointcloudApiUrl }) {
|
|
|
128
128
|
return bLoaded - aLoaded;
|
|
129
129
|
});
|
|
130
130
|
}, [pointcloudsFiles]);
|
|
131
|
-
async function createPointCloud(name,
|
|
131
|
+
async function createPointCloud(name, buildingId2) {
|
|
132
132
|
const res = await fetch("/api/point-cloud", {
|
|
133
133
|
method: "POST",
|
|
134
134
|
headers: { "Content-Type": "application/json" },
|
|
135
|
-
body: JSON.stringify({ name,
|
|
135
|
+
body: JSON.stringify({ name, buildingId: buildingId2 })
|
|
136
136
|
});
|
|
137
137
|
if (!res.ok) {
|
|
138
138
|
const msg = await res.text();
|
|
@@ -222,6 +222,7 @@ function PointCloudsSection({ files, pointcloudApiUrl }) {
|
|
|
222
222
|
);
|
|
223
223
|
es.close();
|
|
224
224
|
esRef.current = null;
|
|
225
|
+
window.location.reload();
|
|
225
226
|
});
|
|
226
227
|
es.addEventListener("failed", () => {
|
|
227
228
|
console.log("[failed] Conversion failed");
|
|
@@ -254,7 +255,7 @@ function PointCloudsSection({ files, pointcloudApiUrl }) {
|
|
|
254
255
|
try {
|
|
255
256
|
console.log("Creating point cloud entry...");
|
|
256
257
|
const userEmail = ((_a = session == null ? void 0 : session.user) == null ? void 0 : _a.email) || null;
|
|
257
|
-
const { pointCloud, upload } = await createPointCloud(fileName,
|
|
258
|
+
const { pointCloud, upload } = await createPointCloud(fileName, buildingId);
|
|
258
259
|
console.log("Point cloud created:", pointCloud.id);
|
|
259
260
|
console.log("Uploading file...");
|
|
260
261
|
await uploadToPresignedUrl(upload.uploadUrl, file, (pct) => {
|