Skip to content

Commit e0ebfcd

Browse files
authored
fix(stac): let a vector layer draw above STAC Search imagery (#1727)
A layer added from Add Data > STAC Layer covered every vector layer no matter where the user put it in the Layers panel. Moving the vector to the top of the panel changed nothing: its features stayed hidden wherever the imagery covered them. The STAC Search control renders its COGs as deck.gl layers through its own `MapboxOverlay` built with `interleaved: false`. Such an overlay owns a separate canvas stacked above the entire MapLibre style, so its layers cannot sit at any depth within it. layer-sync could not help either: the layer has no real style layer for `moveLayer` to move, and the store layer did not carry `externalDeckLayer`, so the computed `beforeId` was never forwarded to anyone. Route these layers through GeoLibre's single shared interleaved overlay instead (the same one the web COG raster control, Google 3D Tiles and deck-viz already share), each carrying the `beforeId` derived from the store's layer order. The control's private overlay is no longer created, so the add / opacity / visibility / remove paths all re-render the shared one, and teardown clears the source. `applyStacSearchLayerOrder` claims only ids the STAC Search control owns; the app shell asks it first and falls through to the raster control for everything else. The raster-tile variant of a STAC Search layer is a real MapLibre layer and keeps reordering through `moveLayer` as before. Fixes #1718
1 parent 9f87629 commit e0ebfcd

6 files changed

Lines changed: 121 additions & 12 deletions

File tree

apps/geolibre-desktop/src/components/layout/DesktopShell.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
addRasterToMap,
99
prepareRasterControl,
1010
applyRasterLayerOrder,
11+
applyStacSearchLayerOrder,
1112
DECK_VIZ_PLUGIN_ID,
1213
DIRECTIONS_PLUGIN_ID,
1314
EFFECTS_PLUGIN_ID,
@@ -1158,10 +1159,15 @@ export function DesktopShell({
11581159
// Re-read drag-dropped / Add Data local-file GeoJSON layers from disk
11591160
// (their data was saved as a path, not embedded).
11601161
void restoreLocalFileLayers();
1161-
// Let layer-sync push the store-derived beforeId into the raster control so
1162-
// deck.gl COG rasters interleave with vector layers instead of always
1163-
// drawing on top.
1164-
setExternalDeckLayerOrderHandler(applyRasterLayerOrder);
1162+
// Let layer-sync push the store-derived beforeId into the control that owns
1163+
// each deck.gl COG raster so it interleaves with vector layers instead of
1164+
// always drawing on top. Two controls render such layers: the raster
1165+
// control and the STAC Search control (#1718). The STAC one claims only its
1166+
// own layer ids, so ask it first and fall through for everything else.
1167+
setExternalDeckLayerOrderHandler((layerId, beforeId) => {
1168+
if (applyStacSearchLayerOrder(layerId, beforeId)) return;
1169+
applyRasterLayerOrder(layerId, beforeId);
1170+
});
11651171
// activeByDefault plugins are marked active without activate() being
11661172
// called, so the effects engine must be kicked explicitly to match the
11671173
// restored active state (idempotent).

packages/plugins/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export {
8787
isViewStatePanelVisible,
8888
COMPONENTS_PLUGIN_ID,
8989
maplibreComponentsPlugin,
90+
applyStacSearchLayerOrder,
9091
openBookmarkPanel,
9192
openFlatGeobufAddVectorLayerPanel,
9293
openColorbarPanel,

packages/plugins/src/plugins/maplibre-components.ts

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import type { GaussianSplatControl, GaussianSplatLayerAdapter } from "maplibre-g
7474
import type { LidarControlEventHandler, PointCloudInfo } from "maplibre-gl-lidar";
7575
import type { GeoLibreAppAPI, GeoLibreMapControlPosition, GeoLibrePlugin } from "../types";
7676
import { ensureMercatorProjection } from "./map-projection-utils";
77+
import { ensureSharedDeckOverlay, setSharedDeckLayers } from "./shared-deck-overlay";
7778
import { attachTerrainMeasure, measurePanelElement, type TerrainMapLike } from "./terrain-measure";
7879
import { INTERNAL_HELPER_LAYER_PATTERNS } from "./internal-layers";
7980
import {
@@ -732,6 +733,12 @@ let bookmarkControl: BookmarkControl | null = null;
732733
let minimapControl: MinimapControl | null = null;
733734
let viewStateControl: ViewStateControl | null = null;
734735
let stacSearchControl: StacSearchControl | null = null;
736+
// The host API the STAC Search control was opened with, kept so its deck.gl COG
737+
// layers can reach the shared interleaved overlay from the patched hooks below.
738+
let stacSearchApp: GeoLibreAppAPI | null = null;
739+
// Store-derived `beforeId` per STAC Search deck layer id, pushed in by
740+
// `applyStacSearchLayerOrder`. See `renderStacSearchDeckLayers`.
741+
const stacSearchBeforeIds = new Map<string, string | undefined>();
735742
let zarrControl: ZarrLayerControl | null = null;
736743
let colorbarControl: ColorbarGuiControl | null = null;
737744
let legendControl: LegendGuiControl | null = null;
@@ -3124,6 +3131,7 @@ async function openStandaloneViewStateControl(app: GeoLibreAppAPI): Promise<bool
31243131
async function openStandaloneStacSearchControl(app: GeoLibreAppAPI): Promise<boolean> {
31253132
const { StacSearchControl: StacSearchControlClass } = await getComponentsConstructors();
31263133

3134+
stacSearchApp = app;
31273135
stacSearchControl ??= createStacSearchControl(StacSearchControlClass);
31283136

31293137
if (!stacSearchControlMounted) {
@@ -4320,6 +4328,10 @@ function teardownStacSearchControl(app: GeoLibreAppAPI): void {
43204328
}
43214329
stacSearchControl = null;
43224330
stacSearchControlMounted = false;
4331+
stacSearchApp = null;
4332+
// Its deck layers live in the shared overlay, which outlives this control.
4333+
stacSearchBeforeIds.clear();
4334+
setSharedDeckLayers("stac-search", []);
43234335
}
43244336

43254337
function hideSearchControl(): void {
@@ -5432,6 +5444,11 @@ function createStacSearchStoreLayer(
54325444
metadata: {
54335445
collectionId,
54345446
customLayerType: "raster",
5447+
// The COG variant renders as a deck.gl layer with no MapLibre style layer
5448+
// to move, so layer-sync must hand its computed `beforeId` to the control
5449+
// instead of calling `moveLayer` (#1718). The raster-tile variant is a
5450+
// real style layer and reorders normally.
5451+
...(rasterLayerInfo ? {} : { externalDeckLayer: true }),
54355452
externalNativeLayer: true,
54365453
identifiable: false,
54375454
nativeLayerIds,
@@ -5597,6 +5614,10 @@ function patchStacSearchRemoveLayer(control: StacSearchControl): void {
55975614
mutableControl._removeLayer = (id?: string) => {
55985615
const layerIds = id ? [id] : Array.from(mutableControl._cogLayers?.keys() ?? []);
55995616
removeLayer(id);
5617+
// Upstream repaints its own overlay, which GeoLibre bypasses, so drop the
5618+
// removed layers from the shared interleaved overlay here (#1718).
5619+
for (const layerId of layerIds) stacSearchBeforeIds.delete(layerId);
5620+
renderStacSearchDeckLayers();
56005621
const store = useAppStore.getState();
56015622
for (const layerId of layerIds) {
56025623
const layer = store.layers.find((item) => item.id === layerId);
@@ -5628,7 +5649,10 @@ function patchStacSearchCogLayer(control: StacSearchControl): void {
56285649

56295650
mutableControl._addCogLayer = async (url: string, item: StacSearchItem, assetKey: string) => {
56305651
ensureMercatorProjection(mutableControl._map);
5631-
await mutableControl._ensureOverlay?.();
5652+
// Deliberately NOT `_ensureOverlay()`: that builds the control's own
5653+
// non-interleaved overlay, which can never be ordered against the style.
5654+
// The shared interleaved overlay renders these layers instead (#1718).
5655+
if (stacSearchApp) await ensureSharedDeckOverlay(stacSearchApp);
56325656
const selectedAsset = getStacSearchSelectedAsset(mutableControl, item, {
56335657
key: assetKey,
56345658
url,
@@ -5655,9 +5679,7 @@ function patchStacSearchCogLayer(control: StacSearchControl): void {
56555679
...renderProps,
56565680
});
56575681
mutableControl._cogLayers?.set(id, layer as unknown as Layer);
5658-
mutableControl._deckOverlay?.setProps({
5659-
layers: Array.from(mutableControl._cogLayers?.values() ?? []) as Layer[],
5660-
});
5682+
renderStacSearchDeckLayers();
56615683
if (mutableControl._state) {
56625684
mutableControl._state.hasLayer = true;
56635685
mutableControl._state.layerCount = mutableControl._cogLayers?.size ?? 0;
@@ -6008,9 +6030,7 @@ function setStacSearchControlLayerState(id: string, visible: boolean, opacity: n
60086030
id,
60096031
layer.clone({ opacity: appliedOpacity }) as StacSearchRenderableLayer,
60106032
);
6011-
mutableControl?._deckOverlay?.setProps({
6012-
layers: getStacSearchDeckLayers(mutableControl),
6013-
});
6033+
renderStacSearchDeckLayers();
60146034
}
60156035

60166036
function getStacSearchDeckLayers(control: MutableStacSearchControl): Layer[] {
@@ -6019,6 +6039,50 @@ function getStacSearchDeckLayers(control: MutableStacSearchControl): Layer[] {
60196039
);
60206040
}
60216041

6042+
/**
6043+
* Pushes the STAC Search control's deck.gl COG layers into GeoLibre's shared
6044+
* interleaved overlay, each carrying the `beforeId` derived from the store's
6045+
* layer order.
6046+
*
6047+
* Upstream renders them through the control's own non-interleaved
6048+
* `MapboxOverlay`, which owns a separate canvas stacked above the entire
6049+
* MapLibre style — so STAC imagery covered every vector layer no matter where
6050+
* the user placed it in the Layers panel (opengeos/GeoLibre#1718). Interleaved
6051+
* layers are drawn inside the style instead, at the depth their `beforeId`
6052+
* selects, which is what makes panel order mean anything for them.
6053+
*/
6054+
function renderStacSearchDeckLayers(): void {
6055+
const control = stacSearchControl as unknown as MutableStacSearchControl | null;
6056+
if (!control) return;
6057+
const layers = getStacSearchDeckLayers(control).map((layer) => {
6058+
const beforeId = stacSearchBeforeIds.get(layer.id);
6059+
if ((layer.props as { beforeId?: string }).beforeId === beforeId) return layer;
6060+
return layer.clone({ beforeId } as unknown as Partial<Layer["props"]>);
6061+
});
6062+
setSharedDeckLayers("stac-search", layers);
6063+
}
6064+
6065+
/**
6066+
* Applies a store-derived draw order to a STAC Search deck.gl COG layer.
6067+
*
6068+
* Registered by the app shell as part of the external deck-layer order handler:
6069+
* such a layer is not a real MapLibre style layer, so `moveLayer` cannot reorder
6070+
* it and layer-sync forwards the computed `beforeId` here instead.
6071+
*
6072+
* @param layerId - The store layer id, which doubles as the deck layer id.
6073+
* @param beforeId - The style layer to draw beneath, or undefined for the top.
6074+
* @returns True when the id belongs to the STAC Search control.
6075+
*/
6076+
export function applyStacSearchLayerOrder(layerId: string, beforeId: string | undefined): boolean {
6077+
const control = stacSearchControl as unknown as MutableStacSearchControl | null;
6078+
const layer = control?._cogLayers?.get(layerId);
6079+
if (!layer || getStacSearchRasterLayerInfo(layer)) return false;
6080+
if (stacSearchBeforeIds.get(layerId) === beforeId) return true;
6081+
stacSearchBeforeIds.set(layerId, beforeId);
6082+
renderStacSearchDeckLayers();
6083+
return true;
6084+
}
6085+
60226086
function getStacSearchRasterLayerInfo(
60236087
layer: StacSearchRenderableLayer,
60246088
): { layerId: string; sourceId: string; tileUrl?: string } | null {

packages/plugins/src/plugins/shared-deck-overlay.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ type DeviceListener = (device: unknown) => void;
3232
* position stays visible above the 3D track it rides (see #1210).
3333
* Ordering WITHIN a source is whatever order that source supplies.
3434
*/
35-
const SOURCE_DRAW_ORDER = ["raster", "google-3d-tiles", "deckviz", "route-anim"] as const;
35+
const SOURCE_DRAW_ORDER = [
36+
"raster",
37+
"stac-search",
38+
"google-3d-tiles",
39+
"deckviz",
40+
"route-anim",
41+
] as const;
3642
export type SharedDeckSource = (typeof SOURCE_DRAW_ORDER)[number];
3743

3844
let overlay: MapboxOverlay | null = null;

tests/external-deck-order.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,28 @@ describe("external deck-layer order handler", () => {
6262
assert.deepEqual(calls, [["raster-1", undefined]]);
6363
});
6464

65+
it("forwards the beforeId for a STAC Search COG layer", () => {
66+
const calls: Array<[string, string | undefined]> = [];
67+
setExternalDeckLayerOrderHandler((id, beforeId) => calls.push([id, beforeId]));
68+
69+
// The STAC Search control renders its COGs as deck layers too, so its store
70+
// layers must carry the same flag or their imagery would keep drawing above
71+
// every vector layer whatever the panel order says (#1718).
72+
const layer = rasterDeckLayer();
73+
layer.id = "stac-search-item-visual-0";
74+
layer.metadata = {
75+
customLayerType: "raster",
76+
externalDeckLayer: true,
77+
externalNativeLayer: true,
78+
nativeLayerIds: ["stac-search-item-visual-0"],
79+
sourceIds: [],
80+
sourceKind: "stac-search-cog",
81+
};
82+
syncLayer(makeMapStub() as never, layer, "vector-line");
83+
84+
assert.deepEqual(calls, [["stac-search-item-visual-0", "vector-line"]]);
85+
});
86+
6587
it("does not fire for a non-deck external custom layer", () => {
6688
const calls: unknown[] = [];
6789
setExternalDeckLayerOrderHandler((id, beforeId) => calls.push([id, beforeId]));

tests/shared-deck-overlay.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,18 @@ describe("shared-deck-overlay", () => {
8686
"raster drawn first (bottom), deckviz last (top)",
8787
);
8888

89+
// STAC Search COGs are imagery too, so they sit with the rasters at the
90+
// bottom rather than above the 3D tiles / vector overlays (#1718).
91+
setSharedDeckLayers("stac-search", [layer("s1")] as never);
92+
assert.deepEqual(
93+
overlay?.layerIds(),
94+
["r1", "s1", "g1", "d1"],
95+
"STAC Search imagery draws with the rasters, under google and deckviz",
96+
);
97+
8998
// Cleanup for the next test.
9099
setSharedDeckLayers("raster", [] as never);
100+
setSharedDeckLayers("stac-search", [] as never);
91101
setSharedDeckLayers("google-3d-tiles", [] as never);
92102
setSharedDeckLayers("deckviz", [] as never);
93103
assert.deepEqual(overlay?.layerIds(), []);

0 commit comments

Comments
 (0)