@collabdt/core 0.3.1 → 0.3.2
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/dist/core/components/authentication/ForgotPassword.d.ts.map +1 -1
- package/dist/core/components/authentication/ForgotPassword.js +0 -1
- package/dist/core/components/authentication/ForgotPassword.js.map +1 -1
- package/dist/core/components/ui/Comments/CollapsibleCommentItem.d.ts +12 -2
- package/dist/core/components/ui/Comments/CollapsibleCommentItem.d.ts.map +1 -1
- package/dist/core/components/ui/Comments/CollapsibleCommentItem.js +137 -139
- package/dist/core/components/ui/Comments/CollapsibleCommentItem.js.map +1 -1
- package/dist/core/components/ui/Comments/Comment.d.ts +18 -1
- package/dist/core/components/ui/Comments/Comment.d.ts.map +1 -1
- package/dist/core/components/ui/Comments/Comment.js +94 -23
- package/dist/core/components/ui/Comments/Comment.js.map +1 -1
- package/dist/core/components/ui/Comments/CommentActionButtons.d.ts +34 -0
- package/dist/core/components/ui/Comments/CommentActionButtons.d.ts.map +1 -0
- package/dist/core/components/ui/Comments/CommentActionButtons.js +106 -0
- package/dist/core/components/ui/Comments/CommentActionButtons.js.map +1 -0
- package/dist/core/components/ui/Comments/CommentInput.d.ts.map +1 -1
- package/dist/core/components/ui/Comments/CommentInput.js +5 -2
- package/dist/core/components/ui/Comments/CommentInput.js.map +1 -1
- package/dist/core/components/ui/Comments/CommentsSection.d.ts.map +1 -1
- package/dist/core/components/ui/Comments/CommentsSection.js +86 -42
- package/dist/core/components/ui/Comments/CommentsSection.js.map +1 -1
- package/dist/core/components/ui/Comments/commentUtils.d.ts.map +1 -1
- package/dist/core/components/ui/Comments/commentUtils.js +2 -1
- package/dist/core/components/ui/Comments/commentUtils.js.map +1 -1
- package/dist/core/components/ui/FilesManager/src/useFileUploadWithProgress.d.ts.map +1 -1
- package/dist/core/components/ui/FilesManager/src/useFileUploadWithProgress.js +0 -1
- package/dist/core/components/ui/FilesManager/src/useFileUploadWithProgress.js.map +1 -1
- package/dist/core/components/ui/InfoSidebar/Container.d.ts.map +1 -1
- package/dist/core/components/ui/InfoSidebar/Container.js +16 -12
- package/dist/core/components/ui/InfoSidebar/Container.js.map +1 -1
- package/dist/core/components/ui/Sensors/SensorInput.d.ts.map +1 -1
- package/dist/core/components/ui/Sensors/SensorInput.js +0 -1
- package/dist/core/components/ui/Sensors/SensorInput.js.map +1 -1
- package/dist/core/components/ui/Sensors/SensorsSection.js +0 -2
- package/dist/core/components/ui/Sensors/SensorsSection.js.map +1 -1
- package/dist/core/components/ui/Sidebar.d.ts.map +1 -1
- package/dist/core/components/ui/Sidebar.js +27 -4
- package/dist/core/components/ui/Sidebar.js.map +1 -1
- package/dist/core/components/ui/ToolbarButton.d.ts.map +1 -1
- package/dist/core/components/ui/ToolbarButton.js +0 -1
- package/dist/core/components/ui/ToolbarButton.js.map +1 -1
- package/dist/core/components/viewers/Data/advancedFilter/FiltersDialog.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/advancedFilter/FiltersDialog.js +0 -2
- package/dist/core/components/viewers/Data/advancedFilter/FiltersDialog.js.map +1 -1
- package/dist/core/components/viewers/Data/details/EnergySourcesGrid.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/details/EnergySourcesGrid.js +0 -10
- package/dist/core/components/viewers/Data/details/EnergySourcesGrid.js.map +1 -1
- package/dist/core/components/viewers/Data/files/DatatableFileAdder.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/files/DatatableFileAdder.js +0 -1
- package/dist/core/components/viewers/Data/files/DatatableFileAdder.js.map +1 -1
- package/dist/core/components/viewers/Data/files/utils/fuzzySearch.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/files/utils/fuzzySearch.js +0 -1
- package/dist/core/components/viewers/Data/files/utils/fuzzySearch.js.map +1 -1
- package/dist/core/components/viewers/Data/userDetails/UserMoreOptions.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/userDetails/UserMoreOptions.js +0 -1
- package/dist/core/components/viewers/Data/userDetails/UserMoreOptions.js.map +1 -1
- package/dist/core/components/viewers/Data/utils/filterHelper.d.ts.map +1 -1
- package/dist/core/components/viewers/Data/utils/filterHelper.js +0 -2
- package/dist/core/components/viewers/Data/utils/filterHelper.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 +0 -3
- 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 +1 -7
- package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.js.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/ModelsSection.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/ModelsSection.js +1 -9
- package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/ModelsSection.js.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/TabSelector.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/BimSidebar/src/TabSelector.js +4 -3
- package/dist/core/components/viewers/bim/src/BimSidebar/src/TabSelector.js.map +1 -1
- package/dist/core/components/viewers/bim/src/ClippingPlane.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/ClippingPlane.js +0 -1
- package/dist/core/components/viewers/bim/src/ClippingPlane.js.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimComment.d.ts +16 -10
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimComment.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimComment.js +26 -6
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimComment.js.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimCommentCluster.d.ts +23 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimCommentCluster.d.ts.map +1 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimCommentCluster.js +108 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimCommentCluster.js.map +1 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/clusterMarkersByScreenSpace.d.ts +44 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/clusterMarkersByScreenSpace.d.ts.map +1 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/clusterMarkersByScreenSpace.js +77 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/clusterMarkersByScreenSpace.js.map +1 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentCameraUtils.d.ts +24 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentCameraUtils.d.ts.map +1 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentCameraUtils.js +27 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentCameraUtils.js.map +1 -0
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/renderCSS2DMarkers.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/renderCSS2DMarkers.js +2 -2
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/renderCSS2DMarkers.js.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/useCommentMarkers.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/useCommentMarkers.js +242 -61
- package/dist/core/components/viewers/bim/src/tools/AddToBim/src/useCommentMarkers.js.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/ClippingTool/ClippingTool.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/ClippingTool/ClippingTool.js +1 -5
- package/dist/core/components/viewers/bim/src/tools/ClippingTool/ClippingTool.js.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/ExplodeByLevelTool.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/ExplodeByLevelTool.js +0 -1
- package/dist/core/components/viewers/bim/src/tools/ExplodeByLevelTool.js.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/measureBimTool.d.ts.map +1 -1
- package/dist/core/components/viewers/bim/src/tools/measureBimTool.js +1 -2
- package/dist/core/components/viewers/bim/src/tools/measureBimTool.js.map +1 -1
- package/dist/core/components/viewers/map/MapViewer.d.ts.map +1 -1
- package/dist/core/components/viewers/map/MapViewer.js.map +1 -1
- package/dist/core/components/viewers/map/datasets/src/arcGISDatasets.d.ts.map +1 -1
- package/dist/core/components/viewers/map/datasets/src/arcGISDatasets.js +11 -14
- package/dist/core/components/viewers/map/datasets/src/arcGISDatasets.js.map +1 -1
- package/dist/core/components/viewers/map/datasets/src/ckanDatasets.d.ts.map +1 -1
- package/dist/core/components/viewers/map/datasets/src/ckanDatasets.js +2 -5
- package/dist/core/components/viewers/map/datasets/src/ckanDatasets.js.map +1 -1
- package/dist/core/components/viewers/map/datasets/src/localDatasets.d.ts.map +1 -1
- package/dist/core/components/viewers/map/datasets/src/localDatasets.js +0 -3
- package/dist/core/components/viewers/map/datasets/src/localDatasets.js.map +1 -1
- package/dist/core/components/viewers/map/datasets/src/opendatasoftDatasets.d.ts.map +1 -1
- package/dist/core/components/viewers/map/datasets/src/opendatasoftDatasets.js +0 -11
- package/dist/core/components/viewers/map/datasets/src/opendatasoftDatasets.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapFeaturePopoverMenu/src/DatabaseBuildingPopover/src/BuildingsTools.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/MapFeaturePopoverMenu/src/DatabaseBuildingPopover/src/BuildingsTools.js +0 -1
- package/dist/core/components/viewers/map/src/MapFeaturePopoverMenu/src/DatabaseBuildingPopover/src/BuildingsTools.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapFeaturePopoverMenu/src/NonDatabaseBuildingPopover.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/MapFeaturePopoverMenu/src/NonDatabaseBuildingPopover.js +0 -1
- package/dist/core/components/viewers/map/src/MapFeaturePopoverMenu/src/NonDatabaseBuildingPopover.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapFeaturePopoverMenu/src/OpenDataFeaturePopover.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/MapFeaturePopoverMenu/src/OpenDataFeaturePopover.js +0 -1
- package/dist/core/components/viewers/map/src/MapFeaturePopoverMenu/src/OpenDataFeaturePopover.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/src/CommentLayer/index.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/src/CommentLayer/index.js +174 -53
- package/dist/core/components/viewers/map/src/MapLayers/src/CommentLayer/index.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/src/FileLayer/components/MapFileManager.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/src/FileLayer/components/MapFileManager.js +2 -1
- package/dist/core/components/viewers/map/src/MapLayers/src/FileLayer/components/MapFileManager.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/src/SensorsLayer/index.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/MapLayers/src/SensorsLayer/index.js +0 -3
- package/dist/core/components/viewers/map/src/MapLayers/src/SensorsLayer/index.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapSidebar/TabSelector.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/MapSidebar/TabSelector.js +4 -3
- package/dist/core/components/viewers/map/src/MapSidebar/TabSelector.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapSidebar/src/FileTab/src/FilesSection.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/MapSidebar/src/FileTab/src/FilesSection.js +0 -1
- package/dist/core/components/viewers/map/src/MapSidebar/src/FileTab/src/FilesSection.js.map +1 -1
- package/dist/core/components/viewers/map/src/MapSidebar/src/FileTab/src/ModelsSection.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/MapSidebar/src/FileTab/src/ModelsSection.js +1 -5
- package/dist/core/components/viewers/map/src/MapSidebar/src/FileTab/src/ModelsSection.js.map +1 -1
- package/dist/core/components/viewers/map/src/tools/AddTools/AddFile/utils/uploadToPresignedURLS.d.ts.map +1 -1
- package/dist/core/components/viewers/map/src/tools/AddTools/AddFile/utils/uploadToPresignedURLS.js +1 -3
- package/dist/core/components/viewers/map/src/tools/AddTools/AddFile/utils/uploadToPresignedURLS.js.map +1 -1
- package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.d.ts.map +1 -1
- package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.js +0 -1
- package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.js.map +1 -1
- package/dist/core/components/viewers/map/utils/flyToEach.d.ts.map +1 -1
- package/dist/core/components/viewers/map/utils/flyToEach.js +0 -1
- package/dist/core/components/viewers/map/utils/flyToEach.js.map +1 -1
- package/dist/core/components/viewers/map/utils/getOsmIdbyCoordinates.d.ts.map +1 -1
- package/dist/core/components/viewers/map/utils/getOsmIdbyCoordinates.js +0 -2
- package/dist/core/components/viewers/map/utils/getOsmIdbyCoordinates.js.map +1 -1
- package/dist/core/hooks/comments/comments.d.ts +11 -0
- package/dist/core/hooks/comments/comments.d.ts.map +1 -1
- package/dist/core/hooks/comments/comments.js +3 -1
- package/dist/core/hooks/comments/comments.js.map +1 -1
- package/dist/core/hooks/comments/createCommentHooks.d.ts +11 -0
- package/dist/core/hooks/comments/createCommentHooks.d.ts.map +1 -1
- package/dist/core/hooks/comments/createCommentHooks.js +30 -1
- package/dist/core/hooks/comments/createCommentHooks.js.map +1 -1
- package/dist/core/hooks/files/files.d.ts.map +1 -1
- package/dist/core/hooks/files/files.js +0 -1
- package/dist/core/hooks/files/files.js.map +1 -1
- package/dist/core/hooks/ui/useResizableSidebarWidth.d.ts +30 -0
- package/dist/core/hooks/ui/useResizableSidebarWidth.d.ts.map +1 -0
- package/dist/core/hooks/ui/useResizableSidebarWidth.js +75 -0
- package/dist/core/hooks/ui/useResizableSidebarWidth.js.map +1 -0
- package/dist/core/i18n/messages/en.json +5 -0
- package/dist/core/i18n/messages/es.json +6 -1
- package/dist/core/i18n/messages/fr.json +6 -1
- package/dist/core/store/Datasets/reducer.d.ts.map +1 -1
- package/dist/core/store/Datasets/reducer.js +0 -2
- package/dist/core/store/Datasets/reducer.js.map +1 -1
- package/dist/core/store/Files/reducer.js +0 -1
- package/dist/core/store/Files/reducer.js.map +1 -1
- package/dist/core/store/Menus/context.d.ts.map +1 -1
- package/dist/core/store/Menus/context.js +4 -1
- package/dist/core/store/Menus/context.js.map +1 -1
- package/dist/core/store/Menus/reducer.d.ts +20 -0
- package/dist/core/store/Menus/reducer.d.ts.map +1 -1
- package/dist/core/store/Menus/reducer.js +21 -3
- package/dist/core/store/Menus/reducer.js.map +1 -1
- package/dist/core/utils/markerUtils.d.ts +10 -0
- package/dist/core/utils/markerUtils.d.ts.map +1 -1
- package/dist/core/utils/markerUtils.js +9 -1
- package/dist/core/utils/markerUtils.js.map +1 -1
- package/dist/core/utils/memcache.d.ts.map +1 -1
- package/dist/core/utils/memcache.js +0 -7
- package/dist/core/utils/memcache.js.map +1 -1
- package/package.json +4 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../src/core/components/viewers/map/MapViewer.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 maplibregl from 'maplibre-gl'\r\nimport { useSearchParams } from 'next/navigation'\r\nimport React from 'react'\r\nimport Map, { NavigationControl } from 'react-map-gl/maplibre'\r\n\r\n\r\nimport { MapContext } from '../../../store'\r\nimport SettingsButton from '../../ui/SettingsButton'\r\nimport { StatsOverlay } from '../../ui/stats'\r\n\r\nimport DatasetManagerMenu from './datasets/DatasetManager'\r\nimport { MapLegendHost } from './legends/MapLegendHost'\r\nimport { MapLayers } from './src/MapLayers'\r\nimport { MapClickManager } from './utils/MapEventManager/MapClickManager'\r\nimport { MapHoverManager } from './utils/MapEventManager/MapHoverManager'\r\nimport { resolveBounds } from './utils/validateBounds'\r\n\r\n\r\n\r\n\r\nimport type { Organization } from '../../../types/dbTypes'\r\nimport type { CurrentLocation } from '../../../types/map'\r\nimport type { MapRef} from 'react-map-gl/maplibre';\r\n\r\nconst CANADA_DEFAULTS = {\r\n zoom: 3,\r\n lat: 56.415,\r\n long: -98.74,\r\n} as const\r\n\r\nconst DEFAULT_MAP_STYLE = { name: 'Satellite', url: 'mapStyles/satellite.json' } as const\r\nconst MAX_GLOBE_ZOOM = 5\r\n\r\ninterface Props {\r\n width?: string\r\n height?: string\r\n organization: Organization\r\n minioBaseUrl?: string\r\n maptilerKey?: string\r\n}\r\n\r\nexport function MapViewer({ width = '100%', height = '100%', organization, minioBaseUrl, maptilerKey }: Props) {\r\n\r\n const searchParams = useSearchParams()\r\n\r\n // Add state to track if map is loaded\r\n const [isMapLoaded, setIsMapLoaded] = React.useState(false)\r\n\r\n const viewState = React.useMemo(() => {\r\n return searchParams.size === 0\r\n ? {\r\n zoom: organization.zoom ?? CANADA_DEFAULTS.zoom,\r\n bearing: organization.bearing ?? 0,\r\n pitch: organization.pitch ?? 0,\r\n longitude: organization.long ?? CANADA_DEFAULTS.long,\r\n latitude: organization.lat ?? CANADA_DEFAULTS.lat,\r\n }\r\n : {\r\n latitude: searchParams.has('lat') ? Number.parseFloat(searchParams.get('lat')) : organization.lat ?? CANADA_DEFAULTS.lat,\r\n longitude: searchParams.has('lng') ? Number.parseFloat(searchParams.get('lng')) : organization.long ?? CANADA_DEFAULTS.long,\r\n bearing: searchParams.has('bearing') ? Number.parseFloat(searchParams.get('bearing')) : organization.bearing ?? 0,\r\n pitch: searchParams.has('pitch') ? Number.parseFloat(searchParams.get('pitch')) : organization.pitch ?? 0,\r\n zoom: searchParams.has('zoom') ? Number.parseFloat(searchParams.get('zoom')) : organization.zoom ?? CANADA_DEFAULTS.zoom,\r\n }\r\n }, [searchParams, organization])\r\n\r\n const initialProjection = viewState.zoom > MAX_GLOBE_ZOOM ? 'mercator' : 'globe'\r\n\r\n // Memoize the inline style object that gets passed to <Map> for a stable prop identity.\r\n const mapContainerStyle = React.useMemo(\r\n () => ({ width, height, backgroundColor: 'black' }),\r\n [width, height],\r\n )\r\n\r\n // Adaptive pixel ratio. A hardcoded 2 doubled fragment-shader cost on 1× DPR\r\n // desktops for zero visual gain; capping at 2 keeps phones (DPR 3-4) effective.\r\n const pixelRatio = React.useMemo(\r\n () => Math.min(typeof window !== 'undefined' ? window.devicePixelRatio : 1, 2),\r\n [],\r\n )\r\n\r\n const mapRef = React.useRef<MapRef>(null)\r\n const { dispatch: mapDispatch, state: mapState } = React.useContext(MapContext)\r\n\r\n // Keep the projection in sync with zoom at all times — not only while the\r\n // map-settings sidebar is mounted. The globe projection degrades past\r\n // MAX_GLOBE_ZOOM, so we force mercator regardless of which panels are open.\r\n const activeMap = mapState?.map?.map\r\n\r\n React.useEffect(() => {\r\n if (!activeMap) return\r\n\r\n const enforceProjectionForZoom = () => {\r\n if (activeMap.getZoom() <= MAX_GLOBE_ZOOM) return\r\n\r\n const projection = activeMap.getProjection()\r\n const currentType =\r\n typeof projection === 'string'\r\n ? projection\r\n : (projection as { type?: string; name?: string })?.type ?? (projection as { type?: string; name?: string })?.name\r\n\r\n if (currentType !== 'mercator') {\r\n activeMap.setProjection({ type: 'mercator' })\r\n }\r\n }\r\n\r\n enforceProjectionForZoom()\r\n activeMap.on('zoom', enforceProjectionForZoom)\r\n\r\n return () => {\r\n activeMap.off('zoom', enforceProjectionForZoom)\r\n }\r\n }, [activeMap])\r\n\r\n const handleMapLoad = () => {\r\n const map = mapRef.current.getMap()\r\n\r\n if (map.getZoom() > MAX_GLOBE_ZOOM) {\r\n map.setProjection({ type: 'mercator' })\r\n }\r\n\r\n // Only dispatch SET_MAP after map is fully loaded\r\n if (mapRef.current) {\r\n mapDispatch({\r\n type: 'SET_MAP',\r\n payload: { map },\r\n })\r\n }\r\n // initilize a new click manager\r\n const mapClickManager = new MapClickManager(map)\r\n mapDispatch({\r\n type: 'ADD_MAP_CLICK_MANAGER',\r\n payload: { mapClickManager },\r\n })\r\n\r\n const mapHoverManager = new MapHoverManager(map)\r\n mapDispatch({\r\n type: 'ADD_MAP_HOVER_MANAGER',\r\n payload: { mapHoverManager },\r\n })\r\n\r\n // update current Location with URL params or fallback to theme location\r\n const updatedLocation: CurrentLocation = {\r\n // Then override with URL params (this ensures URL params take precedence)\r\n lat: searchParams.has('lat') ? Number.parseFloat(searchParams.get('lat')) : organization.lat ?? CANADA_DEFAULTS.lat,\r\n lng: searchParams.has('lng') ? Number.parseFloat(searchParams.get('lng')) : organization.long ?? CANADA_DEFAULTS.long,\r\n bearing: searchParams.has('bearing') ? Number.parseFloat(searchParams.get('bearing')) : organization.bearing ?? 0,\r\n pitch: searchParams.has('pitch') ? Number.parseFloat(searchParams.get('pitch')) : organization.pitch ?? 0,\r\n zoom: searchParams.has('zoom') ? Number.parseFloat(searchParams.get('zoom')) : organization.zoom ?? CANADA_DEFAULTS.zoom,\r\n // Add location details from URL params if available\r\n countrySubdivision: searchParams.has('countrySubdivision') ? searchParams.get('countrySubdivision') : (organization.countrySubdivision ?? ''),\r\n municipality: searchParams.has('municipality') ? searchParams.get('municipality') : (organization.municipality ?? ''),\r\n address: searchParams.has('address') ? searchParams.get('address') : '', // organization.address,\r\n site: searchParams.has('site') ? searchParams.get('site') : String(organization.locationSiteId ?? ''),\r\n id: String(organization.id),\r\n }\r\n\r\n mapDispatch({\r\n type: 'UPDATE_LOCATION',\r\n payload: { currentLocation: updatedLocation },\r\n })\r\n\r\n // Set map as loaded after all dispatches are complete\r\n setIsMapLoaded(true)\r\n }\r\n\r\n // Enable the perf HUD by appending ?debug=1 to the URL. Never reaches\r\n // production users unless they explicitly opt in.\r\n const isDebug = searchParams.get('debug') === '1'\r\n\r\n // useCallback so the onDblClick prop identity is stable across renders of\r\n // MapViewer; a new function each render caused react-map-gl to detach/re-attach\r\n // the listener. The console.log is gated behind a dev-mode check.\r\n const onDblClick = React.useCallback((e) => {\r\n if (process.env.NODE_ENV === 'production') return\r\n const { lng, lat } = e.lngLat\r\n const elevation = mapRef.current?.queryTerrainElevation([lng, lat]) || 0\r\n const coordinates = { lng, lat, elevation, zoom: mapRef.current?.getZoom() }\r\n console.log('📍 Map coordinates:', coordinates)\r\n }, [])\r\n\r\n const mapStyle = mapState?.map.mapStyle ?? DEFAULT_MAP_STYLE;\r\n\r\n return (\r\n <>\r\n <Map\r\n id=\"map-component\"\r\n mapStyle={mapStyle.url}\r\n mapLib={maplibregl}\r\n onLoad={handleMapLoad}\r\n ref={mapRef}\r\n style={mapContainerStyle}\r\n initialViewState={viewState}\r\n maxPitch={60}\r\n minZoom={organization?.minZoom ?? CANADA_DEFAULTS.zoom}\r\n maxBounds={resolveBounds(organization?.maxBounds)}\r\n projection={initialProjection}\r\n doubleClickZoom={false}\r\n onDblClick={onDblClick}\r\n pixelRatio={pixelRatio}\r\n >\r\n <NavigationControl visualizePitch />\r\n <SettingsButton />\r\n {isMapLoaded\r\n && (\r\n <>\r\n <MapLayers minioBaseUrl={minioBaseUrl} organization={organization} maptilerKey={maptilerKey} />\r\n {/* Bottom-left stack: legend above the layers/styling card, gap auto-managed by flex. */}\r\n <div className=\"absolute bottom-[10px] left-3 z-10 flex flex-col gap-2 pointer-events-none\">\r\n {/* Portal slot for on-map WMS time controls; display:contents so an\r\n empty slot adds no flex item / gap, but a mounted control stacks\r\n above the legend + dataset-manager cards. */}\r\n <div id=\"wms-time-slot\" style={{ display: 'contents' }} />\r\n <MapLegendHost />\r\n <DatasetManagerMenu />\r\n </div>\r\n </>\r\n )}\r\n </Map>\r\n <StatsOverlay mapRef={mapRef} enabled={isDebug} />\r\n </>\r\n\r\n )\r\n}\r\n"],"mappings":"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","names":["_a","_b","_c"]}
|
|
1
|
+
{"version":3,"sources":["../../../../../src/core/components/viewers/map/MapViewer.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 maplibregl from 'maplibre-gl'\r\nimport { useSearchParams } from 'next/navigation'\r\nimport React from 'react'\r\nimport Map, { NavigationControl } from 'react-map-gl/maplibre'\r\n\r\n\r\nimport { MapContext } from '../../../store'\r\nimport SettingsButton from '../../ui/SettingsButton'\r\nimport { StatsOverlay } from '../../ui/stats'\r\n\r\nimport DatasetManagerMenu from './datasets/DatasetManager'\r\nimport { MapLegendHost } from './legends/MapLegendHost'\r\nimport { MapLayers } from './src/MapLayers'\r\nimport { MapClickManager } from './utils/MapEventManager/MapClickManager'\r\nimport { MapHoverManager } from './utils/MapEventManager/MapHoverManager'\r\nimport { resolveBounds } from './utils/validateBounds'\r\n\r\n\r\n\r\n\r\nimport type { Organization } from '../../../types/dbTypes'\r\nimport type { CurrentLocation } from '../../../types/map'\r\nimport type { MapRef} from 'react-map-gl/maplibre';\r\n\r\nconst CANADA_DEFAULTS = {\r\n zoom: 3,\r\n lat: 56.415,\r\n long: -98.74,\r\n} as const\r\n\r\nconst DEFAULT_MAP_STYLE = { name: 'Satellite', url: 'mapStyles/satellite.json' } as const\r\nconst MAX_GLOBE_ZOOM = 5\r\n\r\ninterface Props {\r\n width?: string\r\n height?: string\r\n organization: Organization\r\n minioBaseUrl?: string\r\n maptilerKey?: string\r\n}\r\n\r\nexport function MapViewer({ width = '100%', height = '100%', organization, minioBaseUrl, maptilerKey }: Props) {\r\n\r\n const searchParams = useSearchParams()\r\n\r\n // Add state to track if map is loaded\r\n const [isMapLoaded, setIsMapLoaded] = React.useState(false)\r\n\r\n const viewState = React.useMemo(() => {\r\n return searchParams.size === 0\r\n ? {\r\n zoom: organization.zoom ?? CANADA_DEFAULTS.zoom,\r\n bearing: organization.bearing ?? 0,\r\n pitch: organization.pitch ?? 0,\r\n longitude: organization.long ?? CANADA_DEFAULTS.long,\r\n latitude: organization.lat ?? CANADA_DEFAULTS.lat,\r\n }\r\n : {\r\n latitude: searchParams.has('lat') ? Number.parseFloat(searchParams.get('lat')) : organization.lat ?? CANADA_DEFAULTS.lat,\r\n longitude: searchParams.has('lng') ? Number.parseFloat(searchParams.get('lng')) : organization.long ?? CANADA_DEFAULTS.long,\r\n bearing: searchParams.has('bearing') ? Number.parseFloat(searchParams.get('bearing')) : organization.bearing ?? 0,\r\n pitch: searchParams.has('pitch') ? Number.parseFloat(searchParams.get('pitch')) : organization.pitch ?? 0,\r\n zoom: searchParams.has('zoom') ? Number.parseFloat(searchParams.get('zoom')) : organization.zoom ?? CANADA_DEFAULTS.zoom,\r\n }\r\n }, [searchParams, organization])\r\n\r\n const initialProjection = viewState.zoom > MAX_GLOBE_ZOOM ? 'mercator' : 'globe'\r\n\r\n // Memoize the inline style object that gets passed to <Map> for a stable prop identity.\r\n const mapContainerStyle = React.useMemo(\r\n () => ({ width, height, backgroundColor: 'black' }),\r\n [width, height],\r\n )\r\n\r\n // Adaptive pixel ratio. A hardcoded 2 doubled fragment-shader cost on 1× DPR\r\n // desktops for zero visual gain; capping at 2 keeps phones (DPR 3-4) effective.\r\n const pixelRatio = React.useMemo(\r\n () => Math.min(typeof window !== 'undefined' ? window.devicePixelRatio : 1, 2),\r\n [],\r\n )\r\n\r\n const mapRef = React.useRef<MapRef>(null)\r\n const { dispatch: mapDispatch, state: mapState } = React.useContext(MapContext)\r\n\r\n // Keep the projection in sync with zoom at all times — not only while the\r\n // map-settings sidebar is mounted. The globe projection degrades past\r\n // MAX_GLOBE_ZOOM, so we force mercator regardless of which panels are open.\r\n const activeMap = mapState?.map?.map\r\n\r\n React.useEffect(() => {\r\n if (!activeMap) return\r\n\r\n const enforceProjectionForZoom = () => {\r\n if (activeMap.getZoom() <= MAX_GLOBE_ZOOM) return\r\n\r\n const projection = activeMap.getProjection()\r\n const currentType =\r\n typeof projection === 'string'\r\n ? projection\r\n : (projection as { type?: string; name?: string })?.type ?? (projection as { type?: string; name?: string })?.name\r\n\r\n if (currentType !== 'mercator') {\r\n activeMap.setProjection({ type: 'mercator' })\r\n }\r\n }\r\n\r\n enforceProjectionForZoom()\r\n activeMap.on('zoom', enforceProjectionForZoom)\r\n\r\n return () => {\r\n activeMap.off('zoom', enforceProjectionForZoom)\r\n }\r\n }, [activeMap])\r\n\r\n const handleMapLoad = () => {\r\n const map = mapRef.current.getMap()\r\n\r\n if (map.getZoom() > MAX_GLOBE_ZOOM) {\r\n map.setProjection({ type: 'mercator' })\r\n }\r\n\r\n // Only dispatch SET_MAP after map is fully loaded\r\n if (mapRef.current) {\r\n mapDispatch({\r\n type: 'SET_MAP',\r\n payload: { map },\r\n })\r\n }\r\n // initilize a new click manager\r\n const mapClickManager = new MapClickManager(map)\r\n mapDispatch({\r\n type: 'ADD_MAP_CLICK_MANAGER',\r\n payload: { mapClickManager },\r\n })\r\n\r\n const mapHoverManager = new MapHoverManager(map)\r\n mapDispatch({\r\n type: 'ADD_MAP_HOVER_MANAGER',\r\n payload: { mapHoverManager },\r\n })\r\n\r\n // update current Location with URL params or fallback to theme location\r\n const updatedLocation: CurrentLocation = {\r\n // Then override with URL params (this ensures URL params take precedence)\r\n lat: searchParams.has('lat') ? Number.parseFloat(searchParams.get('lat')) : organization.lat ?? CANADA_DEFAULTS.lat,\r\n lng: searchParams.has('lng') ? Number.parseFloat(searchParams.get('lng')) : organization.long ?? CANADA_DEFAULTS.long,\r\n bearing: searchParams.has('bearing') ? Number.parseFloat(searchParams.get('bearing')) : organization.bearing ?? 0,\r\n pitch: searchParams.has('pitch') ? Number.parseFloat(searchParams.get('pitch')) : organization.pitch ?? 0,\r\n zoom: searchParams.has('zoom') ? Number.parseFloat(searchParams.get('zoom')) : organization.zoom ?? CANADA_DEFAULTS.zoom,\r\n // Add location details from URL params if available\r\n countrySubdivision: searchParams.has('countrySubdivision') ? searchParams.get('countrySubdivision') : (organization.countrySubdivision ?? ''),\r\n municipality: searchParams.has('municipality') ? searchParams.get('municipality') : (organization.municipality ?? ''),\r\n address: searchParams.has('address') ? searchParams.get('address') : '', // organization.address,\r\n site: searchParams.has('site') ? searchParams.get('site') : String(organization.locationSiteId ?? ''),\r\n id: String(organization.id),\r\n }\r\n\r\n mapDispatch({\r\n type: 'UPDATE_LOCATION',\r\n payload: { currentLocation: updatedLocation },\r\n })\r\n\r\n // Set map as loaded after all dispatches are complete\r\n setIsMapLoaded(true)\r\n }\r\n\r\n // Enable the perf HUD by appending ?debug=1 to the URL. Never reaches\r\n // production users unless they explicitly opt in.\r\n const isDebug = searchParams.get('debug') === '1'\r\n\r\n // useCallback so the onDblClick prop identity is stable across renders of\r\n // MapViewer; a new function each render caused react-map-gl to detach/re-attach\r\n // the listener. The console.log is intentional dev-only coordinate debugging.\r\n const onDblClick = React.useCallback((e) => {\r\n if (process.env.NODE_ENV === 'production') return\r\n const { lng, lat } = e.lngLat\r\n const elevation = mapRef.current?.queryTerrainElevation([lng, lat]) || 0\r\n const coordinates = { lng, lat, elevation, zoom: mapRef.current?.getZoom() }\r\n // eslint-disable-next-line no-console -- intentional dev-mode coordinate logging\r\n console.log('📍 Map coordinates:', coordinates)\r\n }, [])\r\n\r\n const mapStyle = mapState?.map.mapStyle ?? DEFAULT_MAP_STYLE;\r\n\r\n return (\r\n <>\r\n <Map\r\n id=\"map-component\"\r\n mapStyle={mapStyle.url}\r\n mapLib={maplibregl}\r\n onLoad={handleMapLoad}\r\n ref={mapRef}\r\n style={mapContainerStyle}\r\n initialViewState={viewState}\r\n maxPitch={60}\r\n minZoom={organization?.minZoom ?? CANADA_DEFAULTS.zoom}\r\n maxBounds={resolveBounds(organization?.maxBounds)}\r\n projection={initialProjection}\r\n doubleClickZoom={false}\r\n onDblClick={onDblClick}\r\n pixelRatio={pixelRatio}\r\n >\r\n <NavigationControl visualizePitch />\r\n <SettingsButton />\r\n {isMapLoaded\r\n && (\r\n <>\r\n <MapLayers minioBaseUrl={minioBaseUrl} organization={organization} maptilerKey={maptilerKey} />\r\n {/* Bottom-left stack: legend above the layers/styling card, gap auto-managed by flex. */}\r\n <div className=\"absolute bottom-[10px] left-3 z-10 flex flex-col gap-2 pointer-events-none\">\r\n {/* Portal slot for on-map WMS time controls; display:contents so an\r\n empty slot adds no flex item / gap, but a mounted control stacks\r\n above the legend + dataset-manager cards. */}\r\n <div id=\"wms-time-slot\" style={{ display: 'contents' }} />\r\n <MapLegendHost />\r\n <DatasetManagerMenu />\r\n </div>\r\n </>\r\n )}\r\n </Map>\r\n <StatsOverlay mapRef={mapRef} enabled={isDebug} />\r\n </>\r\n\r\n )\r\n}\r\n"],"mappings":"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","names":["_a","_b","_c"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arcGISDatasets.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/components/viewers/map/datasets/src/arcGISDatasets.ts"],"names":[],"mappings":"AAKA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAMxF,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAA;AAI3E,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AA8CD,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,cAAc,EACtB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,OAAO,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"arcGISDatasets.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/components/viewers/map/datasets/src/arcGISDatasets.ts"],"names":[],"mappings":"AAKA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAMxF,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAA;AAI3E,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AA8CD,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,cAAc,EACtB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,OAAO,EAAE,CAAC,CAsQpB;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,CAAA;CACxD;AAED,wBAAgB,cAAc,IAAI,cAAc,CAmB/C"}
|
|
@@ -53,19 +53,16 @@ async function withConcurrencyLimit(items, limit, fn) {
|
|
|
53
53
|
return results;
|
|
54
54
|
}
|
|
55
55
|
async function fetchArcGISDatasets(portal, rowsPerPage) {
|
|
56
|
-
var _a, _b;
|
|
57
56
|
const { apiUrl, countrySubdivision, municipality, group } = portal;
|
|
58
57
|
try {
|
|
59
58
|
const baseUrl = (apiUrl || "").replace(/\/+$/, "");
|
|
60
59
|
const servicesUrl = `${baseUrl}/services?f=pjson`;
|
|
61
|
-
console.log(`[ArcGIS] Fetching services for portal "${portal.name}": ${servicesUrl}`);
|
|
62
60
|
const response = await fetchWithTimeout(servicesUrl);
|
|
63
61
|
if (!response.ok) {
|
|
64
62
|
console.error(`[ArcGIS] "${portal.name}" services request failed \u2014 status ${response.status}`);
|
|
65
63
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
66
64
|
}
|
|
67
65
|
const data = await response.json();
|
|
68
|
-
console.log(`[ArcGIS] "${portal.name}" \u2014 ${(_b = (_a = data.services) == null ? void 0 : _a.length) != null ? _b : 0} services found`);
|
|
69
66
|
const transformedDatasets = [];
|
|
70
67
|
const processService = async (service) => {
|
|
71
68
|
const url = `${baseUrl}/services/${service.name}/${service.type}`;
|
|
@@ -82,12 +79,12 @@ async function fetchArcGISDatasets(portal, rowsPerPage) {
|
|
|
82
79
|
serviceInfo = await serviceRes.value.json();
|
|
83
80
|
const layers = serviceInfo.layers && serviceInfo.layers.length > 0 ? serviceInfo.layers.filter((l) => l.type === "Feature Layer") : [{ id: 0, name: null, geometryType: null }];
|
|
84
81
|
const hasNonNullGeometry = (featuresUrl, cacheKey) => withCache(cacheKey, 24 * 60 * 60 * 1e3, async () => {
|
|
85
|
-
var
|
|
82
|
+
var _a, _b;
|
|
86
83
|
try {
|
|
87
84
|
const res = await fetchWithTimeout(`${featuresUrl}&resultRecordCount=1`);
|
|
88
85
|
if (!res.ok) return false;
|
|
89
86
|
const d = await res.json();
|
|
90
|
-
if (((
|
|
87
|
+
if (((_a = d == null ? void 0 : d.error) == null ? void 0 : _a.code) || ((_b = d == null ? void 0 : d.error) == null ? void 0 : _b.message)) return false;
|
|
91
88
|
if (!d || d.type !== "FeatureCollection" || !Array.isArray(d.features)) return false;
|
|
92
89
|
return d.features.some((f) => f && f.geometry);
|
|
93
90
|
} catch (e) {
|
|
@@ -96,21 +93,21 @@ async function fetchArcGISDatasets(portal, rowsPerPage) {
|
|
|
96
93
|
});
|
|
97
94
|
const layerResults = await Promise.allSettled(
|
|
98
95
|
layers.map(async (layer) => {
|
|
99
|
-
var
|
|
96
|
+
var _a, _b, _c;
|
|
100
97
|
const layerIndex = layer.id;
|
|
101
98
|
const featuresUrl = `${url}/${layerIndex}/query?outFields=*&where=1%3D1&f=geojson`;
|
|
102
99
|
const featureInfoUrl = `${url}/${layerIndex}?f=pjson`;
|
|
103
100
|
const validKey = buildValidationKey(
|
|
104
101
|
"arcgis",
|
|
105
102
|
`${service.name}_${layerIndex}`,
|
|
106
|
-
((
|
|
103
|
+
((_a = serviceInfo == null ? void 0 : serviceInfo.documentInfo) == null ? void 0 : _a.ModifiedTime) || (extraInfo == null ? void 0 : extraInfo.modified) || void 0
|
|
107
104
|
);
|
|
108
105
|
const hasGeom = await hasNonNullGeometry(featuresUrl, validKey);
|
|
109
106
|
if (!hasGeom) return null;
|
|
110
|
-
const modTime = ((
|
|
107
|
+
const modTime = ((_b = serviceInfo == null ? void 0 : serviceInfo.documentInfo) == null ? void 0 : _b.ModifiedTime) || (extraInfo == null ? void 0 : extraInfo.modified) || "";
|
|
111
108
|
const getFeatures = async (options) => {
|
|
112
|
-
var
|
|
113
|
-
const zoom = (
|
|
109
|
+
var _a2;
|
|
110
|
+
const zoom = (_a2 = options == null ? void 0 : options.zoom) != null ? _a2 : 10;
|
|
114
111
|
const bbox = options == null ? void 0 : options.bbox;
|
|
115
112
|
const pageSize = zoom < 6 ? 500 : zoom < 10 ? 1e3 : 2e3;
|
|
116
113
|
let spatialFilter = "";
|
|
@@ -150,8 +147,8 @@ async function fetchArcGISDatasets(portal, rowsPerPage) {
|
|
|
150
147
|
`${baseQuery}&resultOffset=${pageSize * (i + 1)}&resultRecordCount=${pageSize}`,
|
|
151
148
|
15e3
|
|
152
149
|
).then((r) => r.ok ? r.json() : null).then((p) => {
|
|
153
|
-
var
|
|
154
|
-
return (
|
|
150
|
+
var _a3;
|
|
151
|
+
return (_a3 = p == null ? void 0 : p.features) != null ? _a3 : [];
|
|
155
152
|
}).catch(() => [])
|
|
156
153
|
)
|
|
157
154
|
);
|
|
@@ -209,9 +206,9 @@ async function fetchArcGISDatasets(portal, rowsPerPage) {
|
|
|
209
206
|
return "string";
|
|
210
207
|
};
|
|
211
208
|
const mapped = featureData.fields.map((field) => {
|
|
212
|
-
var
|
|
209
|
+
var _a2;
|
|
213
210
|
const rawEsriType = field.type;
|
|
214
|
-
const inferred = nameBasedType(field) || ((
|
|
211
|
+
const inferred = nameBasedType(field) || ((_a2 = typeMap[rawEsriType]) == null ? void 0 : _a2.type) || inferFieldType(field);
|
|
215
212
|
return __spreadProps(__spreadValues({}, field), { esriType: rawEsriType, type: inferred });
|
|
216
213
|
});
|
|
217
214
|
setCache(fieldsKey, mapped, 24 * 60 * 60 * 1e3);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../src/core/components/viewers/map/datasets/src/arcGISDatasets.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport { type AllGeoJSON } from '@turf/turf'\r\n\r\nimport { DataManagementSystem, type OpenDataPortal } from '../../../../../types/dbTypes'\r\nimport { layerColorByName } from '../../utils/stringToColour'\r\nimport { formatName, stripHtml } from '../utils'\r\n\r\nimport { buildValidationKey, getCache, setCache, withCache } from './cache'\r\n\r\nimport type { Dataset, FieldType } from '../../../../../types/datasetTypes'\r\n\r\nconst inFlightFeatureRequests = new Map<string, Promise<AllGeoJSON>>()\r\n\r\nexport type ViewportOptions = {\r\n bbox?: [number, number, number, number] // [west, south, east, north]\r\n zoom?: number\r\n}\r\n\r\nasync function fetchWithTimeout(url: string, timeout = 10_000): Promise<Response> {\r\n const controller = new AbortController()\r\n const timeoutId = setTimeout(() => controller.abort(), timeout)\r\n try {\r\n const response = await fetch(url, { signal: controller.signal })\r\n clearTimeout(timeoutId)\r\n return response\r\n }\r\n catch (error) {\r\n clearTimeout(timeoutId)\r\n throw error\r\n }\r\n}\r\n\r\n/**\r\n * Run `fn` over `items` with at most `limit` concurrent executions.\r\n * Preserves order of results matching input order.\r\n */\r\nasync function withConcurrencyLimit<T, R>(\r\n items: T[],\r\n limit: number,\r\n fn: (item: T) => Promise<R>,\r\n): Promise<PromiseSettledResult<R>[]> {\r\n const results: PromiseSettledResult<R>[] = new Array(items.length)\r\n let next = 0\r\n\r\n async function worker() {\r\n while (next < items.length) {\r\n const i = next++\r\n try {\r\n results[i] = { status: 'fulfilled', value: await fn(items[i]) }\r\n }\r\n catch (reason) {\r\n results[i] = { status: 'rejected', reason }\r\n }\r\n }\r\n }\r\n\r\n await Promise.all(\r\n Array.from({ length: Math.min(limit, items.length) }, worker),\r\n )\r\n return results\r\n}\r\n\r\nexport async function fetchArcGISDatasets(\r\n portal: OpenDataPortal,\r\n rowsPerPage: number,\r\n): Promise<Dataset[]> {\r\n const { apiUrl, countrySubdivision, municipality, group } = portal\r\n\r\n try {\r\n const baseUrl = (apiUrl || '').replace(/\\/+$/, '')\r\n const servicesUrl = `${baseUrl}/services?f=pjson`\r\n console.log(`[ArcGIS] Fetching services for portal \"${portal.name}\": ${servicesUrl}`)\r\n\r\n const response = await fetchWithTimeout(servicesUrl)\r\n if (!response.ok) {\r\n console.error(`[ArcGIS] \"${portal.name}\" services request failed — status ${response.status}`)\r\n throw new Error(`HTTP error! status: ${response.status}`)\r\n }\r\n\r\n const data = await response.json()\r\n console.log(`[ArcGIS] \"${portal.name}\" — ${data.services?.length ?? 0} services found`)\r\n\r\n const transformedDatasets: Dataset[] = []\r\n\r\n const processService = async (service: any): Promise<Dataset[]> => {\r\n const url = `${baseUrl}/services/${service.name}/${service.type}`\r\n const infoUrl = `${url}/info/itemInfo?f=pjson`\r\n\r\n const [infoRes, serviceRes] = await Promise.allSettled([\r\n fetchWithTimeout(infoUrl),\r\n fetchWithTimeout(`${url}?f=pjson`),\r\n ])\r\n\r\n let extraInfo: any = {}\r\n let serviceInfo: any = {}\r\n\r\n if (infoRes.status === 'fulfilled' && infoRes.value.ok)\r\n extraInfo = await infoRes.value.json()\r\n if (serviceRes.status === 'fulfilled' && serviceRes.value.ok)\r\n serviceInfo = await serviceRes.value.json()\r\n\r\n const layers = serviceInfo.layers && serviceInfo.layers.length > 0\r\n ? serviceInfo.layers.filter((l: any) => l.type === 'Feature Layer')\r\n : [{ id: 0, name: null, geometryType: null }]\r\n\r\n const hasNonNullGeometry = (featuresUrl: string, cacheKey: string): Promise<boolean> =>\r\n withCache<boolean>(cacheKey, 24 * 60 * 60 * 1000, async () => {\r\n try {\r\n const res = await fetchWithTimeout(`${featuresUrl}&resultRecordCount=1`)\r\n if (!res.ok) return false\r\n const d = await res.json()\r\n if (d?.error?.code || d?.error?.message) return false\r\n if (!d || d.type !== 'FeatureCollection' || !Array.isArray(d.features)) return false\r\n return d.features.some((f: any) => f && f.geometry)\r\n }\r\n catch { return false }\r\n })\r\n\r\n // Layers within a service are few (2-5 typically) — parallel is fine here\r\n const layerResults = await Promise.allSettled(\r\n layers.map(async (layer: any): Promise<Dataset | null> => {\r\n const layerIndex = layer.id\r\n const featuresUrl = `${url}/${layerIndex}/query?outFields=*&where=1%3D1&f=geojson`\r\n const featureInfoUrl = `${url}/${layerIndex}?f=pjson`\r\n\r\n const validKey = buildValidationKey(\r\n 'arcgis',\r\n `${service.name}_${layerIndex}`,\r\n serviceInfo?.documentInfo?.ModifiedTime || extraInfo?.modified || undefined,\r\n )\r\n const hasGeom = await hasNonNullGeometry(featuresUrl, validKey)\r\n if (!hasGeom) return null\r\n\r\n const modTime = serviceInfo?.documentInfo?.ModifiedTime || extraInfo?.modified || ''\r\n\r\n /**\r\n * getFeatures — viewport-aware, paginated.\r\n * If `bbox` is provided, only features intersecting the viewport are fetched.\r\n * Automatically paginates beyond the per-page limit.\r\n */\r\n const getFeatures = async (options?: ViewportOptions): Promise<AllGeoJSON> => {\r\n const zoom = options?.zoom ?? 10\r\n const bbox = options?.bbox\r\n\r\n // Page size scales with zoom: fewer records at low zoom (overview), more at high zoom (detail)\r\n const pageSize = zoom < 6 ? 500 : zoom < 10 ? 1000 : 2000\r\n\r\n // Build spatial filter string for ArcGIS when viewport is available\r\n let spatialFilter = ''\r\n if (bbox) {\r\n const [west, south, east, north] = bbox\r\n const geomStr = encodeURIComponent(\r\n JSON.stringify({ xmin: west, ymin: south, xmax: east, ymax: north, spatialReference: { wkid: 4326 } }),\r\n )\r\n spatialFilter = `&geometry=${geomStr}&geometryType=esriGeometryEnvelope&spatialRel=esriSpatialRelIntersects&inSR=4326`\r\n }\r\n\r\n // Round bbox to 0.1° grid to maximise cache hits while panning\r\n const bboxKey = bbox\r\n ? bbox.map(n => (Math.round(n * 10) / 10).toFixed(1)).join(',')\r\n : 'global'\r\n const zoomBucket = Math.floor(zoom / 2)\r\n const featuresKey = `features:arcgis:${service.name}_${layerIndex}:${bboxKey}:z${zoomBucket}:${modTime}`\r\n\r\n const cached = getCache<AllGeoJSON>(featuresKey)\r\n if (cached) return cached\r\n\r\n const inFlight = inFlightFeatureRequests.get(featuresKey)\r\n if (inFlight) return inFlight\r\n\r\n const baseQuery = `${url}/${layerIndex}/query?outFields=*&where=1%3D1&f=geojson${spatialFilter}`\r\n\r\n const request = (async () => {\r\n try {\r\n const firstRes = await fetchWithTimeout(`${baseQuery}&resultOffset=0&resultRecordCount=${pageSize}`, 15_000)\r\n if (!firstRes.ok) return { type: 'FeatureCollection', features: [] } as AllGeoJSON\r\n\r\n const firstPage = await firstRes.json()\r\n if (!firstPage?.features) return { type: 'FeatureCollection', features: [] } as AllGeoJSON\r\n\r\n const allFeatures = [...firstPage.features]\r\n\r\n // If the server returned fewer features than the requested page size, the first page is complete.\r\n // This avoids an unnecessary count request for small datasets that have already finished loading.\r\n if (firstPage.exceededTransferLimit && firstPage.features.length >= pageSize) {\r\n try {\r\n const countRes = await fetchWithTimeout(`${baseQuery}&returnCountOnly=true&f=json`, 5_000)\r\n if (countRes.ok) {\r\n const { count: total } = await countRes.json()\r\n if (total > pageSize) {\r\n // Cap at 10 additional pages (~20 000 records max) to avoid browser memory issues\r\n const numPages = Math.min(Math.ceil((total - pageSize) / pageSize), 10)\r\n const morePages = await Promise.all(\r\n Array.from({ length: numPages }, (_, i) =>\r\n fetchWithTimeout(\r\n `${baseQuery}&resultOffset=${pageSize * (i + 1)}&resultRecordCount=${pageSize}`,\r\n 15_000,\r\n )\r\n .then(r => r.ok ? r.json() : null)\r\n .then(p => p?.features ?? [])\r\n .catch(() => []),\r\n ),\r\n )\r\n for (const page of morePages) allFeatures.push(...page)\r\n }\r\n }\r\n }\r\n catch { /* use first page only */ }\r\n }\r\n\r\n const result = { type: 'FeatureCollection', features: allFeatures } as AllGeoJSON\r\n // Viewport-constrained results cached for 15 min; global results for 1 hr\r\n setCache(featuresKey, result, bbox ? 15 * 60 * 1000 : 60 * 60 * 1000)\r\n return result\r\n }\r\n catch (error) {\r\n console.error(`ArcGIS: Error fetching features for layer ${layerIndex}:`, error)\r\n return { type: 'FeatureCollection', features: [] } as AllGeoJSON\r\n }\r\n finally {\r\n inFlightFeatureRequests.delete(featuresKey)\r\n }\r\n })()\r\n\r\n inFlightFeatureRequests.set(featuresKey, request)\r\n return request\r\n }\r\n\r\n const getFeaturesData = async () => {\r\n try {\r\n const res = await fetchWithTimeout(`${url}/${layerIndex}?f=pjson`)\r\n return res.ok ? res.json() : undefined\r\n }\r\n catch { return undefined }\r\n }\r\n\r\n const getFields = async () => {\r\n try {\r\n const fieldsKey = `fields:arcgis:${service.name}:${layerIndex}:${modTime}`\r\n const cached = getCache<any[]>(fieldsKey)\r\n if (cached) return cached\r\n\r\n const res = await fetchWithTimeout(featureInfoUrl)\r\n if (!res.ok) return []\r\n const featureData = await res.json()\r\n if (!featureData.fields) return []\r\n\r\n const typeMap = esriFieldTypes()\r\n\r\n const nameBasedType = (raw: any): FieldType | undefined => {\r\n const lc = (raw.name || '').toLowerCase()\r\n if (/(percentage|percent|ratio|average)/.test(lc)) return 'number'\r\n if (/(date|year|month|day|time)/.test(lc)) return 'datetime'\r\n return undefined\r\n }\r\n\r\n const inferFieldType = (raw: any): FieldType => {\r\n const norm = ([raw.type, raw.actualType, raw.sqlType, raw.esriType] as (string | undefined)[])\r\n .filter(Boolean).map(v => String(v).toLowerCase())\r\n if (norm.some(v => v.includes('boolean') || v === 'bool' || v === 'esrifieldtypeboolean')) return 'boolean'\r\n if (norm.some(v => /(double|float|integer|smallint|single|numeric|number|decimal)/.test(v)\r\n || ['esrifieldtypeinteger', 'esrifieldtypesmallinteger', 'esrifieldtypedouble', 'esrifieldtypefloat', 'esrifieldtypesingle'].includes(v))) return 'number'\r\n if (norm.some(v => /(date|time|timestamp)/.test(v))) return 'datetime'\r\n if (norm.some(v => /(globalid|guid|objectid|^oid$|_id$|id$)/.test(v))) return 'id'\r\n if (norm.some(v => v.includes('blob'))) return 'blob'\r\n if (norm.some(v => v.includes('raster') || v.includes('geometry'))) return 'object'\r\n return 'string'\r\n }\r\n\r\n const mapped = featureData.fields.map((field: any) => {\r\n const rawEsriType = field.type\r\n const inferred = nameBasedType(field) || typeMap[rawEsriType]?.type || inferFieldType(field)\r\n return { ...field, esriType: rawEsriType, type: inferred }\r\n })\r\n setCache(fieldsKey, mapped, 24 * 60 * 60 * 1000)\r\n return mapped\r\n }\r\n catch { return [] }\r\n }\r\n\r\n const baseName = formatName(service.name)\r\n const layerName = layer.name || `Layer ${layerIndex}`\r\n const name = layers.length > 1 ? `${baseName} - ${layerName}` : baseName\r\n\r\n return {\r\n id: extraInfo.id ? `${extraInfo.id}_${layerIndex}` : `${service.name}_${layerIndex}`,\r\n name,\r\n type: Array.isArray(service.layers) && service.layers[0]?.geometryType\r\n ? service.layers[0].geometryType\r\n : extraInfo.type || 'Feature Layer',\r\n datasetType: 'GeoJSON',\r\n dataManagementSystem: DataManagementSystem.Arcgis,\r\n url,\r\n license: extraInfo.licenseInfo || 'Unknown',\r\n description: extraInfo.description ? stripHtml(extraInfo.description) : 'No description available',\r\n publisher: extraInfo.owner || 'Unknown',\r\n contact: extraInfo.contact || 'Unknown',\r\n keywords: extraInfo.tags || extraInfo.typedKeywords || [],\r\n subject: extraInfo.subject || [],\r\n countrySubdivision,\r\n municipality,\r\n getFields,\r\n getFeatures,\r\n getFeaturesData,\r\n group,\r\n portal,\r\n layerColor: layerColorByName(name),\r\n } satisfies Dataset\r\n }),\r\n )\r\n\r\n return layerResults\r\n .filter((r): r is PromiseFulfilledResult<Dataset> => r.status === 'fulfilled' && r.value !== null)\r\n .map(r => r.value)\r\n }\r\n\r\n // Process at most 5 services concurrently — prevents ArcGIS rate-limiting\r\n const serviceResults = await withConcurrencyLimit(data.services, 5, processService)\r\n\r\n for (const result of serviceResults) {\r\n if (result.status === 'fulfilled') transformedDatasets.push(...result.value)\r\n }\r\n\r\n return transformedDatasets\r\n }\r\n catch (error) {\r\n console.error(`[ArcGIS] Failed to fetch datasets for portal \"${portal.name}\":`, error)\r\n return []\r\n }\r\n}\r\n\r\nexport interface EsriFieldTypes {\r\n [key: string]: { description: string, type: FieldType }\r\n}\r\n\r\nexport function esriFieldTypes(): EsriFieldTypes {\r\n return {\r\n esriFieldTypeString: { description: 'Text Field', type: 'string' },\r\n esriFieldTypeGlobalID: { description: 'Global ID', type: 'id' },\r\n esriFieldTypeGUID: { description: 'Global UID', type: 'id' },\r\n esriFieldTypeSmallInteger: { description: '16-bit Integer', type: 'number' },\r\n esriFieldTypeInteger: { description: '32-bit Integer', type: 'number' },\r\n esriFieldTypeSingle: { description: 'Single Precision Float', type: 'number' },\r\n esriFieldTypeDouble: { description: 'Double Precision Float', type: 'number' },\r\n esriFieldTypeDate: { description: 'Date/Time', type: 'datetime' },\r\n esriFieldTypedOnly: { description: 'Date', type: 'datetime' },\r\n esriFieldTypeTimeOnly: { description: 'Time', type: 'string' },\r\n esriFieldTypeTimestampOffset: { description: 'Timestamp', type: 'string' },\r\n esriFieldTypeBlob: { description: 'Binary', type: 'blob' },\r\n esriFieldTypeRaster: { description: 'Raster', type: 'object' },\r\n esriFieldTypeXML: { description: 'XML', type: 'string' },\r\n esriFieldTypeGeometry: { description: 'Spatial Geometry', type: 'object' },\r\n esriFieldTypeOID: { description: 'Object ID', type: 'id' },\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAKA,SAAS,4BAAiD;AAC1D,SAAS,wBAAwB;AACjC,SAAS,YAAY,iBAAiB;AAEtC,SAAS,oBAAoB,UAAU,UAAU,iBAAiB;AAIlE,MAAM,0BAA0B,oBAAI,IAAiC;AAOrE,eAAe,iBAAiB,KAAa,UAAU,KAA2B;AAChF,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,YAAY,WAAW,MAAM,WAAW,MAAM,GAAG,OAAO;AAC9D,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC;AAC/D,iBAAa,SAAS;AACtB,WAAO;AAAA,EACT,SACO,OAAO;AACZ,iBAAa,SAAS;AACtB,UAAM;AAAA,EACR;AACF;AAMA,eAAe,qBACb,OACA,OACA,IACoC;AACpC,QAAM,UAAqC,IAAI,MAAM,MAAM,MAAM;AACjE,MAAI,OAAO;AAEX,iBAAe,SAAS;AACtB,WAAO,OAAO,MAAM,QAAQ;AAC1B,YAAM,IAAI;AACV,UAAI;AACF,gBAAQ,CAAC,IAAI,EAAE,QAAQ,aAAa,OAAO,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE;AAAA,MAChE,SACO,QAAQ;AACb,gBAAQ,CAAC,IAAI,EAAE,QAAQ,YAAY,OAAO;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAQ;AAAA,IACZ,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,OAAO,MAAM,MAAM,EAAE,GAAG,MAAM;AAAA,EAC9D;AACA,SAAO;AACT;AAEA,eAAsB,oBACpB,QACA,aACoB;AAnEtB;AAoEE,QAAM,EAAE,QAAQ,oBAAoB,cAAc,MAAM,IAAI;AAE5D,MAAI;AACF,UAAM,WAAW,UAAU,IAAI,QAAQ,QAAQ,EAAE;AACjD,UAAM,cAAc,GAAG,OAAO;AAC9B,YAAQ,IAAI,0CAA0C,OAAO,IAAI,MAAM,WAAW,EAAE;AAEpF,UAAM,WAAW,MAAM,iBAAiB,WAAW;AACnD,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,aAAa,OAAO,IAAI,2CAAsC,SAAS,MAAM,EAAE;AAC7F,YAAM,IAAI,MAAM,uBAAuB,SAAS,MAAM,EAAE;AAAA,IAC1D;AAEA,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAQ,IAAI,aAAa,OAAO,IAAI,aAAO,gBAAK,aAAL,mBAAe,WAAf,YAAyB,CAAC,iBAAiB;AAEtF,UAAM,sBAAiC,CAAC;AAExC,UAAM,iBAAiB,OAAO,YAAqC;AACjE,YAAM,MAAM,GAAG,OAAO,aAAa,QAAQ,IAAI,IAAI,QAAQ,IAAI;AAC/D,YAAM,UAAU,GAAG,GAAG;AAEtB,YAAM,CAAC,SAAS,UAAU,IAAI,MAAM,QAAQ,WAAW;AAAA,QACrD,iBAAiB,OAAO;AAAA,QACxB,iBAAiB,GAAG,GAAG,UAAU;AAAA,MACnC,CAAC;AAED,UAAI,YAAiB,CAAC;AACtB,UAAI,cAAmB,CAAC;AAExB,UAAI,QAAQ,WAAW,eAAe,QAAQ,MAAM;AAClD,oBAAY,MAAM,QAAQ,MAAM,KAAK;AACvC,UAAI,WAAW,WAAW,eAAe,WAAW,MAAM;AACxD,sBAAc,MAAM,WAAW,MAAM,KAAK;AAE5C,YAAM,SAAS,YAAY,UAAU,YAAY,OAAO,SAAS,IAC7D,YAAY,OAAO,OAAO,CAAC,MAAW,EAAE,SAAS,eAAe,IAChE,CAAC,EAAE,IAAI,GAAG,MAAM,MAAM,cAAc,KAAK,CAAC;AAE9C,YAAM,qBAAqB,CAAC,aAAqB,aAC/C,UAAmB,UAAU,KAAK,KAAK,KAAK,KAAM,YAAY;AA5GtE,YAAAA,KAAAC;AA6GU,YAAI;AACF,gBAAM,MAAM,MAAM,iBAAiB,GAAG,WAAW,sBAAsB;AACvE,cAAI,CAAC,IAAI,GAAI,QAAO;AACpB,gBAAM,IAAI,MAAM,IAAI,KAAK;AACzB,gBAAID,MAAA,uBAAG,UAAH,gBAAAA,IAAU,WAAQC,MAAA,uBAAG,UAAH,gBAAAA,IAAU,SAAS,QAAO;AAChD,cAAI,CAAC,KAAK,EAAE,SAAS,uBAAuB,CAAC,MAAM,QAAQ,EAAE,QAAQ,EAAG,QAAO;AAC/E,iBAAO,EAAE,SAAS,KAAK,CAAC,MAAW,KAAK,EAAE,QAAQ;AAAA,QACpD,SACM;AAAE,iBAAO;AAAA,QAAM;AAAA,MACvB,CAAC;AAGH,YAAM,eAAe,MAAM,QAAQ;AAAA,QACjC,OAAO,IAAI,OAAO,UAAwC;AA1HlE,cAAAD,KAAAC,KAAA;AA2HU,gBAAM,aAAa,MAAM;AACzB,gBAAM,cAAc,GAAG,GAAG,IAAI,UAAU;AACxC,gBAAM,iBAAiB,GAAG,GAAG,IAAI,UAAU;AAE3C,gBAAM,WAAW;AAAA,YACf;AAAA,YACA,GAAG,QAAQ,IAAI,IAAI,UAAU;AAAA,cAC7BD,MAAA,2CAAa,iBAAb,gBAAAA,IAA2B,kBAAgB,uCAAW,aAAY;AAAA,UACpE;AACA,gBAAM,UAAU,MAAM,mBAAmB,aAAa,QAAQ;AAC9D,cAAI,CAAC,QAAS,QAAO;AAErB,gBAAM,YAAUC,MAAA,2CAAa,iBAAb,gBAAAA,IAA2B,kBAAgB,uCAAW,aAAY;AAOlF,gBAAM,cAAc,OAAO,YAAmD;AA9IxF,gBAAAD;AA+IY,kBAAM,QAAOA,MAAA,mCAAS,SAAT,OAAAA,MAAiB;AAC9B,kBAAM,OAAO,mCAAS;AAGtB,kBAAM,WAAW,OAAO,IAAI,MAAM,OAAO,KAAK,MAAO;AAGrD,gBAAI,gBAAgB;AACpB,gBAAI,MAAM;AACR,oBAAM,CAAC,MAAM,OAAO,MAAM,KAAK,IAAI;AACnC,oBAAM,UAAU;AAAA,gBACd,KAAK,UAAU,EAAE,MAAM,MAAM,MAAM,OAAO,MAAM,MAAM,MAAM,OAAO,kBAAkB,EAAE,MAAM,KAAK,EAAE,CAAC;AAAA,cACvG;AACA,8BAAgB,aAAa,OAAO;AAAA,YACtC;AAGA,kBAAM,UAAU,OACZ,KAAK,IAAI,QAAM,KAAK,MAAM,IAAI,EAAE,IAAI,IAAI,QAAQ,CAAC,CAAC,EAAE,KAAK,GAAG,IAC5D;AACJ,kBAAM,aAAa,KAAK,MAAM,OAAO,CAAC;AACtC,kBAAM,cAAc,mBAAmB,QAAQ,IAAI,IAAI,UAAU,IAAI,OAAO,KAAK,UAAU,IAAI,OAAO;AAEtG,kBAAM,SAAS,SAAqB,WAAW;AAC/C,gBAAI,OAAQ,QAAO;AAEnB,kBAAM,WAAW,wBAAwB,IAAI,WAAW;AACxD,gBAAI,SAAU,QAAO;AAErB,kBAAM,YAAY,GAAG,GAAG,IAAI,UAAU,2CAA2C,aAAa;AAE9F,kBAAM,WAAW,YAAY;AAC3B,kBAAI;AACF,sBAAM,WAAW,MAAM,iBAAiB,GAAG,SAAS,qCAAqC,QAAQ,IAAI,IAAM;AAC3G,oBAAI,CAAC,SAAS,GAAI,QAAO,EAAE,MAAM,qBAAqB,UAAU,CAAC,EAAE;AAEnE,sBAAM,YAAY,MAAM,SAAS,KAAK;AACtC,oBAAI,EAAC,uCAAW,UAAU,QAAO,EAAE,MAAM,qBAAqB,UAAU,CAAC,EAAE;AAE3E,sBAAM,cAAc,CAAC,GAAG,UAAU,QAAQ;AAI1C,oBAAI,UAAU,yBAAyB,UAAU,SAAS,UAAU,UAAU;AAC5E,sBAAI;AACF,0BAAM,WAAW,MAAM,iBAAiB,GAAG,SAAS,gCAAgC,GAAK;AACzF,wBAAI,SAAS,IAAI;AACf,4BAAM,EAAE,OAAO,MAAM,IAAI,MAAM,SAAS,KAAK;AAC7C,0BAAI,QAAQ,UAAU;AAEpB,8BAAM,WAAW,KAAK,IAAI,KAAK,MAAM,QAAQ,YAAY,QAAQ,GAAG,EAAE;AACtE,8BAAM,YAAY,MAAM,QAAQ;AAAA,0BAC9B,MAAM;AAAA,4BAAK,EAAE,QAAQ,SAAS;AAAA,4BAAG,CAAC,GAAG,MACnC;AAAA,8BACE,GAAG,SAAS,iBAAiB,YAAY,IAAI,EAAE,sBAAsB,QAAQ;AAAA,8BAC7E;AAAA,4BACF,EACG,KAAK,OAAK,EAAE,KAAK,EAAE,KAAK,IAAI,IAAI,EAChC,KAAK,OAAE;AAzMtC,kCAAAA;AAyMyC,sCAAAA,MAAA,uBAAG,aAAH,OAAAA,MAAe,CAAC;AAAA,6BAAC,EAC3B,MAAM,MAAM,CAAC,CAAC;AAAA,0BACnB;AAAA,wBACF;AACA,mCAAW,QAAQ,UAAW,aAAY,KAAK,GAAG,IAAI;AAAA,sBACxD;AAAA,oBACF;AAAA,kBACF,SACM;AAAA,kBAA4B;AAAA,gBACpC;AAEA,sBAAM,SAAS,EAAE,MAAM,qBAAqB,UAAU,YAAY;AAElE,yBAAS,aAAa,QAAQ,OAAO,KAAK,KAAK,MAAO,KAAK,KAAK,GAAI;AACpE,uBAAO;AAAA,cACT,SACO,OAAO;AACZ,wBAAQ,MAAM,6CAA6C,UAAU,KAAK,KAAK;AAC/E,uBAAO,EAAE,MAAM,qBAAqB,UAAU,CAAC,EAAE;AAAA,cACnD,UACA;AACE,wCAAwB,OAAO,WAAW;AAAA,cAC5C;AAAA,YACF,GAAG;AAEH,oCAAwB,IAAI,aAAa,OAAO;AAChD,mBAAO;AAAA,UACT;AAEA,gBAAM,kBAAkB,YAAY;AAClC,gBAAI;AACF,oBAAM,MAAM,MAAM,iBAAiB,GAAG,GAAG,IAAI,UAAU,UAAU;AACjE,qBAAO,IAAI,KAAK,IAAI,KAAK,IAAI;AAAA,YAC/B,SACM;AAAE,qBAAO;AAAA,YAAU;AAAA,UAC3B;AAEA,gBAAM,YAAY,YAAY;AAC5B,gBAAI;AACF,oBAAM,YAAY,iBAAiB,QAAQ,IAAI,IAAI,UAAU,IAAI,OAAO;AACxE,oBAAM,SAAS,SAAgB,SAAS;AACxC,kBAAI,OAAQ,QAAO;AAEnB,oBAAM,MAAM,MAAM,iBAAiB,cAAc;AACjD,kBAAI,CAAC,IAAI,GAAI,QAAO,CAAC;AACrB,oBAAM,cAAc,MAAM,IAAI,KAAK;AACnC,kBAAI,CAAC,YAAY,OAAQ,QAAO,CAAC;AAEjC,oBAAM,UAAU,eAAe;AAE/B,oBAAM,gBAAgB,CAAC,QAAoC;AACzD,sBAAM,MAAM,IAAI,QAAQ,IAAI,YAAY;AACxC,oBAAI,qCAAqC,KAAK,EAAE,EAAG,QAAO;AAC1D,oBAAI,6BAA6B,KAAK,EAAE,EAAG,QAAO;AAClD,uBAAO;AAAA,cACT;AAEA,oBAAM,iBAAiB,CAAC,QAAwB;AAC9C,sBAAM,OAAQ,CAAC,IAAI,MAAM,IAAI,YAAY,IAAI,SAAS,IAAI,QAAQ,EAC/D,OAAO,OAAO,EAAE,IAAI,OAAK,OAAO,CAAC,EAAE,YAAY,CAAC;AACnD,oBAAI,KAAK,KAAK,OAAK,EAAE,SAAS,SAAS,KAAK,MAAM,UAAU,MAAM,sBAAsB,EAAG,QAAO;AAClG,oBAAI,KAAK,KAAK,OAAK,gEAAgE,KAAK,CAAC,KACpF,CAAC,wBAAwB,6BAA6B,uBAAuB,sBAAsB,qBAAqB,EAAE,SAAS,CAAC,CAAC,EAAG,QAAO;AACpJ,oBAAI,KAAK,KAAK,OAAK,wBAAwB,KAAK,CAAC,CAAC,EAAG,QAAO;AAC5D,oBAAI,KAAK,KAAK,OAAK,0CAA0C,KAAK,CAAC,CAAC,EAAG,QAAO;AAC9E,oBAAI,KAAK,KAAK,OAAK,EAAE,SAAS,MAAM,CAAC,EAAG,QAAO;AAC/C,oBAAI,KAAK,KAAK,OAAK,EAAE,SAAS,QAAQ,KAAK,EAAE,SAAS,UAAU,CAAC,EAAG,QAAO;AAC3E,uBAAO;AAAA,cACT;AAEA,oBAAM,SAAS,YAAY,OAAO,IAAI,CAAC,UAAe;AA/QpE,oBAAAA;AAgRgB,sBAAM,cAAc,MAAM;AAC1B,sBAAM,WAAW,cAAc,KAAK,OAAKA,MAAA,QAAQ,WAAW,MAAnB,gBAAAA,IAAsB,SAAQ,eAAe,KAAK;AAC3F,uBAAO,iCAAK,QAAL,EAAY,UAAU,aAAa,MAAM,SAAS;AAAA,cAC3D,CAAC;AACD,uBAAS,WAAW,QAAQ,KAAK,KAAK,KAAK,GAAI;AAC/C,qBAAO;AAAA,YACT,SACM;AAAE,qBAAO,CAAC;AAAA,YAAE;AAAA,UACpB;AAEA,gBAAM,WAAW,WAAW,QAAQ,IAAI;AACxC,gBAAM,YAAY,MAAM,QAAQ,SAAS,UAAU;AACnD,gBAAM,OAAO,OAAO,SAAS,IAAI,GAAG,QAAQ,MAAM,SAAS,KAAK;AAEhE,iBAAO;AAAA,YACL,IAAI,UAAU,KAAK,GAAG,UAAU,EAAE,IAAI,UAAU,KAAK,GAAG,QAAQ,IAAI,IAAI,UAAU;AAAA,YAClF;AAAA,YACA,MAAM,MAAM,QAAQ,QAAQ,MAAM,OAAK,aAAQ,OAAO,CAAC,MAAhB,mBAAmB,gBACtD,QAAQ,OAAO,CAAC,EAAE,eAClB,UAAU,QAAQ;AAAA,YACtB,aAAa;AAAA,YACb,sBAAsB,qBAAqB;AAAA,YAC3C;AAAA,YACA,SAAS,UAAU,eAAe;AAAA,YAClC,aAAa,UAAU,cAAc,UAAU,UAAU,WAAW,IAAI;AAAA,YACxE,WAAW,UAAU,SAAS;AAAA,YAC9B,SAAS,UAAU,WAAW;AAAA,YAC9B,UAAU,UAAU,QAAQ,UAAU,iBAAiB,CAAC;AAAA,YACxD,SAAS,UAAU,WAAW,CAAC;AAAA,YAC/B;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,YAAY,iBAAiB,IAAI;AAAA,UACnC;AAAA,QACF,CAAC;AAAA,MACH;AAEA,aAAO,aACJ,OAAO,CAAC,MAA4C,EAAE,WAAW,eAAe,EAAE,UAAU,IAAI,EAChG,IAAI,OAAK,EAAE,KAAK;AAAA,IACrB;AAGA,UAAM,iBAAiB,MAAM,qBAAqB,KAAK,UAAU,GAAG,cAAc;AAElF,eAAW,UAAU,gBAAgB;AACnC,UAAI,OAAO,WAAW,YAAa,qBAAoB,KAAK,GAAG,OAAO,KAAK;AAAA,IAC7E;AAEA,WAAO;AAAA,EACT,SACO,OAAO;AACZ,YAAQ,MAAM,iDAAiD,OAAO,IAAI,MAAM,KAAK;AACrF,WAAO,CAAC;AAAA,EACV;AACF;AAMO,SAAS,iBAAiC;AAC/C,SAAO;AAAA,IACL,qBAAqB,EAAE,aAAa,cAAc,MAAM,SAAS;AAAA,IACjE,uBAAuB,EAAE,aAAa,aAAa,MAAM,KAAK;AAAA,IAC9D,mBAAmB,EAAE,aAAa,cAAc,MAAM,KAAK;AAAA,IAC3D,2BAA2B,EAAE,aAAa,kBAAkB,MAAM,SAAS;AAAA,IAC3E,sBAAsB,EAAE,aAAa,kBAAkB,MAAM,SAAS;AAAA,IACtE,qBAAqB,EAAE,aAAa,0BAA0B,MAAM,SAAS;AAAA,IAC7E,qBAAqB,EAAE,aAAa,0BAA0B,MAAM,SAAS;AAAA,IAC7E,mBAAmB,EAAE,aAAa,aAAa,MAAM,WAAW;AAAA,IAChE,oBAAoB,EAAE,aAAa,QAAQ,MAAM,WAAW;AAAA,IAC5D,uBAAuB,EAAE,aAAa,QAAQ,MAAM,SAAS;AAAA,IAC7D,8BAA8B,EAAE,aAAa,aAAa,MAAM,SAAS;AAAA,IACzE,mBAAmB,EAAE,aAAa,UAAU,MAAM,OAAO;AAAA,IACzD,qBAAqB,EAAE,aAAa,UAAU,MAAM,SAAS;AAAA,IAC7D,kBAAkB,EAAE,aAAa,OAAO,MAAM,SAAS;AAAA,IACvD,uBAAuB,EAAE,aAAa,oBAAoB,MAAM,SAAS;AAAA,IACzE,kBAAkB,EAAE,aAAa,aAAa,MAAM,KAAK;AAAA,EAC3D;AACF;","names":["_a","_b"]}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../src/core/components/viewers/map/datasets/src/arcGISDatasets.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport { type AllGeoJSON } from '@turf/turf'\r\n\r\nimport { DataManagementSystem, type OpenDataPortal } from '../../../../../types/dbTypes'\r\nimport { layerColorByName } from '../../utils/stringToColour'\r\nimport { formatName, stripHtml } from '../utils'\r\n\r\nimport { buildValidationKey, getCache, setCache, withCache } from './cache'\r\n\r\nimport type { Dataset, FieldType } from '../../../../../types/datasetTypes'\r\n\r\nconst inFlightFeatureRequests = new Map<string, Promise<AllGeoJSON>>()\r\n\r\nexport type ViewportOptions = {\r\n bbox?: [number, number, number, number] // [west, south, east, north]\r\n zoom?: number\r\n}\r\n\r\nasync function fetchWithTimeout(url: string, timeout = 10_000): Promise<Response> {\r\n const controller = new AbortController()\r\n const timeoutId = setTimeout(() => controller.abort(), timeout)\r\n try {\r\n const response = await fetch(url, { signal: controller.signal })\r\n clearTimeout(timeoutId)\r\n return response\r\n }\r\n catch (error) {\r\n clearTimeout(timeoutId)\r\n throw error\r\n }\r\n}\r\n\r\n/**\r\n * Run `fn` over `items` with at most `limit` concurrent executions.\r\n * Preserves order of results matching input order.\r\n */\r\nasync function withConcurrencyLimit<T, R>(\r\n items: T[],\r\n limit: number,\r\n fn: (item: T) => Promise<R>,\r\n): Promise<PromiseSettledResult<R>[]> {\r\n const results: PromiseSettledResult<R>[] = new Array(items.length)\r\n let next = 0\r\n\r\n async function worker() {\r\n while (next < items.length) {\r\n const i = next++\r\n try {\r\n results[i] = { status: 'fulfilled', value: await fn(items[i]) }\r\n }\r\n catch (reason) {\r\n results[i] = { status: 'rejected', reason }\r\n }\r\n }\r\n }\r\n\r\n await Promise.all(\r\n Array.from({ length: Math.min(limit, items.length) }, worker),\r\n )\r\n return results\r\n}\r\n\r\nexport async function fetchArcGISDatasets(\r\n portal: OpenDataPortal,\r\n rowsPerPage: number,\r\n): Promise<Dataset[]> {\r\n const { apiUrl, countrySubdivision, municipality, group } = portal\r\n\r\n try {\r\n const baseUrl = (apiUrl || '').replace(/\\/+$/, '')\r\n const servicesUrl = `${baseUrl}/services?f=pjson`\r\n\r\n const response = await fetchWithTimeout(servicesUrl)\r\n if (!response.ok) {\r\n console.error(`[ArcGIS] \"${portal.name}\" services request failed — status ${response.status}`)\r\n throw new Error(`HTTP error! status: ${response.status}`)\r\n }\r\n\r\n const data = await response.json()\r\n\r\n const transformedDatasets: Dataset[] = []\r\n\r\n const processService = async (service: any): Promise<Dataset[]> => {\r\n const url = `${baseUrl}/services/${service.name}/${service.type}`\r\n const infoUrl = `${url}/info/itemInfo?f=pjson`\r\n\r\n const [infoRes, serviceRes] = await Promise.allSettled([\r\n fetchWithTimeout(infoUrl),\r\n fetchWithTimeout(`${url}?f=pjson`),\r\n ])\r\n\r\n let extraInfo: any = {}\r\n let serviceInfo: any = {}\r\n\r\n if (infoRes.status === 'fulfilled' && infoRes.value.ok)\r\n extraInfo = await infoRes.value.json()\r\n if (serviceRes.status === 'fulfilled' && serviceRes.value.ok)\r\n serviceInfo = await serviceRes.value.json()\r\n\r\n const layers = serviceInfo.layers && serviceInfo.layers.length > 0\r\n ? serviceInfo.layers.filter((l: any) => l.type === 'Feature Layer')\r\n : [{ id: 0, name: null, geometryType: null }]\r\n\r\n const hasNonNullGeometry = (featuresUrl: string, cacheKey: string): Promise<boolean> =>\r\n withCache<boolean>(cacheKey, 24 * 60 * 60 * 1000, async () => {\r\n try {\r\n const res = await fetchWithTimeout(`${featuresUrl}&resultRecordCount=1`)\r\n if (!res.ok) return false\r\n const d = await res.json()\r\n if (d?.error?.code || d?.error?.message) return false\r\n if (!d || d.type !== 'FeatureCollection' || !Array.isArray(d.features)) return false\r\n return d.features.some((f: any) => f && f.geometry)\r\n }\r\n catch { return false }\r\n })\r\n\r\n // Layers within a service are few (2-5 typically) — parallel is fine here\r\n const layerResults = await Promise.allSettled(\r\n layers.map(async (layer: any): Promise<Dataset | null> => {\r\n const layerIndex = layer.id\r\n const featuresUrl = `${url}/${layerIndex}/query?outFields=*&where=1%3D1&f=geojson`\r\n const featureInfoUrl = `${url}/${layerIndex}?f=pjson`\r\n\r\n const validKey = buildValidationKey(\r\n 'arcgis',\r\n `${service.name}_${layerIndex}`,\r\n serviceInfo?.documentInfo?.ModifiedTime || extraInfo?.modified || undefined,\r\n )\r\n const hasGeom = await hasNonNullGeometry(featuresUrl, validKey)\r\n if (!hasGeom) return null\r\n\r\n const modTime = serviceInfo?.documentInfo?.ModifiedTime || extraInfo?.modified || ''\r\n\r\n /**\r\n * getFeatures — viewport-aware, paginated.\r\n * If `bbox` is provided, only features intersecting the viewport are fetched.\r\n * Automatically paginates beyond the per-page limit.\r\n */\r\n const getFeatures = async (options?: ViewportOptions): Promise<AllGeoJSON> => {\r\n const zoom = options?.zoom ?? 10\r\n const bbox = options?.bbox\r\n\r\n // Page size scales with zoom: fewer records at low zoom (overview), more at high zoom (detail)\r\n const pageSize = zoom < 6 ? 500 : zoom < 10 ? 1000 : 2000\r\n\r\n // Build spatial filter string for ArcGIS when viewport is available\r\n let spatialFilter = ''\r\n if (bbox) {\r\n const [west, south, east, north] = bbox\r\n const geomStr = encodeURIComponent(\r\n JSON.stringify({ xmin: west, ymin: south, xmax: east, ymax: north, spatialReference: { wkid: 4326 } }),\r\n )\r\n spatialFilter = `&geometry=${geomStr}&geometryType=esriGeometryEnvelope&spatialRel=esriSpatialRelIntersects&inSR=4326`\r\n }\r\n\r\n // Round bbox to 0.1° grid to maximise cache hits while panning\r\n const bboxKey = bbox\r\n ? bbox.map(n => (Math.round(n * 10) / 10).toFixed(1)).join(',')\r\n : 'global'\r\n const zoomBucket = Math.floor(zoom / 2)\r\n const featuresKey = `features:arcgis:${service.name}_${layerIndex}:${bboxKey}:z${zoomBucket}:${modTime}`\r\n\r\n const cached = getCache<AllGeoJSON>(featuresKey)\r\n if (cached) return cached\r\n\r\n const inFlight = inFlightFeatureRequests.get(featuresKey)\r\n if (inFlight) return inFlight\r\n\r\n const baseQuery = `${url}/${layerIndex}/query?outFields=*&where=1%3D1&f=geojson${spatialFilter}`\r\n\r\n const request = (async () => {\r\n try {\r\n const firstRes = await fetchWithTimeout(`${baseQuery}&resultOffset=0&resultRecordCount=${pageSize}`, 15_000)\r\n if (!firstRes.ok) return { type: 'FeatureCollection', features: [] } as AllGeoJSON\r\n\r\n const firstPage = await firstRes.json()\r\n if (!firstPage?.features) return { type: 'FeatureCollection', features: [] } as AllGeoJSON\r\n\r\n const allFeatures = [...firstPage.features]\r\n\r\n // If the server returned fewer features than the requested page size, the first page is complete.\r\n // This avoids an unnecessary count request for small datasets that have already finished loading.\r\n if (firstPage.exceededTransferLimit && firstPage.features.length >= pageSize) {\r\n try {\r\n const countRes = await fetchWithTimeout(`${baseQuery}&returnCountOnly=true&f=json`, 5_000)\r\n if (countRes.ok) {\r\n const { count: total } = await countRes.json()\r\n if (total > pageSize) {\r\n // Cap at 10 additional pages (~20 000 records max) to avoid browser memory issues\r\n const numPages = Math.min(Math.ceil((total - pageSize) / pageSize), 10)\r\n const morePages = await Promise.all(\r\n Array.from({ length: numPages }, (_, i) =>\r\n fetchWithTimeout(\r\n `${baseQuery}&resultOffset=${pageSize * (i + 1)}&resultRecordCount=${pageSize}`,\r\n 15_000,\r\n )\r\n .then(r => r.ok ? r.json() : null)\r\n .then(p => p?.features ?? [])\r\n .catch(() => []),\r\n ),\r\n )\r\n for (const page of morePages) allFeatures.push(...page)\r\n }\r\n }\r\n }\r\n catch { /* use first page only */ }\r\n }\r\n\r\n const result = { type: 'FeatureCollection', features: allFeatures } as AllGeoJSON\r\n // Viewport-constrained results cached for 15 min; global results for 1 hr\r\n setCache(featuresKey, result, bbox ? 15 * 60 * 1000 : 60 * 60 * 1000)\r\n return result\r\n }\r\n catch (error) {\r\n console.error(`ArcGIS: Error fetching features for layer ${layerIndex}:`, error)\r\n return { type: 'FeatureCollection', features: [] } as AllGeoJSON\r\n }\r\n finally {\r\n inFlightFeatureRequests.delete(featuresKey)\r\n }\r\n })()\r\n\r\n inFlightFeatureRequests.set(featuresKey, request)\r\n return request\r\n }\r\n\r\n const getFeaturesData = async () => {\r\n try {\r\n const res = await fetchWithTimeout(`${url}/${layerIndex}?f=pjson`)\r\n return res.ok ? res.json() : undefined\r\n }\r\n catch { return undefined }\r\n }\r\n\r\n const getFields = async () => {\r\n try {\r\n const fieldsKey = `fields:arcgis:${service.name}:${layerIndex}:${modTime}`\r\n const cached = getCache<any[]>(fieldsKey)\r\n if (cached) return cached\r\n\r\n const res = await fetchWithTimeout(featureInfoUrl)\r\n if (!res.ok) return []\r\n const featureData = await res.json()\r\n if (!featureData.fields) return []\r\n\r\n const typeMap = esriFieldTypes()\r\n\r\n const nameBasedType = (raw: any): FieldType | undefined => {\r\n const lc = (raw.name || '').toLowerCase()\r\n if (/(percentage|percent|ratio|average)/.test(lc)) return 'number'\r\n if (/(date|year|month|day|time)/.test(lc)) return 'datetime'\r\n return undefined\r\n }\r\n\r\n const inferFieldType = (raw: any): FieldType => {\r\n const norm = ([raw.type, raw.actualType, raw.sqlType, raw.esriType] as (string | undefined)[])\r\n .filter(Boolean).map(v => String(v).toLowerCase())\r\n if (norm.some(v => v.includes('boolean') || v === 'bool' || v === 'esrifieldtypeboolean')) return 'boolean'\r\n if (norm.some(v => /(double|float|integer|smallint|single|numeric|number|decimal)/.test(v)\r\n || ['esrifieldtypeinteger', 'esrifieldtypesmallinteger', 'esrifieldtypedouble', 'esrifieldtypefloat', 'esrifieldtypesingle'].includes(v))) return 'number'\r\n if (norm.some(v => /(date|time|timestamp)/.test(v))) return 'datetime'\r\n if (norm.some(v => /(globalid|guid|objectid|^oid$|_id$|id$)/.test(v))) return 'id'\r\n if (norm.some(v => v.includes('blob'))) return 'blob'\r\n if (norm.some(v => v.includes('raster') || v.includes('geometry'))) return 'object'\r\n return 'string'\r\n }\r\n\r\n const mapped = featureData.fields.map((field: any) => {\r\n const rawEsriType = field.type\r\n const inferred = nameBasedType(field) || typeMap[rawEsriType]?.type || inferFieldType(field)\r\n return { ...field, esriType: rawEsriType, type: inferred }\r\n })\r\n setCache(fieldsKey, mapped, 24 * 60 * 60 * 1000)\r\n return mapped\r\n }\r\n catch { return [] }\r\n }\r\n\r\n const baseName = formatName(service.name)\r\n const layerName = layer.name || `Layer ${layerIndex}`\r\n const name = layers.length > 1 ? `${baseName} - ${layerName}` : baseName\r\n\r\n return {\r\n id: extraInfo.id ? `${extraInfo.id}_${layerIndex}` : `${service.name}_${layerIndex}`,\r\n name,\r\n type: Array.isArray(service.layers) && service.layers[0]?.geometryType\r\n ? service.layers[0].geometryType\r\n : extraInfo.type || 'Feature Layer',\r\n datasetType: 'GeoJSON',\r\n dataManagementSystem: DataManagementSystem.Arcgis,\r\n url,\r\n license: extraInfo.licenseInfo || 'Unknown',\r\n description: extraInfo.description ? stripHtml(extraInfo.description) : 'No description available',\r\n publisher: extraInfo.owner || 'Unknown',\r\n contact: extraInfo.contact || 'Unknown',\r\n keywords: extraInfo.tags || extraInfo.typedKeywords || [],\r\n subject: extraInfo.subject || [],\r\n countrySubdivision,\r\n municipality,\r\n getFields,\r\n getFeatures,\r\n getFeaturesData,\r\n group,\r\n portal,\r\n layerColor: layerColorByName(name),\r\n } satisfies Dataset\r\n }),\r\n )\r\n\r\n return layerResults\r\n .filter((r): r is PromiseFulfilledResult<Dataset> => r.status === 'fulfilled' && r.value !== null)\r\n .map(r => r.value)\r\n }\r\n\r\n // Process at most 5 services concurrently — prevents ArcGIS rate-limiting\r\n const serviceResults = await withConcurrencyLimit(data.services, 5, processService)\r\n\r\n for (const result of serviceResults) {\r\n if (result.status === 'fulfilled') transformedDatasets.push(...result.value)\r\n }\r\n\r\n return transformedDatasets\r\n }\r\n catch (error) {\r\n console.error(`[ArcGIS] Failed to fetch datasets for portal \"${portal.name}\":`, error)\r\n return []\r\n }\r\n}\r\n\r\nexport interface EsriFieldTypes {\r\n [key: string]: { description: string, type: FieldType }\r\n}\r\n\r\nexport function esriFieldTypes(): EsriFieldTypes {\r\n return {\r\n esriFieldTypeString: { description: 'Text Field', type: 'string' },\r\n esriFieldTypeGlobalID: { description: 'Global ID', type: 'id' },\r\n esriFieldTypeGUID: { description: 'Global UID', type: 'id' },\r\n esriFieldTypeSmallInteger: { description: '16-bit Integer', type: 'number' },\r\n esriFieldTypeInteger: { description: '32-bit Integer', type: 'number' },\r\n esriFieldTypeSingle: { description: 'Single Precision Float', type: 'number' },\r\n esriFieldTypeDouble: { description: 'Double Precision Float', type: 'number' },\r\n esriFieldTypeDate: { description: 'Date/Time', type: 'datetime' },\r\n esriFieldTypedOnly: { description: 'Date', type: 'datetime' },\r\n esriFieldTypeTimeOnly: { description: 'Time', type: 'string' },\r\n esriFieldTypeTimestampOffset: { description: 'Timestamp', type: 'string' },\r\n esriFieldTypeBlob: { description: 'Binary', type: 'blob' },\r\n esriFieldTypeRaster: { description: 'Raster', type: 'object' },\r\n esriFieldTypeXML: { description: 'XML', type: 'string' },\r\n esriFieldTypeGeometry: { description: 'Spatial Geometry', type: 'object' },\r\n esriFieldTypeOID: { description: 'Object ID', type: 'id' },\r\n }\r\n}\r\n"],"mappings":"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","names":["_a"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ckanDatasets.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/components/viewers/map/datasets/src/ckanDatasets.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAA;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAOlE,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,OAAO,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"ckanDatasets.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/components/viewers/map/datasets/src/ckanDatasets.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAA;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAOlE,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,OAAO,EAAE,CAAC,CA4QpB;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,CAAA;CACxD"}
|
|
@@ -122,12 +122,9 @@ async function fetchCkanDatasets(portal) {
|
|
|
122
122
|
return [];
|
|
123
123
|
};
|
|
124
124
|
const getFeatures = async () => {
|
|
125
|
-
var _a2, _b2;
|
|
126
125
|
const featuresKey = `features:ckan:${geoJsonResource.id}:${pkg.metadata_modified || pkg.metadata_created || ""}`;
|
|
127
126
|
const cached = getCache(featuresKey);
|
|
128
127
|
if (cached) {
|
|
129
|
-
const cachedCount = "features" in cached ? (_b2 = (_a2 = cached.features) == null ? void 0 : _a2.length) != null ? _b2 : 0 : 0;
|
|
130
|
-
console.log(`\u2705 CKAN getFeatures: Returning ${cachedCount} cached features for "${pkg.name}"`);
|
|
131
128
|
return cached;
|
|
132
129
|
}
|
|
133
130
|
const limit = 5e3;
|
|
@@ -145,10 +142,10 @@ async function fetchCkanDatasets(portal) {
|
|
|
145
142
|
let featuresCollection;
|
|
146
143
|
if (data2.result && Array.isArray(data2.result.records)) {
|
|
147
144
|
const features = data2.result.records.map((properties) => {
|
|
148
|
-
var
|
|
145
|
+
var _a2;
|
|
149
146
|
try {
|
|
150
147
|
let geometry;
|
|
151
|
-
geometry = ((
|
|
148
|
+
geometry = ((_a2 = properties.geometry) == null ? void 0 : _a2.type) ? properties.geometry : JSON.parse(properties.geometry);
|
|
152
149
|
return {
|
|
153
150
|
type: "Feature",
|
|
154
151
|
geometry,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../src/core/components/viewers/map/datasets/src/ckanDatasets.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport { layerColorByName } from '../../utils/stringToColour'\r\nimport { formatName, stripHtml } from '../utils'\r\n\r\nimport { buildValidationKey, getCache, setCache, withCache } from './cache'\r\n\r\nimport type { Dataset, FieldType } from '../../../../../types/datasetTypes'\r\nimport type { OpenDataPortal } from '../../../../../types/dbTypes'\r\nimport type { AllGeoJSON } from '@turf/turf'\r\n\r\n\r\n\r\n\r\n// Fetch CKAN GeoJSON datasets and transform to Dataset array\r\nexport async function fetchCkanDatasets(\r\n portal: OpenDataPortal,\r\n): Promise<Dataset[]> {\r\n const { apiUrl, countrySubdivision, municipality, group } = portal\r\n const isServer = typeof window === 'undefined'\r\n\r\n const buildUrl = (target: string) => {\r\n if (isServer) {\r\n return target\r\n }\r\n\r\n const proxied = `/api/ckanProxy?url=${encodeURIComponent(target)}`\r\n return proxied\r\n }\r\n\r\n // https://www.donneesquebec.ca/recherche/dataset/?sort=metadata_created+desc&_organization_limit=0&res_format=GeoJSON\r\n\r\n try {\r\n // Use your local API route to proxy the CKAN request when on the client\r\n const geoJsonListTarget = `${apiUrl}/api/3/action/package_search?fq=res_format:(geojson OR GeoJSON)&rows=100000`\r\n const response = await fetch(buildUrl(geoJsonListTarget))\r\n\r\n if (!response.ok) {\r\n return []\r\n }\r\n\r\n const data = await response.json()\r\n\r\n if (!data.success || !data.result || !Array.isArray(data.result.results)) {\r\n throw new Error('Invalid CKAN response structure')\r\n }\r\n\r\n const transformedDatasets: Dataset[] = (\r\n await Promise.all(\r\n data.result.results.map(async (pkg: any) => {\r\n // Find first GeoJSON resource\r\n const geoJsonResource = pkg.resources?.find(\r\n (resource: any) => resource.format?.toLowerCase() === 'geojson',\r\n )\r\n\r\n // Exclude if no GeoJSON resource\r\n if (!geoJsonResource) return\r\n\r\n // Exclude if not active\r\n const datastore = geoJsonResource.datastore_active?.toString().toLowerCase() === 'true'\r\n const isActive = datastore || geoJsonResource.state === 'active'\r\n if (!(datastore && geoJsonResource.url && isActive)) return\r\n\r\n // Determine the correct URL based on your working method\r\n let resourceUrl: string | null = null\r\n let resourceTarget: string | null = null\r\n let fieldsUrl: string | null = null\r\n\r\n if (datastore) {\r\n resourceTarget = `${apiUrl}/api/3/action/datastore_search?resource_id=${geoJsonResource.id}&limit=1000000`\r\n resourceUrl = buildUrl(resourceTarget)\r\n fieldsUrl = buildUrl(`${apiUrl}/api/3/action/datastore_search?resource_id=${geoJsonResource.id}&limit=0`)\r\n }\r\n else {\r\n resourceTarget = geoJsonResource.url\r\n resourceUrl = resourceTarget ? buildUrl(resourceTarget) : null\r\n }\r\n\r\n try {\r\n const sampleTarget = resourceTarget?.includes('datastore_search')\r\n ? resourceTarget.replace('limit=1000000', 'limit=1')\r\n : resourceTarget\r\n const sampleUrl = sampleTarget\r\n ? (sampleTarget.includes('limit=') ? buildUrl(sampleTarget) : buildUrl(`${sampleTarget}?limit=1`))\r\n : null\r\n\r\n if (sampleUrl) {\r\n const validKey = buildValidationKey('ckan', pkg.id, pkg.metadata_modified || pkg.metadata_created || undefined)\r\n const isValid = await withCache<boolean>(validKey, 24 * 60 * 60 * 1000, async () => {\r\n const sampleRes = await fetch(sampleUrl)\r\n if (sampleRes.ok) {\r\n const sample = await sampleRes.json()\r\n if (sample?.error) {\r\n const code = sample.error.code\r\n const msg = `${sample.error.message || ''} ${sample.error.messageCode || ''}`\r\n if (code === 499 || code === 504 || /token required|unauthorized|forbidden|timed out/i.test(msg)) return false\r\n }\r\n let hasGeom = true\r\n if (sample?.result?.records) {\r\n const rec = sample.result.records[0]\r\n const geom = rec?.geometry || (typeof rec?.geometry === 'string' ? JSON.parse(rec.geometry) : undefined)\r\n hasGeom = !!geom\r\n }\r\n else if (Array.isArray(sample?.features)) {\r\n hasGeom = sample.features.some((f: any) => f && f.geometry)\r\n }\r\n return !!hasGeom\r\n } else if ([401, 403, 504].includes(sampleRes.status)) {\r\n return false\r\n }\r\n return false\r\n })\r\n if (!isValid) return\r\n }\r\n }\r\n catch {\r\n }\r\n\r\n const getFeaturesData = async () => {\r\n if (resourceUrl) {\r\n try {\r\n const response = await fetch(fieldsUrl ?? resourceUrl)\r\n if (!response.ok) {\r\n console.error('Network response was not ok:', response.status)\r\n return\r\n }\r\n return await response.json()\r\n }\r\n catch (error) {\r\n console.error('Error fetching features data:', error)\r\n return\r\n }\r\n }\r\n }\r\n\r\n const getFields = async () => {\r\n if (fieldsUrl) {\r\n try {\r\n const fieldsKey = `fields:ckan:${geoJsonResource.id}:${pkg.metadata_modified || pkg.metadata_created || ''}`\r\n const cached = getCache<any[]>(fieldsKey)\r\n if (cached) return cached\r\n const dsRes = await fetch(fieldsUrl)\r\n if (dsRes.ok) {\r\n const dsData = await dsRes.json()\r\n const fields = (\r\n dsData.result.fields?.map((field: any) => ({\r\n name: field.id,\r\n type: ckanFieldTypes[field.type]?.type || field.type,\r\n ckanType: field.type,\r\n description: field.info || '',\r\n })) || []\r\n )\r\n setCache(fieldsKey, fields, 24 * 60 * 60 * 1000)\r\n return fields\r\n }\r\n }\r\n catch {\r\n return []\r\n }\r\n }\r\n return []\r\n }\r\n\r\n const getFeatures: () => Promise<AllGeoJSON> = async () => {\r\n // Check cache first\r\n const featuresKey = `features:ckan:${geoJsonResource.id}:${pkg.metadata_modified || pkg.metadata_created || ''}`\r\n const cached = getCache<AllGeoJSON>(featuresKey)\r\n if (cached) {\r\n const cachedCount = 'features' in cached ? cached.features?.length ?? 0 : 0\r\n console.log(`✅ CKAN getFeatures: Returning ${cachedCount} cached features for \"${pkg.name}\"`)\r\n return cached\r\n }\r\n\r\n const limit = 5_000\r\n const url2 = resourceUrl?.includes('datastore_search')\r\n ? `${resourceUrl}&limit=${limit}`\r\n : resourceUrl\r\n\r\n try {\r\n const response = await fetch(url2)\r\n if (!response.ok) {\r\n console.error('CKAN: Network response was not ok:', response.status)\r\n return {\r\n type: 'FeatureCollection',\r\n features: [],\r\n } as AllGeoJSON\r\n }\r\n const data = await response.json()\r\n let featuresCollection: AllGeoJSON | undefined\r\n\r\n if (data.result && Array.isArray(data.result.records)) {\r\n // CKAN Datastore: build FeatureCollection from records\r\n const features = data.result.records\r\n .map((properties: any) => {\r\n try {\r\n let geometry\r\n geometry = properties.geometry?.type ? properties.geometry : JSON.parse(properties.geometry)\r\n\r\n return {\r\n type: 'Feature',\r\n geometry,\r\n properties,\r\n }\r\n }\r\n catch {\r\n return null\r\n }\r\n })\r\n .filter((f: any) => f !== null)\r\n\r\n featuresCollection = {\r\n type: 'FeatureCollection',\r\n features,\r\n }\r\n }\r\n else if (\r\n data.type === 'FeatureCollection'\r\n && Array.isArray(data.features)\r\n ) {\r\n // Already a FeatureCollection\r\n featuresCollection = data\r\n }\r\n else {\r\n console.error(\r\n 'CKAN: No features available or unexpected response:',\r\n data,\r\n )\r\n return {\r\n type: 'FeatureCollection',\r\n features: [],\r\n } as AllGeoJSON\r\n }\r\n\r\n // Cache the result for 1 hour\r\n if (featuresCollection) {\r\n setCache(featuresKey, featuresCollection, 60 * 60 * 1000)\r\n }\r\n\r\n return featuresCollection\r\n }\r\n catch (error) {\r\n console.error('CKAN: Error processing dataset:', error)\r\n return {\r\n type: 'FeatureCollection',\r\n features: [],\r\n } as AllGeoJSON\r\n }\r\n }\r\n\r\n const name = formatName(pkg.title || pkg.name)\r\n\r\n return {\r\n id: pkg.id,\r\n name,\r\n url: fieldsUrl,\r\n publisher:\r\n pkg.organization?.title || pkg.organization?.name || 'Unknown',\r\n dataManagementSystem: 'ckan',\r\n license: pkg.license_title || pkg.license_id || 'Unknown',\r\n contact: pkg.author || pkg.maintainer || 'Unknown',\r\n keywords: pkg.tags?.map((tag: any) => tag.name) || [],\r\n description: pkg.notes\r\n ? stripHtml(pkg.notes)\r\n : 'No description available',\r\n dateReleased: pkg.metadata_created || '',\r\n dateUpdated: pkg.metadata_modified || '',\r\n information: pkg.information_url || '',\r\n countrySubdivision,\r\n municipality,\r\n type: pkg.type || 'GeoJSON',\r\n portal,\r\n getFields,\r\n getFeatures,\r\n getFeaturesData,\r\n group,\r\n layerColor: layerColorByName(name),\r\n }\r\n }),\r\n )\r\n ).filter(Boolean) // Remove nulls (excluded datasets)\r\n\r\n return transformedDatasets\r\n }\r\n catch (error) {\r\n console.error('Error fetching CKAN GeoJSON datasets:', error)\r\n return []\r\n }\r\n}\r\n\r\nexport interface CkanFieldTypes {\r\n [key: string]: { description: string, type: FieldType }\r\n}\r\n\r\nfunction ckanFieldTypes(): CkanFieldTypes {\r\n return {\r\n text: { description: 'Text', type: 'string' },\r\n int: { description: 'Integer', type: 'number' },\r\n float: { description: 'Float', type: 'number' },\r\n bool: { description: 'Boolean', type: 'boolean' },\r\n date: { description: 'Date', type: 'datetime' },\r\n timestamp: { description: 'Timestamp', type: 'datetime' },\r\n json: { description: 'JSON', type: 'object' },\r\n // Add more CKAN field types as needed\r\n }\r\n}\r\n"],"mappings":"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","names":["_a","response","_b","data"]}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../src/core/components/viewers/map/datasets/src/ckanDatasets.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport { layerColorByName } from '../../utils/stringToColour'\r\nimport { formatName, stripHtml } from '../utils'\r\n\r\nimport { buildValidationKey, getCache, setCache, withCache } from './cache'\r\n\r\nimport type { Dataset, FieldType } from '../../../../../types/datasetTypes'\r\nimport type { OpenDataPortal } from '../../../../../types/dbTypes'\r\nimport type { AllGeoJSON } from '@turf/turf'\r\n\r\n\r\n\r\n\r\n// Fetch CKAN GeoJSON datasets and transform to Dataset array\r\nexport async function fetchCkanDatasets(\r\n portal: OpenDataPortal,\r\n): Promise<Dataset[]> {\r\n const { apiUrl, countrySubdivision, municipality, group } = portal\r\n const isServer = typeof window === 'undefined'\r\n\r\n const buildUrl = (target: string) => {\r\n if (isServer) {\r\n return target\r\n }\r\n\r\n const proxied = `/api/ckanProxy?url=${encodeURIComponent(target)}`\r\n return proxied\r\n }\r\n\r\n // https://www.donneesquebec.ca/recherche/dataset/?sort=metadata_created+desc&_organization_limit=0&res_format=GeoJSON\r\n\r\n try {\r\n // Use your local API route to proxy the CKAN request when on the client\r\n const geoJsonListTarget = `${apiUrl}/api/3/action/package_search?fq=res_format:(geojson OR GeoJSON)&rows=100000`\r\n const response = await fetch(buildUrl(geoJsonListTarget))\r\n\r\n if (!response.ok) {\r\n return []\r\n }\r\n\r\n const data = await response.json()\r\n\r\n if (!data.success || !data.result || !Array.isArray(data.result.results)) {\r\n throw new Error('Invalid CKAN response structure')\r\n }\r\n\r\n const transformedDatasets: Dataset[] = (\r\n await Promise.all(\r\n data.result.results.map(async (pkg: any) => {\r\n // Find first GeoJSON resource\r\n const geoJsonResource = pkg.resources?.find(\r\n (resource: any) => resource.format?.toLowerCase() === 'geojson',\r\n )\r\n\r\n // Exclude if no GeoJSON resource\r\n if (!geoJsonResource) return\r\n\r\n // Exclude if not active\r\n const datastore = geoJsonResource.datastore_active?.toString().toLowerCase() === 'true'\r\n const isActive = datastore || geoJsonResource.state === 'active'\r\n if (!(datastore && geoJsonResource.url && isActive)) return\r\n\r\n // Determine the correct URL based on your working method\r\n let resourceUrl: string | null = null\r\n let resourceTarget: string | null = null\r\n let fieldsUrl: string | null = null\r\n\r\n if (datastore) {\r\n resourceTarget = `${apiUrl}/api/3/action/datastore_search?resource_id=${geoJsonResource.id}&limit=1000000`\r\n resourceUrl = buildUrl(resourceTarget)\r\n fieldsUrl = buildUrl(`${apiUrl}/api/3/action/datastore_search?resource_id=${geoJsonResource.id}&limit=0`)\r\n }\r\n else {\r\n resourceTarget = geoJsonResource.url\r\n resourceUrl = resourceTarget ? buildUrl(resourceTarget) : null\r\n }\r\n\r\n try {\r\n const sampleTarget = resourceTarget?.includes('datastore_search')\r\n ? resourceTarget.replace('limit=1000000', 'limit=1')\r\n : resourceTarget\r\n const sampleUrl = sampleTarget\r\n ? (sampleTarget.includes('limit=') ? buildUrl(sampleTarget) : buildUrl(`${sampleTarget}?limit=1`))\r\n : null\r\n\r\n if (sampleUrl) {\r\n const validKey = buildValidationKey('ckan', pkg.id, pkg.metadata_modified || pkg.metadata_created || undefined)\r\n const isValid = await withCache<boolean>(validKey, 24 * 60 * 60 * 1000, async () => {\r\n const sampleRes = await fetch(sampleUrl)\r\n if (sampleRes.ok) {\r\n const sample = await sampleRes.json()\r\n if (sample?.error) {\r\n const code = sample.error.code\r\n const msg = `${sample.error.message || ''} ${sample.error.messageCode || ''}`\r\n if (code === 499 || code === 504 || /token required|unauthorized|forbidden|timed out/i.test(msg)) return false\r\n }\r\n let hasGeom = true\r\n if (sample?.result?.records) {\r\n const rec = sample.result.records[0]\r\n const geom = rec?.geometry || (typeof rec?.geometry === 'string' ? JSON.parse(rec.geometry) : undefined)\r\n hasGeom = !!geom\r\n }\r\n else if (Array.isArray(sample?.features)) {\r\n hasGeom = sample.features.some((f: any) => f && f.geometry)\r\n }\r\n return !!hasGeom\r\n } else if ([401, 403, 504].includes(sampleRes.status)) {\r\n return false\r\n }\r\n return false\r\n })\r\n if (!isValid) return\r\n }\r\n }\r\n catch {\r\n }\r\n\r\n const getFeaturesData = async () => {\r\n if (resourceUrl) {\r\n try {\r\n const response = await fetch(fieldsUrl ?? resourceUrl)\r\n if (!response.ok) {\r\n console.error('Network response was not ok:', response.status)\r\n return\r\n }\r\n return await response.json()\r\n }\r\n catch (error) {\r\n console.error('Error fetching features data:', error)\r\n return\r\n }\r\n }\r\n }\r\n\r\n const getFields = async () => {\r\n if (fieldsUrl) {\r\n try {\r\n const fieldsKey = `fields:ckan:${geoJsonResource.id}:${pkg.metadata_modified || pkg.metadata_created || ''}`\r\n const cached = getCache<any[]>(fieldsKey)\r\n if (cached) return cached\r\n const dsRes = await fetch(fieldsUrl)\r\n if (dsRes.ok) {\r\n const dsData = await dsRes.json()\r\n const fields = (\r\n dsData.result.fields?.map((field: any) => ({\r\n name: field.id,\r\n type: ckanFieldTypes[field.type]?.type || field.type,\r\n ckanType: field.type,\r\n description: field.info || '',\r\n })) || []\r\n )\r\n setCache(fieldsKey, fields, 24 * 60 * 60 * 1000)\r\n return fields\r\n }\r\n }\r\n catch {\r\n return []\r\n }\r\n }\r\n return []\r\n }\r\n\r\n const getFeatures: () => Promise<AllGeoJSON> = async () => {\r\n // Check cache first\r\n const featuresKey = `features:ckan:${geoJsonResource.id}:${pkg.metadata_modified || pkg.metadata_created || ''}`\r\n const cached = getCache<AllGeoJSON>(featuresKey)\r\n if (cached) {\r\n return cached\r\n }\r\n\r\n const limit = 5_000\r\n const url2 = resourceUrl?.includes('datastore_search')\r\n ? `${resourceUrl}&limit=${limit}`\r\n : resourceUrl\r\n\r\n try {\r\n const response = await fetch(url2)\r\n if (!response.ok) {\r\n console.error('CKAN: Network response was not ok:', response.status)\r\n return {\r\n type: 'FeatureCollection',\r\n features: [],\r\n } as AllGeoJSON\r\n }\r\n const data = await response.json()\r\n let featuresCollection: AllGeoJSON | undefined\r\n\r\n if (data.result && Array.isArray(data.result.records)) {\r\n // CKAN Datastore: build FeatureCollection from records\r\n const features = data.result.records\r\n .map((properties: any) => {\r\n try {\r\n let geometry\r\n geometry = properties.geometry?.type ? properties.geometry : JSON.parse(properties.geometry)\r\n\r\n return {\r\n type: 'Feature',\r\n geometry,\r\n properties,\r\n }\r\n }\r\n catch {\r\n return null\r\n }\r\n })\r\n .filter((f: any) => f !== null)\r\n\r\n featuresCollection = {\r\n type: 'FeatureCollection',\r\n features,\r\n }\r\n }\r\n else if (\r\n data.type === 'FeatureCollection'\r\n && Array.isArray(data.features)\r\n ) {\r\n // Already a FeatureCollection\r\n featuresCollection = data\r\n }\r\n else {\r\n console.error(\r\n 'CKAN: No features available or unexpected response:',\r\n data,\r\n )\r\n return {\r\n type: 'FeatureCollection',\r\n features: [],\r\n } as AllGeoJSON\r\n }\r\n\r\n // Cache the result for 1 hour\r\n if (featuresCollection) {\r\n setCache(featuresKey, featuresCollection, 60 * 60 * 1000)\r\n }\r\n\r\n return featuresCollection\r\n }\r\n catch (error) {\r\n console.error('CKAN: Error processing dataset:', error)\r\n return {\r\n type: 'FeatureCollection',\r\n features: [],\r\n } as AllGeoJSON\r\n }\r\n }\r\n\r\n const name = formatName(pkg.title || pkg.name)\r\n\r\n return {\r\n id: pkg.id,\r\n name,\r\n url: fieldsUrl,\r\n publisher:\r\n pkg.organization?.title || pkg.organization?.name || 'Unknown',\r\n dataManagementSystem: 'ckan',\r\n license: pkg.license_title || pkg.license_id || 'Unknown',\r\n contact: pkg.author || pkg.maintainer || 'Unknown',\r\n keywords: pkg.tags?.map((tag: any) => tag.name) || [],\r\n description: pkg.notes\r\n ? stripHtml(pkg.notes)\r\n : 'No description available',\r\n dateReleased: pkg.metadata_created || '',\r\n dateUpdated: pkg.metadata_modified || '',\r\n information: pkg.information_url || '',\r\n countrySubdivision,\r\n municipality,\r\n type: pkg.type || 'GeoJSON',\r\n portal,\r\n getFields,\r\n getFeatures,\r\n getFeaturesData,\r\n group,\r\n layerColor: layerColorByName(name),\r\n }\r\n }),\r\n )\r\n ).filter(Boolean) // Remove nulls (excluded datasets)\r\n\r\n return transformedDatasets\r\n }\r\n catch (error) {\r\n console.error('Error fetching CKAN GeoJSON datasets:', error)\r\n return []\r\n }\r\n}\r\n\r\nexport interface CkanFieldTypes {\r\n [key: string]: { description: string, type: FieldType }\r\n}\r\n\r\nfunction ckanFieldTypes(): CkanFieldTypes {\r\n return {\r\n text: { description: 'Text', type: 'string' },\r\n int: { description: 'Integer', type: 'number' },\r\n float: { description: 'Float', type: 'number' },\r\n bool: { description: 'Boolean', type: 'boolean' },\r\n date: { description: 'Date', type: 'datetime' },\r\n timestamp: { description: 'Timestamp', type: 'datetime' },\r\n json: { description: 'JSON', type: 'object' },\r\n // Add more CKAN field types as needed\r\n }\r\n}\r\n"],"mappings":"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","names":["_a","response","data"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localDatasets.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/components/viewers/map/datasets/src/localDatasets.ts"],"names":[],"mappings":"AAMA,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAOhF,OAAO,KAAK,EAAE,OAAO,EAAgC,MAAM,mCAAmC,CAAA;
|
|
1
|
+
{"version":3,"file":"localDatasets.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/components/viewers/map/datasets/src/localDatasets.ts"],"names":[],"mappings":"AAMA,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAOhF,OAAO,KAAK,EAAE,OAAO,EAAgC,MAAM,mCAAmC,CAAA;AAuZ9F,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,OAAO,EAAE,CAAC,CA+LpB"}
|
|
@@ -185,7 +185,6 @@ async function fetchMartinServerDatasets(portal) {
|
|
|
185
185
|
return [];
|
|
186
186
|
}
|
|
187
187
|
try {
|
|
188
|
-
console.log("Fetching Martin server catalog from:", martinServerUrl);
|
|
189
188
|
const { indexUrl, tileBaseUrl, fallbackCatalogUrl } = resolveMartinUrls(martinServerUrl);
|
|
190
189
|
const publishedLabels = await fetchPublishedTileLabels();
|
|
191
190
|
let response = await fetch(indexUrl);
|
|
@@ -198,7 +197,6 @@ async function fetchMartinServerDatasets(portal) {
|
|
|
198
197
|
}
|
|
199
198
|
const catalog = await response.json();
|
|
200
199
|
const tiles = normalizeMartinCatalog(catalog, tileBaseUrl).filter((t) => !isPublishedOrgTable(t.tileName) && t.tileName !== "cdt_tiles");
|
|
201
|
-
console.log("Martin catalog received:", tiles.length, "tiles");
|
|
202
200
|
const datasets = await Promise.all(tiles.map(async (tileInfo) => {
|
|
203
201
|
const publishedLabel = publishedLabels.get(tileInfo.tileName);
|
|
204
202
|
const cleanName = publishedLabel != null ? publishedLabel : formatName(
|
|
@@ -263,7 +261,6 @@ async function fetchMartinServerDatasets(portal) {
|
|
|
263
261
|
};
|
|
264
262
|
return dataset;
|
|
265
263
|
}));
|
|
266
|
-
console.log(`Transformed ${datasets.length} Martin datasets`);
|
|
267
264
|
let publishedDatasets = [];
|
|
268
265
|
try {
|
|
269
266
|
const fileRes = await fetch("/api/files");
|