-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add restoreLayers API to rehydrate host-persisted layers #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,6 +89,8 @@ export class EnviroAtlasControl implements IControl { | |
| private _searchEpoch = 0; | ||
| private _noticeTimer: ReturnType<typeof setTimeout> | null = null; | ||
| private _debouncedSearch?: (query: string) => void; | ||
| /** Layers handed to restoreLayers before the control was added to a map */ | ||
| private _pendingRestore: AddedLayer[] = []; | ||
|
|
||
| // Panel positioning handlers | ||
| private _resizeHandler: (() => void) | null = null; | ||
|
|
@@ -155,6 +157,14 @@ export class EnviroAtlasControl implements IControl { | |
| // Setup event listeners for panel positioning and click-outside | ||
| this._setupEventListeners(); | ||
|
|
||
| // Apply any layers handed to restoreLayers before the control was | ||
| // added to a map. | ||
| if (this._pendingRestore.length > 0) { | ||
| const pending = this._pendingRestore; | ||
| this._pendingRestore = []; | ||
| this.restoreLayers(pending); | ||
| } | ||
|
|
||
| // Set initial panel state | ||
| if (!this._state.collapsed) { | ||
| this._panel.classList.add('expanded'); | ||
|
|
@@ -207,6 +217,7 @@ export class EnviroAtlasControl implements IControl { | |
| this._allServices = []; | ||
| this._prefetchStarted = false; | ||
| this._state.addedLayers = []; | ||
| this._pendingRestore = []; | ||
|
|
||
| // Remove panel from map container | ||
| this._panel?.parentNode?.removeChild(this._panel); | ||
|
|
@@ -371,6 +382,47 @@ export class EnviroAtlasControl implements IControl { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Re-registers layers that were previously added and persisted by a | ||
| * host application. | ||
| * | ||
| * Host applications that save and restore map state (for example a | ||
| * project file) typically recreate the native MapLibre source and | ||
| * layer for each persisted {@link AddedLayer} themselves and then need | ||
| * to hand those layers back to the control so it tracks them again. | ||
| * This method does exactly that without duplicating the natives: when | ||
| * the source/layer already exist they are reused (and opacity and | ||
| * visibility reconciled), otherwise they are created. | ||
| * | ||
| * Entries that are already tracked, or that match an existing layer by | ||
| * service and sublayer, are skipped. Unlike {@link addServiceLayer}, | ||
| * this method never fits the map bounds and shows no notices. | ||
| * | ||
| * When the control has not been added to a map yet the entries are | ||
| * stored and applied automatically in {@link onAdd}. | ||
| * | ||
| * @param entries - The persisted added-layer entries to restore | ||
| */ | ||
| restoreLayers(entries: AddedLayer[]): void { | ||
| if (!this._layerManager) { | ||
| this._pendingRestore.push(...entries); | ||
| return; | ||
| } | ||
|
|
||
| let restored = false; | ||
| for (const entry of entries) { | ||
| if (this._layerManager.findLayer(entry.service, entry.sublayerId)) continue; | ||
| const layer = this._layerManager.restoreLayer(entry); | ||
| restored = true; | ||
| this._emit('layeradd', { layer }); | ||
| } | ||
|
|
||
| if (!restored) return; | ||
| this._state.addedLayers = this._layerManager.getLayers(); | ||
| this._addedView?.update(this._state.addedLayers); | ||
| this._emit('statechange'); | ||
|
Comment on lines
+413
to
+423
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isolate per-entry restore failures so one bad entry doesn’t break batch/onAdd.
Suggested fix restoreLayers(entries: AddedLayer[]): void {
if (!this._layerManager) {
this._pendingRestore.push(...entries);
return;
}
+ const tracked = this._layerManager.getLayers();
+ const seenIds = new Set(tracked.map((l) => l.id));
+ const seenServiceKeys = new Set(
+ tracked.map((l) => `${l.service.fullName}::${l.sublayerId ?? ''}`)
+ );
+
let restored = false;
for (const entry of entries) {
- if (this._layerManager.findLayer(entry.service, entry.sublayerId)) continue;
- const layer = this._layerManager.restoreLayer(entry);
- restored = true;
- this._emit('layeradd', { layer });
+ const serviceKey = `${entry.service.fullName}::${entry.sublayerId ?? ''}`;
+ if (seenIds.has(entry.id) || seenServiceKeys.has(serviceKey)) continue;
+ try {
+ const layer = this._layerManager.restoreLayer(entry);
+ restored = true;
+ seenIds.add(layer.id);
+ seenServiceKeys.add(`${layer.service.fullName}::${layer.sublayerId ?? ''}`);
+ this._emit('layeradd', { layer });
+ } catch (error) {
+ this._handleError(error instanceof Error ? error : new Error(String(error)));
+ }
}
if (!restored) return;
this._state.addedLayers = this._layerManager.getLayers();
this._addedView?.update(this._state.addedLayers);
this._emit('statechange');
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** | ||
| * Removes a layer previously added through the control. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -114,6 +114,63 @@ export class MapLayerManager { | |
| bounds, | ||
| }; | ||
|
|
||
| this._createSource(entry); | ||
| this._createLayer(entry); | ||
|
|
||
| this._layers.set(id, entry); | ||
| return entry; | ||
| } | ||
|
|
||
| /** | ||
| * Re-registers a previously persisted layer, reusing native source | ||
| * and layer objects the host application may have already recreated. | ||
| * | ||
| * Host applications that persist {@link AddedLayer} entries (for | ||
| * example in a saved project) often recreate the native MapLibre | ||
| * source and layer themselves before re-activating the control. This | ||
| * method hands those layers back to the manager without duplicating | ||
| * the natives: existing source/layer objects are kept and only the | ||
| * missing ones are created, while opacity and visibility are | ||
| * reconciled to match the entry. | ||
| * | ||
| * @param entry - The persisted added-layer entry to restore | ||
| * @returns The tracked added-layer entry (an existing one when the id | ||
| * was already managed, otherwise the newly registered copy) | ||
| */ | ||
| restoreLayer(entry: AddedLayer): AddedLayer { | ||
| const existing = this._layers.get(entry.id); | ||
| if (existing) return existing; | ||
|
|
||
| const copy: AddedLayer = { ...entry }; | ||
|
|
||
| if (!this._map.getSource(copy.sourceId)) { | ||
| this._createSource(copy); | ||
| } else if (this._options.renderMode === 'image') { | ||
| // The host recreated the source; still ensure the shared view | ||
| // listener that refreshes image layers is registered. | ||
| this._ensureViewHandler(); | ||
| } | ||
|
|
||
| if (!this._map.getLayer(copy.layerId)) { | ||
| this._createLayer(copy); | ||
| } else { | ||
|
Comment on lines
+154
to
+156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not delete pre-existing sources on restore-layer failure. When Suggested fix- private _createLayer(entry: AddedLayer): void {
+ private _createLayer(entry: AddedLayer, removeSourceOnFailure = true): void {
@@
} catch (error) {
// Avoid orphaning the source when layer creation fails
- if (this._map.getSource(entry.sourceId)) {
+ if (removeSourceOnFailure && this._map.getSource(entry.sourceId)) {
this._map.removeSource(entry.sourceId);
}
throw error;
}
}- if (!this._map.getLayer(copy.layerId)) {
- this._createLayer(copy);
+ const sourceExisted = Boolean(this._map.getSource(copy.sourceId));
+ if (!this._map.getLayer(copy.layerId)) {
+ this._createLayer(copy, !sourceExisted);🤖 Prompt for AI Agents |
||
| // The host recreated the native layer; reconcile paint and layout | ||
| // so manager state and the map agree. | ||
| this._map.setPaintProperty(copy.layerId, 'raster-opacity', copy.opacity); | ||
| this._map.setLayoutProperty(copy.layerId, 'visibility', copy.visible ? 'visible' : 'none'); | ||
| } | ||
|
|
||
| this._layers.set(copy.id, copy); | ||
| return copy; | ||
| } | ||
|
|
||
| /** | ||
| * Creates the native MapLibre source for an added layer according to | ||
| * the current render mode. | ||
| * | ||
| * @param entry - The added layer to create a source for | ||
| */ | ||
| private _createSource(entry: AddedLayer): void { | ||
| if (this._options.renderMode === 'image') { | ||
| const view = this._computeView(entry); | ||
| this._map.addSource(entry.sourceId, { | ||
|
|
@@ -129,8 +186,8 @@ export class MapLayerManager { | |
| this._ensureViewHandler(); | ||
| } else { | ||
| const tileTemplate = buildTileTemplate( | ||
| service, | ||
| sublayerId, | ||
| entry.service, | ||
| entry.sublayerId, | ||
| { tileSize: this._options.tileSize, imageFormat: this._options.imageFormat }, | ||
| this._options.servicesUrl | ||
| ); | ||
|
|
@@ -142,10 +199,18 @@ export class MapLayerManager { | |
| }; | ||
| // Bounds keep MapLibre from requesting tiles far outside the data | ||
| // extent, which the EnviroAtlas server answers with slow 504s. | ||
| if (bounds) source.bounds = bounds; | ||
| if (entry.bounds) source.bounds = entry.bounds; | ||
| this._map.addSource(entry.sourceId, source); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates the native MapLibre raster layer for an added layer, | ||
| * removing the source if layer creation fails. | ||
| * | ||
| * @param entry - The added layer to create a layer for | ||
| */ | ||
| private _createLayer(entry: AddedLayer): void { | ||
| // Insert below the configured layer when it exists on the map | ||
| const beforeId = | ||
| this._options.beforeId && this._map.getLayer(this._options.beforeId) ? this._options.beforeId : undefined; | ||
|
|
@@ -156,12 +221,12 @@ export class MapLayerManager { | |
| type: 'raster', | ||
| source: entry.sourceId, | ||
| paint: { | ||
| 'raster-opacity': opacity, | ||
| 'raster-opacity': entry.opacity, | ||
| // Image-mode sources swap the whole picture on view changes; | ||
| // fading would flash the old extent during the swap. | ||
| ...(this._options.renderMode === 'image' ? { 'raster-fade-duration': 0 } : {}), | ||
| }, | ||
| layout: { visibility: 'visible' }, | ||
| layout: { visibility: entry.visible ? 'visible' : 'none' }, | ||
| }, | ||
| beforeId | ||
| ); | ||
|
|
@@ -172,9 +237,6 @@ export class MapLayerManager { | |
| } | ||
| throw error; | ||
| } | ||
|
|
||
| this._layers.set(id, entry); | ||
| return entry; | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Skip already-tracked IDs before emitting
layeradd.This loop only de-dupes by service+sublayer. If an entry is already tracked by
idbut not by that key,restoreLayercan return the existing layer and this still emits a newlayeradd, violating the “already tracked entries are skipped” contract.🤖 Prompt for AI Agents