Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions api/react-for-mapbox.api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,12 @@ declare class MapboxGroundImageOverlayRenderer extends AbstractGroundImageOverla
/** Sync an already-created image source+layer to the current state (diffed). */
private applyToExisting;
removeGroundImage(entity: GroundImageEntity<string>): Promise<void>;
/**
* Under the polygons, circles, polylines and markers, above the rasters, whatever order the
* overlays were added in. Ground images have no zIndex; among themselves they keep the order
* they were handed over in.
*/
private groundImageBeforeId;
private removeFromStyle;
}

Expand Down Expand Up @@ -458,6 +464,13 @@ declare class MapboxRasterLayerOverlayRenderer implements RasterLayerOverlayRend
onCameraChanged(_mapCameraPosition: MapCameraPosition): Promise<void>;
onPostProcess(): Promise<void>;
private addLayer;
/** This raster's place in the overlay stack: the lowest band, ordered by zIndex. */
private stackKey;
/**
* Under every other overlay (the raster band is the lowest), and under the basemap's labels,
* whichever comes first. Marker tiles are drawn as a raster but belong with the markers.
*/
private beforeId;
/**
* The style's first label layer, so a raster overlay can go under it.
*
Expand Down
13 changes: 3 additions & 10 deletions src/circle/MapboxCircleLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
LayerSpecification,
} from 'mapbox-gl';
import { type CircleEntity } from '@mapconductor/js-sdk-core';
import { bringMarkerLayersToFront, setSourceFeatures, type FeatureCollection, type PolygonFeature } from '../helpers';
import { bringMarkerLayersToFront, setSourceFeatures, type FeatureCollection, type PolygonFeature, overlayLayerBeforeId, overlayStackKeyOf } from '../helpers';
import { MapboxMapViewHolder } from '../MapboxMapViewHolder';

export type MapboxActualCircle = PolygonFeature & { id?: string | number };
Expand Down Expand Up @@ -73,14 +73,8 @@ export class MapboxCircleLayer {
data: { type: 'FeatureCollection', features: [] },
});
}
const beforeId = map.getLayer('polygon-fill-layer')
? 'polygon-fill-layer'
: map.getLayer('polyline-layer')
? 'polyline-layer'
: map.getLayer('mc-marker-layer')
? 'mc-marker-layer'
: undefined;
if (!map.getLayer(this.layerId)) {
const beforeId = overlayLayerBeforeId(map, overlayStackKeyOf(map, this.layerId) ?? { kind: 'circle', sub: 0 });
map.addLayer(
{
id: this.layerId,
Expand All @@ -97,8 +91,7 @@ export class MapboxCircleLayer {
);
}
if (!map.getLayer(this.strokeLayerId)) {
// Added with the same beforeId after the fill layer, so it renders
// directly above the fill layer.
const beforeId = overlayLayerBeforeId(map, overlayStackKeyOf(map, this.strokeLayerId) ?? { kind: 'circle', sub: 1 });
map.addLayer(
{
id: this.strokeLayerId,
Expand Down
24 changes: 21 additions & 3 deletions src/groundimage/MapboxGroundImageOverlayRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import {
type GroundImageEntity,
type GroundImageState,
} from '@mapconductor/js-sdk-core';
import { bringMarkerLayersToFront, groundImageCoordinates, removeLayerIfExists, removeSourceIfExists } from '../helpers';
import {
bringMarkerLayersToFront,
forgetOverlayStackKey,
groundImageCoordinates,
overlayLayerBeforeId,
rememberOverlayStackKey,
removeLayerIfExists,
removeSourceIfExists,
} from '../helpers';
import { MapboxMapViewHolder } from '../MapboxMapViewHolder';

type Coordinates = ReturnType<typeof groundImageCoordinates>;
Expand Down Expand Up @@ -75,7 +83,7 @@ export class MapboxGroundImageOverlayRenderer extends AbstractGroundImageOverlay
// but the two renderers are kept identical, so this is set on both.
'raster-fade-duration': 0,
},
} as LayerSpecification);
} as LayerSpecification, this.groundImageBeforeId(layerId));
}
bringMarkerLayersToFront(this.holder.map);
this.applied.set(state.id, {
Expand All @@ -97,7 +105,7 @@ export class MapboxGroundImageOverlayRenderer extends AbstractGroundImageOverlay
type: 'raster',
source: sourceId,
paint: { 'raster-opacity': state.opacity },
} as LayerSpecification);
} as LayerSpecification, this.groundImageBeforeId(layerId));
}
this.applyToExisting(state, source, layerId, coordinates);
bringMarkerLayersToFront(this.holder.map);
Expand Down Expand Up @@ -176,7 +184,17 @@ export class MapboxGroundImageOverlayRenderer extends AbstractGroundImageOverlay
this.pendingRemovals.clear();
}

/**
* Under the polygons, circles, polylines and markers, above the rasters, whatever order the
* overlays were added in. Ground images have no zIndex; among themselves they keep the order
* they were handed over in.
*/
private groundImageBeforeId(layerId: string): string | undefined {
return overlayLayerBeforeId(this.holder.map, rememberOverlayStackKey(this.holder.map, layerId, 'groundImage'));
}

private removeFromStyle(id: string): void {
forgetOverlayStackKey(this.holder.map, this.layerId(id));
removeLayerIfExists(this.holder.map, this.layerId(id));
removeSourceIfExists(this.holder.map, this.sourceId(id));
}
Expand Down
84 changes: 83 additions & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GeoJSONSource, Map as MapboxMap, SourceSpecification } from 'mapbox-gl';
import { createGeoPoint, TileScheme, type GeoPoint, type GroundImageState, type PolygonState, RasterLayerSource } from '@mapconductor/js-sdk-core';
import { createGeoPoint, TileScheme, type GeoPoint, type GroundImageState, type PolygonState, RasterLayerSource, overlayStackBeforeId, type OverlayKind, type OverlayStackKey } from '@mapconductor/js-sdk-core';

export type Coordinate = [number, number];
export type GeoJSONSourceData = Parameters<GeoJSONSource['setData']>[0];
Expand Down Expand Up @@ -92,6 +92,88 @@ export function bringMarkerLayersToFront(map: MapboxMap): void {
if (arranged) arrangedOrders.set(map, arranged.join('\n'));
}

// ── Overlay stacking ───────────────────────────────────────────────────────
//
// Bottom to top, on every provider: raster < ground image < polygon < circle < polyline <
// marker (js-sdk-core OVERLAY_STACK_ORDER). Every layer this SDK adds goes in with the
// beforeId from overlayLayerBeforeId(), so the order does not depend on the order things were
// added, re-sent after clearOverlays(), or put back after a style swap. It used to: polygons
// went in "before the polyline layer", circles "before the polygon fill" (under polygons),
// ground images and polylines at the top, so a ground image re-sent after the polygons covered
// them, and a raster added after clearOverlays() could cover everything.

export const MARKER_TILES_LAYER_ID = 'mc-raster-lyr-mc-marker-tiles';

/** Layers with a fixed place: one per kind, holding every overlay of that kind. */
const FIXED_STACK_KEYS: Record<string, OverlayStackKey> = {
'polygon-fill-layer': { kind: 'polygon', sub: 0 },
'polygon-outline-layer': { kind: 'polygon', sub: 1 },
'circle-layer': { kind: 'circle', sub: 0 },
'circle-layer-stroke': { kind: 'circle', sub: 1 },
'polyline-layer': { kind: 'polyline' },
[MARKER_TILES_LAYER_ID]: { kind: 'marker', sub: 0 },
'mc-marker-layer': { kind: 'marker', sub: 1 },
'mc-marker-drag-layer': { kind: 'marker', sub: 2 },
};

// Layers holding one overlay each (rasters, ground images), per map: their zIndex and the
// order they were handed over in.
const itemStackKeys = new WeakMap<object, Map<string, OverlayStackKey>>();
const itemSequences = new WeakMap<object, number>();

/** Where a layer of ours belongs in the stack; null for the style's own layers. */
export function overlayStackKeyOf(map: object, layerId: string): OverlayStackKey | null {
const fixed = FIXED_STACK_KEYS[layerId];
if (fixed) return fixed;
const item = itemStackKeys.get(map)?.get(layerId);
if (item) return item;
if (layerId.startsWith('mc-raster-lyr-')) return { kind: 'rasterLayer' };
if (layerId.startsWith('mc-gimg-lyr-')) return { kind: 'groundImage' };
return null;
}

/**
* Records the key of a layer that holds one overlay. A layer re-created for the same overlay
* (a raster whose source changed, a style swap) keeps the place it had among equal zIndexes.
*/
export function rememberOverlayStackKey(map: object, layerId: string, kind: OverlayKind, zIndex = 0): OverlayStackKey {
let keys = itemStackKeys.get(map);
if (!keys) {
keys = new Map();
itemStackKeys.set(map, keys);
}
let sequence = keys.get(layerId)?.sequence;
if (sequence == null) {
sequence = (itemSequences.get(map) ?? 0) + 1;
itemSequences.set(map, sequence);
}
const key: OverlayStackKey = { kind, zIndex, sequence };
keys.set(layerId, key);
return key;
}

/** The overlay is gone (not just its layer): a later one with the same id starts over. */
export function forgetOverlayStackKey(map: object, layerId: string): void {
itemStackKeys.get(map)?.delete(layerId);
}

/** The beforeId that puts a new layer with this key in its place in the stack. */
export function overlayLayerBeforeId(map: MapboxMap, key: OverlayStackKey): string | undefined {
return overlayStackBeforeId(currentLayerOrder(map) ?? [], (id) => overlayStackKeyOf(map, id), key);
}

/** Moves an existing layer of ours to its place (after its zIndex changed). */
export function moveLayerToStackPlace(map: MapboxMap, layerId: string, key: OverlayStackKey): void {
const order = currentLayerOrder(map);
if (!order || !order.includes(layerId)) return;
const rest = order.filter((id) => id !== layerId);
const beforeId = overlayStackBeforeId(rest, (id) => overlayStackKeyOf(map, id), key);
const at = order.indexOf(layerId);
const next = order[at + 1];
if (next === beforeId) return; // already there: moveLayer would fire styledata for nothing
map.moveLayer(layerId, beforeId);
}

// What each GeoJSON source was last given, keyed by the source object itself. A style swap
// replaces the source objects, so a lost source never matches and is written again.
const writtenFeatures = new WeakMap<object, readonly unknown[]>();
Expand Down
14 changes: 3 additions & 11 deletions src/polygon/MapboxPolygonLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
LineFeature,
PolygonFeature,
} from '../helpers';
import { bringMarkerLayersToFront, setSourceFeatures } from '../helpers';
import { bringMarkerLayersToFront, overlayLayerBeforeId, overlayStackKeyOf, setSourceFeatures } from '../helpers';
import { MapboxMapViewHolder } from '../MapboxMapViewHolder';

export interface MapboxActualPolygon {
Expand Down Expand Up @@ -101,11 +101,7 @@ export class MapboxPolygonLayer {
});
}
if (!map.getLayer(this.layerId)) {
const beforeId = map.getLayer('polyline-layer')
? 'polyline-layer'
: map.getLayer('mc-marker-layer')
? 'mc-marker-layer'
: undefined;
const beforeId = overlayLayerBeforeId(map, overlayStackKeyOf(map, this.layerId) ?? { kind: 'polygon', sub: 0 });
map.addLayer(
{
id: this.layerId,
Expand All @@ -122,11 +118,7 @@ export class MapboxPolygonLayer {
);
}
if (!map.getLayer(this.outlineLayerId)) {
const beforeId = map.getLayer('polyline-layer')
? 'polyline-layer'
: map.getLayer('mc-marker-layer')
? 'mc-marker-layer'
: undefined;
const beforeId = overlayLayerBeforeId(map, overlayStackKeyOf(map, this.outlineLayerId) ?? { kind: 'polygon', sub: 1 });
map.addLayer(
{
id: this.outlineLayerId,
Expand Down
4 changes: 2 additions & 2 deletions src/polyline/MapboxPolylineLayer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { GeoJSONSource, LayerSpecification } from 'mapbox-gl';
import type { PolylineEntity } from '@mapconductor/js-sdk-core';
import { bringMarkerLayersToFront, setSourceFeatures, type FeatureCollection, type LineFeature } from '../helpers';
import { bringMarkerLayersToFront, setSourceFeatures, type FeatureCollection, type LineFeature, overlayLayerBeforeId, overlayStackKeyOf } from '../helpers';
import { MapboxMapViewHolder } from '../MapboxMapViewHolder';

export type MapboxActualPolyline = LineFeature[];
Expand Down Expand Up @@ -79,7 +79,7 @@ export class MapboxPolylineLayer {
'line-width': ['get', MapboxPolylineLayer.Prop.STROKE_WIDTH],
},
} as LayerSpecification,
map.getLayer('mc-marker-layer') ? 'mc-marker-layer' : undefined,
overlayLayerBeforeId(map, overlayStackKeyOf(map, this.layerId) ?? { kind: 'polyline' }),
);
}
bringMarkerLayersToFront(map);
Expand Down
45 changes: 41 additions & 4 deletions src/raster/MapboxRasterLayerOverlayRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ import {
type RasterLayerOverlayRenderer,
type RasterLayerState,
} from '@mapconductor/js-sdk-core';
import { bringMarkerLayersToFront, createRasterSource, removeLayerIfExists, removeSourceIfExists } from '../helpers';
import {
bringMarkerLayersToFront,
createRasterSource,
forgetOverlayStackKey,
MARKER_TILES_LAYER_ID,
moveLayerToStackPlace,
overlayLayerBeforeId,
overlayStackKeyOf,
rememberOverlayStackKey,
removeLayerIfExists,
removeSourceIfExists,
} from '../helpers';
import { MapboxMapViewHolder } from '../MapboxMapViewHolder';

/** GL のソース/レイヤー ID の対。android-sdk の MapboxRasterLayerHandle と同一。 */
Expand Down Expand Up @@ -53,10 +64,14 @@ export class MapboxRasterLayerOverlayRenderer
const { prev } = params;
const next = params.current.state;
if (prev.state.source !== next.source) {
this.removeLayer(prev.layer);
// Replaced, not removed: the new layer keeps the old one's place among equal zIndexes.
this.removeLayer(prev.layer, { keepStackPlace: true });
return this.addLayer(next);
}
this.updateLayer(prev.layer, next);
if (prev.state.zIndex !== next.zIndex && prev.layer.layerId !== MARKER_TILES_LAYER_ID && this.canEditStyle()) {
moveLayerToStackPlace(this.holder.map, prev.layer.layerId, this.stackKey(prev.layer.layerId, next));
}
return prev.layer;
});
bringMarkerLayersToFront(this.holder.map);
Expand Down Expand Up @@ -98,14 +113,35 @@ export class MapboxRasterLayerOverlayRenderer
source: handle.sourceId,
paint: { 'raster-opacity': opacity },
} as LayerSpecification,
this.firstBasemapLabelLayerId(),
this.beforeId(handle.layerId, state),
);
} else {
this.holder.map.setPaintProperty(handle.layerId, 'raster-opacity', opacity);
}
return handle;
}

/** This raster's place in the overlay stack: the lowest band, ordered by zIndex. */
private stackKey(layerId: string, state: RasterLayerState) {
return rememberOverlayStackKey(this.holder.map, layerId, 'rasterLayer', state.zIndex);
}

/**
* Under every other overlay (the raster band is the lowest), and under the basemap's labels,
* whichever comes first. Marker tiles are drawn as a raster but belong with the markers.
*/
private beforeId(layerId: string, state: RasterLayerState): string | undefined {
if (layerId === MARKER_TILES_LAYER_ID) {
return overlayLayerBeforeId(this.holder.map, overlayStackKeyOf(this.holder.map, layerId)!);
}
const stack = overlayLayerBeforeId(this.holder.map, this.stackKey(layerId, state));
const label = this.firstBasemapLabelLayerId();
if (label == null) return stack;
if (stack == null) return label;
const layers = this.holder.map.getStyle?.()?.layers?.map((layer) => layer.id) ?? [];
return layers.indexOf(label) < layers.indexOf(stack) ? label : stack;
}

/**
* The style's first label layer, so a raster overlay can go under it.
*
Expand Down Expand Up @@ -150,7 +186,8 @@ export class MapboxRasterLayerOverlayRenderer
}
}

private removeLayer(handle: MapboxRasterLayerHandle): void {
private removeLayer(handle: MapboxRasterLayerHandle, { keepStackPlace = false } = {}): void {
if (!keepStackPlace) forgetOverlayStackKey(this.holder.map, handle.layerId);
if (!this.canEditStyle()) {
this.pendingRemovals.push(handle);
return;
Expand Down
Loading