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
56 changes: 49 additions & 7 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {
createGeoPoint,
type GeoPoint,
type GeoPointInterface,
overlayStackBeforeId,
type OverlayKind,
type OverlayStackKey,
type PolygonState,
} from '@mapconductor/js-sdk-core';
import * as atlas from 'azure-maps-control';
Expand All @@ -14,19 +17,58 @@ export type Position = [number, number];
/** Id of the marker `SymbolLayer` (see AzureMapsMarkerOverlayRenderer). */
export const MARKER_SYMBOL_LAYER_ID = 'mc-marker-symbols-layer';

// Stacking keys of the layers this SDK added, per map, by layer id.
const stackKeys = new WeakMap<object, Map<string, OverlayStackKey>>();
const stackSequences = new WeakMap<object, number>();

const MARKER_TILES_STATE_ID = 'mc-marker-tiles';

function stackKeyOf(map: atlas.Map, layerId: string): OverlayStackKey | null {
if (layerId === MARKER_SYMBOL_LAYER_ID) return { kind: 'marker', sub: 1 };
return stackKeys.get(map)?.get(layerId) ?? null;
}

/**
* Add vector-overlay layers just below the marker layer so markers always draw
* on top of shapes (as DOM HtmlMarkers used to). If the marker layer does not
* exist yet, the layers are simply added on top.
* Adds an overlay's layers at its place in the shared stack (js-sdk-core OVERLAY_STACK_ORDER):
* raster < ground image < polygon < circle < polyline < marker, zIndex within the band, and
* equal zIndexes in the order they were handed over. The layers of one overlay (fill, stroke)
* stay together in the order given.
*
* Every overlay used to go "just below the marker layer", so the order between kinds was the
* order they were added in; rasters went on top of everything, markers included.
*/
export function addLayersBelowMarkers(
export function addStackedLayers(
map: atlas.Map,
layers: atlas.layer.Layer | atlas.layer.Layer[],
kind: OverlayKind,
zIndex: number | null | undefined = 0,
): void {
const hasMarkerLayer = map.layers
const list = Array.isArray(layers) ? layers : [layers];
let keys = stackKeys.get(map);
if (!keys) {
keys = new Map();
stackKeys.set(map, keys);
}
const sequence = (stackSequences.get(map) ?? 0) + 1;
stackSequences.set(map, sequence);
list.forEach((layer, sub) => keys!.set(layer.getId(), { kind, zIndex: zIndex ?? 0, sequence, sub }));
const order = map.layers
.getLayers()
.some(layer => typeof layer.getId === 'function' && layer.getId() === MARKER_SYMBOL_LAYER_ID);
map.layers.add(layers, hasMarkerLayer ? MARKER_SYMBOL_LAYER_ID : undefined);
.map((layer) => (typeof layer.getId === 'function' ? layer.getId() : ''));
const beforeId = overlayStackBeforeId(order, (id) => stackKeyOf(map, id), keys.get(list[0].getId())!);
map.layers.add(list, beforeId);
}

/** Marker tiles are mounted as a raster but belong with the markers. */
export function rasterStackKind(stateId: string): OverlayKind {
return stateId === MARKER_TILES_STATE_ID ? 'marker' : 'rasterLayer';
}

/** Forget the keys of layers that were removed. */
export function forgetStackedLayers(map: atlas.Map, layers: atlas.layer.Layer | atlas.layer.Layer[]): void {
const keys = stackKeys.get(map);
if (!keys) return;
for (const layer of Array.isArray(layers) ? layers : [layers]) keys.delete(layer.getId());
}

export function toPosition(point: GeoPointInterface): Position {
Expand Down
9 changes: 6 additions & 3 deletions src/raster/AzureMapsRasterLayer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { addStackedLayers, forgetStackedLayers, rasterStackKind } from '../helpers';
import {
RasterLayerController,
RasterLayerManager,
Expand Down Expand Up @@ -60,15 +61,17 @@ export class AzureMapsRasterLayerRenderer {
const options = tileLayerOptions(state);
if (!options) return null;
const layer = new atlas.layer.TileLayer(options);
// Insert raster tiles beneath interactive overlays where possible: place it
// before the first existing non-base layer so markers/vectors stay on top.
this.holder.map.layers.add(layer);
// The lowest overlay band: under ground images, shapes and markers. It used to be added on
// top of everything, markers included.
const kind = rasterStackKind(state.id);
addStackedLayers(this.holder.map, layer, kind, kind === 'marker' ? 0 : state.zIndex);
return layer;
}

private remove(layer: atlas.layer.TileLayer): void {
try {
this.holder.map.layers.remove(layer);
forgetStackedLayers(this.holder.map, layer);
} catch {
// Layer may already be gone if the map/style was torn down.
}
Expand Down
14 changes: 9 additions & 5 deletions src/vector/AzureMapsVectorControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from '@mapconductor/js-sdk-core';
import * as atlas from 'azure-maps-control';
import { AzureMapsMapViewHolder } from '../AzureMapsMapViewHolder';
import { addLayersBelowMarkers, closeRingPositions, pathToPositions, polygonRingsToPositions } from '../helpers';
import { addStackedLayers, closeRingPositions, forgetStackedLayers, pathToPositions, polygonRingsToPositions } from '../helpers';

/**
* A rendered vector overlay on Azure Maps: a DataSource (or none, for image
Expand Down Expand Up @@ -70,7 +70,7 @@ export class AzureMapsCircleRenderer extends AbstractCircleOverlayRenderer<Azure
source.add(new atlas.data.Feature(new atlas.data.Polygon([ring])));
const fill = new atlas.layer.PolygonLayer(source, undefined, { fillColor: state.fillColor, fillOpacity: 1 });
const stroke = new atlas.layer.LineLayer(source, undefined, { strokeColor: state.strokeColor, strokeWidth: state.strokeWidth });
addLayersBelowMarkers(map, [fill, stroke]);
addStackedLayers(map, [fill, stroke], 'circle', state.zIndex);
// Clicks are resolved geometrically from the tapped coordinate in
// AzureMapsViewController (see its map 'click' handler), not via Azure's
// per-layer hit-testing, so no layer click handler is attached here.
Expand All @@ -80,6 +80,7 @@ export class AzureMapsCircleRenderer extends AbstractCircleOverlayRenderer<Azure
stroke,
cleanup: () => {
map.layers.remove([fill, stroke]);
forgetStackedLayers(map, [fill, stroke]);
map.sources.remove(source);
},
};
Expand Down Expand Up @@ -131,7 +132,7 @@ export class AzureMapsPolylineRenderer extends AbstractPolylineOverlayRenderer<A
lineJoin: 'round',
lineCap: 'round',
});
addLayersBelowMarkers(map, line);
addStackedLayers(map, line, 'polyline', state.zIndex);
// Clicks are resolved geometrically from the tapped coordinate in
// AzureMapsViewController (distance-to-segment), not via Azure's per-layer
// hit-testing, so no layer click handler is attached here.
Expand All @@ -140,6 +141,7 @@ export class AzureMapsPolylineRenderer extends AbstractPolylineOverlayRenderer<A
stroke: line,
cleanup: () => {
map.layers.remove(line);
forgetStackedLayers(map, line);
map.sources.remove(source);
},
};
Expand Down Expand Up @@ -192,7 +194,7 @@ export class AzureMapsPolygonRenderer extends AbstractPolygonOverlayRenderer<Azu
source.add(new atlas.data.Feature(new atlas.data.Polygon(polygonRings(state))));
const fill = new atlas.layer.PolygonLayer(source, undefined, { fillColor: state.fillColor, fillOpacity: 1 });
const stroke = new atlas.layer.LineLayer(source, undefined, { strokeColor: state.strokeColor, strokeWidth: state.strokeWidth });
addLayersBelowMarkers(map, [fill, stroke]);
addStackedLayers(map, [fill, stroke], 'polygon', state.zIndex);
// Clicks are resolved geometrically from the tapped coordinate in
// AzureMapsViewController (point-in-polygon), not via Azure's per-layer
// hit-testing, so no layer click handler is attached here.
Expand All @@ -202,6 +204,7 @@ export class AzureMapsPolygonRenderer extends AbstractPolygonOverlayRenderer<Azu
stroke,
cleanup: () => {
map.layers.remove([fill, stroke]);
forgetStackedLayers(map, [fill, stroke]);
map.sources.remove(source);
},
};
Expand Down Expand Up @@ -283,7 +286,7 @@ export class AzureMapsGroundImageRenderer extends AbstractGroundImageOverlayRend
coordinates,
opacity: state.opacity,
});
addLayersBelowMarkers(map, layer);
addStackedLayers(map, layer, 'groundImage');
// Clicks are resolved geometrically from the tapped coordinate in
// AzureMapsViewController (bounds containment), not via Azure's per-layer
// hit-testing, so no layer click handler is attached here.
Expand All @@ -292,6 +295,7 @@ export class AzureMapsGroundImageRenderer extends AbstractGroundImageOverlayRend
sourceUrl: state.imageUrl,
cleanup: () => {
map.layers.remove(layer);
forgetStackedLayers(map, layer);
image.revoke();
},
};
Expand Down