@@ -741,6 +741,26 @@ let stacSearchStoreUnsubscribe: (() => void) | null = null;
741741let zarrStoreUnsubscribe : ( ( ) => void ) | null = null ;
742742let lidarStoreUnsubscribe : ( ( ) => void ) | null = null ;
743743let splattingStoreUnsubscribe : ( ( ) => void ) | null = null ;
744+
745+ // Re-streaming saved LiDAR layers on project open. The store only holds a
746+ // `lidar-url` layer's metadata; the point cloud itself is loaded by the LiDAR
747+ // control, not the store, so a reopened project shows the layer in the panel
748+ // but renders nothing until we ask the control to stream it again (see
749+ // restoreLidarLayers). Because loadPointCloud assigns a fresh id, this map
750+ // carries the saved layer's desired state, keyed by source URL, so the load
751+ // handler can reattach the loaded cloud to the saved layer instead of adding a
752+ // duplicate.
753+ interface PendingLidarRestore {
754+ layerId : string ;
755+ name : string ;
756+ visible : boolean ;
757+ opacity : number ;
758+ style : GeoLibreLayer [ "style" ] ;
759+ beforeLayerId : string | null ;
760+ }
761+ const pendingLidarRestores = new Map < string , PendingLidarRestore > ( ) ;
762+ let lidarRestoreInFlight = false ;
763+
744764let pluginActive = false ;
745765let componentsControlRevision = 0 ;
746766let componentsConstructorsPromise : Promise < ComponentsConstructors > | null =
@@ -2433,13 +2453,20 @@ async function openStandaloneHtmlControl(
24332453}
24342454
24352455async function openStandaloneLidarControl (
2436- app : GeoLibreAppAPI
2456+ app : GeoLibreAppAPI ,
2457+ options : { reveal ?: boolean } = { }
24372458) : Promise < boolean > {
2459+ // `reveal` shows and expands the panel (the default, for the Add LiDAR Layer
2460+ // menu action). Project restore mounts the control only to re-stream saved
2461+ // clouds, so it passes `reveal: false` to keep the panel out of the user's
2462+ // way; a freshly created control is hidden so it does not pop open on load.
2463+ const reveal = options . reveal ?? true ;
24382464 const {
24392465 LidarControl : LidarControlClass ,
24402466 LidarLayerAdapter : LidarLayerAdapterClass ,
24412467 } = await getComponentsConstructors ( ) ;
24422468
2469+ const created = ! lidarControl ;
24432470 lidarControl ??= createLidarControl (
24442471 LidarControlClass ,
24452472 LidarLayerAdapterClass
@@ -2457,12 +2484,96 @@ async function openStandaloneLidarControl(
24572484 startLidarThemeSync ( ) ;
24582485
24592486 setTimeout ( ( ) => {
2460- showLidarControl ( lidarControl ) ;
2461- lidarControl ?. expand ( ) ;
2487+ if ( reveal ) {
2488+ showLidarControl ( lidarControl ) ;
2489+ lidarControl ?. expand ( ) ;
2490+ } else if ( created ) {
2491+ hideLidarControl ( lidarControl ) ;
2492+ }
24622493 } , 0 ) ;
24632494 return true ;
24642495}
24652496
2497+ /**
2498+ * Read the source URL of a `lidar-url` layer, preferring the dedicated
2499+ * `sourcePath` and falling back to `source.url`.
2500+ */
2501+ function lidarLayerUrl ( layer : GeoLibreLayer ) : string | null {
2502+ if ( typeof layer . sourcePath === "string" && layer . sourcePath ) {
2503+ return layer . sourcePath ;
2504+ }
2505+ const url = ( layer . source as { url ?: unknown } ) . url ;
2506+ return typeof url === "string" && url ? url : null ;
2507+ }
2508+
2509+ /** Whether a restore is already queued or in flight for this layer. */
2510+ function isLidarRestorePending ( layer : GeoLibreLayer ) : boolean {
2511+ const url = lidarLayerUrl ( layer ) ;
2512+ if ( url && pendingLidarRestores . has ( url ) ) return true ;
2513+ for ( const pending of pendingLidarRestores . values ( ) ) {
2514+ if ( pending . layerId === layer . id ) return true ;
2515+ }
2516+ return false ;
2517+ }
2518+
2519+ /**
2520+ * Re-stream the point clouds for any restored `lidar-url` layers that are not
2521+ * yet loaded into the LiDAR control (e.g. after opening a saved project). The
2522+ * store only holds the layer metadata, so without this the layer appears in the
2523+ * Layers panel but renders nothing. The loaded cloud is reattached to the saved
2524+ * layer in {@link createLidarLoadHandler}, preserving its visibility, opacity,
2525+ * style, name, and position.
2526+ */
2527+ export async function restoreLidarLayers ( app : GeoLibreAppAPI ) : Promise < void > {
2528+ if ( lidarRestoreInFlight ) return ;
2529+
2530+ const pending = useAppStore
2531+ . getState ( )
2532+ . layers . filter (
2533+ ( layer ) =>
2534+ isLidarControlLayer ( layer ) &&
2535+ ! hasLidarPointCloud ( layer . id ) &&
2536+ ! isLidarRestorePending ( layer )
2537+ ) ;
2538+ if ( pending . length === 0 ) return ;
2539+
2540+ lidarRestoreInFlight = true ;
2541+ try {
2542+ const opened = await openStandaloneLidarControl ( app , { reveal : false } ) ;
2543+ if ( ! opened || ! lidarControl ) return ;
2544+ // The deck.gl point-cloud overlay only renders under the Mercator
2545+ // projection (the streaming loader's viewport math breaks under the default
2546+ // globe), matching the USGS LiDAR plugin and the other deck.gl controls.
2547+ ensureMercatorProjection ( app . getMap ?.( ) ) ;
2548+
2549+ for ( const layer of pending ) {
2550+ const url = lidarLayerUrl ( layer ) ;
2551+ if ( ! url ) continue ;
2552+ // Re-check against the live store: a layer may have been removed, already
2553+ // loaded, or queued while the control was loading asynchronously.
2554+ const current = useAppStore . getState ( ) . layers ;
2555+ const index = current . findIndex ( ( item ) => item . id === layer . id ) ;
2556+ if ( index === - 1 ) continue ;
2557+ if ( hasLidarPointCloud ( layer . id ) || isLidarRestorePending ( layer ) ) continue ;
2558+
2559+ pendingLidarRestores . set ( url , {
2560+ layerId : layer . id ,
2561+ name : layer . name ,
2562+ visible : layer . visible ,
2563+ opacity : layer . opacity ,
2564+ style : layer . style ,
2565+ beforeLayerId : current [ index + 1 ] ?. id ?? null ,
2566+ } ) ;
2567+ lidarControl . loadPointCloud ( url ) . catch ( ( error : unknown ) => {
2568+ pendingLidarRestores . delete ( url ) ;
2569+ console . warn ( "[lidar] failed to restore point cloud" , url , error ) ;
2570+ } ) ;
2571+ }
2572+ } finally {
2573+ lidarRestoreInFlight = false ;
2574+ }
2575+ }
2576+
24662577async function openStandaloneSplattingControl (
24672578 app : GeoLibreAppAPI
24682579) : Promise < boolean > {
@@ -3297,6 +3408,7 @@ function setHtmlPanelVisible(visible: boolean): void {
32973408
32983409function teardownLidarControl ( app : GeoLibreAppAPI ) : void {
32993410 stopLidarThemeSync ( ) ;
3411+ pendingLidarRestores . clear ( ) ;
33003412 lidarStoreUnsubscribe ?.( ) ;
33013413 lidarStoreUnsubscribe = null ;
33023414 lidarLayerAdapter ?. destroy ( ) ;
@@ -3326,6 +3438,48 @@ function createLidarLoadHandler(): LidarControlEventHandler {
33263438
33273439 const store = useAppStore . getState ( ) ;
33283440 const layer = createLidarStoreLayer ( event . pointCloud ) ;
3441+
3442+ // Project restore: this load was triggered to re-stream a saved layer (see
3443+ // restoreLidarLayers). loadPointCloud assigns a fresh id, so swap the inert
3444+ // placeholder (saved id) for the loaded layer in place, carrying over the
3445+ // saved visibility, opacity, style, name, and position.
3446+ const restoreKey =
3447+ typeof event . pointCloud . source === "string"
3448+ ? event . pointCloud . source
3449+ : null ;
3450+ const restore = restoreKey ? pendingLidarRestores . get ( restoreKey ) : null ;
3451+ if ( restore ) {
3452+ pendingLidarRestores . delete ( restoreKey as string ) ;
3453+ const restored : GeoLibreLayer = {
3454+ ...layer ,
3455+ name : restore . name || layer . name ,
3456+ visible : restore . visible ,
3457+ opacity : restore . opacity ,
3458+ style : restore . style ,
3459+ } ;
3460+ if (
3461+ restore . layerId !== restored . id &&
3462+ store . layers . some ( ( item ) => item . id === restore . layerId )
3463+ ) {
3464+ store . removeLayer ( restore . layerId ) ;
3465+ }
3466+ const beforeLayerId =
3467+ restore . beforeLayerId &&
3468+ useAppStore
3469+ . getState ( )
3470+ . layers . some ( ( item ) => item . id === restore . beforeLayerId )
3471+ ? restore . beforeLayerId
3472+ : null ;
3473+ store . addLayer ( restored , beforeLayerId ) ;
3474+ if ( ! restored . visible ) {
3475+ lidarLayerAdapter ?. setVisibility ( restored . id , false ) ;
3476+ }
3477+ if ( restored . opacity !== 1 ) {
3478+ lidarLayerAdapter ?. setOpacity ( restored . id , restored . opacity ) ;
3479+ }
3480+ return ;
3481+ }
3482+
33293483 if ( store . layers . some ( ( item ) => item . id === layer . id ) ) {
33303484 store . updateLayer ( layer . id , {
33313485 metadata : layer . metadata ,
0 commit comments