diff --git a/api/react-for-here.api.d.ts b/api/react-for-here.api.d.ts index 89952b3..edc5a34 100644 --- a/api/react-for-here.api.d.ts +++ b/api/react-for-here.api.d.ts @@ -338,6 +338,13 @@ declare class HereCircleController extends CircleController { */ declare class HereGroundImageOverlayRenderer extends AbstractGroundImageOverlayRenderer { + private provider; + /** + * Ground images live in an ObjectLayer of their own, in map.getLayers() above the rasters. + * In the map's own object layer an `H.map.Overlay` is drawn over the polygons, circles and + * polylines whatever its zIndex (it was given -100,000,000 and still covered them). + */ + private objects; createGroundImage(state: GroundImageState): Promise; updateGroundImageProperties({ groundImage, current, prev, }: { groundImage: HereActualGroundImage; @@ -416,6 +423,12 @@ declare class HereRasterLayerOverlayRenderer implements RasterLayerOverlayRender onPostProcess(): Promise; private addLayer; private updateLayer; + /** + * Under the ground-image layer (rasters are the lowest band), ordered by zIndex. Marker tiles + * go above it; they still cannot reach above the polygons, which HERE draws in its object + * layer over every layer. + */ + private addToMap; private removeLayer; } diff --git a/src/circle/HereCircleOverlayRenderer.ts b/src/circle/HereCircleOverlayRenderer.ts index 6fd65ed..053c2c3 100644 --- a/src/circle/HereCircleOverlayRenderer.ts +++ b/src/circle/HereCircleOverlayRenderer.ts @@ -21,6 +21,7 @@ import { import type { HereActualCircle } from '../HereTypeAlias'; import { HereViewHolder } from '../HereViewHolder'; import { toGeoCoordinates } from '../GeoPoint'; +import { spatialStackZIndex } from '../helpers'; export class HereCircleOverlayRenderer extends AbstractCircleOverlayRenderer< HereViewHolder, @@ -104,6 +105,6 @@ function buildCircleGeometry(state: CircleState): H.geo.Polygon | null { } function coerceZIndex(zIndex: number): number { - // Mirrors `(state.zIndex ?: 0).coerceIn(0, 511)` in Android. - return Math.max(0, Math.min(511, zIndex)); + // Circle band, above the polygons (was `coerceIn(0, 511)`, the polygons' range). + return spatialStackZIndex('circle', zIndex); } diff --git a/src/groundimage/HereGroundImageOverlayRenderer.ts b/src/groundimage/HereGroundImageOverlayRenderer.ts index f9402c3..8f49b51 100644 --- a/src/groundimage/HereGroundImageOverlayRenderer.ts +++ b/src/groundimage/HereGroundImageOverlayRenderer.ts @@ -17,17 +17,27 @@ import type { HereActualGroundImage } from '../HereTypeAlias'; import type { HereGroundImageHandle } from './HereGroundImageHandle'; import { HereViewHolder } from '../HereViewHolder'; import { toGeoRect } from '../GeoRectBounds'; - -// HereMarkerRenderer.resolveDrawOrder() draws markers at -// `-lat * 1_000_000 - lng`, which ranges over roughly ±90,000,180 depending -// on position. Staying below that whole range keeps the ground image behind -// every default-positioned marker, so a translucent overlay never dims them. -const GROUND_IMAGE_Z_INDEX = -100_000_000; +import { addStackedLayer } from '../helpers'; export class HereGroundImageOverlayRenderer extends AbstractGroundImageOverlayRenderer< HereViewHolder, HereActualGroundImage > { + private provider: H.map.provider.LocalObjectProvider | null = null; + + /** + * Ground images live in an ObjectLayer of their own, in map.getLayers() above the rasters. + * In the map's own object layer an `H.map.Overlay` is drawn over the polygons, circles and + * polylines whatever its zIndex (it was given -100,000,000 and still covered them). + */ + private objects(): ReturnType { + if (this.provider == null) { + this.provider = new H.map.provider.LocalObjectProvider(); + addStackedLayer(this.holder.map, new H.map.layer.ObjectLayer(this.provider), 'groundImage'); + } + return this.provider.getRootGroup(); + } + override async createGroundImage( state: GroundImageState, ): Promise { @@ -37,10 +47,9 @@ export class HereGroundImageOverlayRenderer extends AbstractGroundImageOverlayRe const image = await loadImage(state.imageUrl); const overlay = new H.map.Overlay(rect, image, { opacity: state.opacity, - zIndex: GROUND_IMAGE_Z_INDEX, data: state.id, }); - this.holder.map.addObject(overlay); + this.objects().addObject(overlay); const handle: HereGroundImageHandle = { routeId: `groundimage-${state.id}`, @@ -95,7 +104,7 @@ export class HereGroundImageOverlayRenderer extends AbstractGroundImageOverlayRe entity: GroundImageEntity, ): Promise { if (entity.groundImage) { - this.holder.map.removeObject((entity.groundImage as HereGroundImageHandle).overlay); + this.objects().removeObject((entity.groundImage as HereGroundImageHandle).overlay); } } } diff --git a/src/helpers.ts b/src/helpers.ts index 9da3fd1..55c4184 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,3 +1,4 @@ +import { overlayStackBeforeId, overlayStackZ, type OverlayKind, type OverlayStackKey } from '@mapconductor/js-sdk-core'; /** * Re-exports the line-model namespaces so the HERE renderer modules can import * them from a single local path. Densify a path with @@ -8,3 +9,38 @@ export { WGS84Geodesic, Planar, } from '@mapconductor/js-sdk-core'; + +// ── Overlay stacking ─────────────────────────────────────────────────────── +// +// Bottom to top, on every provider: raster < ground image < polygon < circle < polyline < +// marker (js-sdk-core OVERLAY_STACK_ORDER). +// +// HERE draws map objects in two places. The map's own object layer (map.addObject) is drawn +// over every layer in map.getLayers(); in it, spatial objects (polygons, circles, polylines) +// are ordered by zIndex, `H.map.Overlay` bitmaps are drawn over the spatials whatever their +// zIndex, and markers over both. Rasters are TileLayers in map.getLayers(), so already under +// the objects. Ground images therefore get an ObjectLayer of their own in map.getLayers(), +// above the rasters, and the spatials get bands of zIndex. + +/** zIndex of a spatial object (polygon, circle, polyline): one band of 1024 per kind. */ +export function spatialStackZIndex(kind: 'polygon' | 'circle' | 'polyline', zIndex: number | null | undefined): number { + return overlayStackZ(kind, zIndex, { bandSize: 1024 }); +} + +const layerStackKeys = new WeakMap(); +let layerStackSequence = 0; + +/** + * Adds a layer to map.getLayers() at its place in the stack: rasters (by zIndex), then the + * ground-image layer, then marker tiles. Layers that are not ours (the base layer) are left + * where they are. + */ +export function addStackedLayer(map: H.Map, layer: H.map.layer.Layer, kind: OverlayKind, zIndex: number | null | undefined = 0): void { + const key: OverlayStackKey = { kind, zIndex: zIndex ?? 0, sequence: ++layerStackSequence }; + layerStackKeys.set(layer, key); + const layers: unknown[] = map.getLayers()?.asArray?.() ?? []; + const ids = layers.map((_, i) => String(i)); + const beforeId = overlayStackBeforeId(ids, (id) => layerStackKeys.get(layers[Number(id)] as object) ?? null, key); + if (beforeId == null) map.addLayer(layer); + else map.addLayer(layer, Number(beforeId)); +} diff --git a/src/here.d.ts b/src/here.d.ts index 9eabcd5..71951d4 100644 --- a/src/here.d.ts +++ b/src/here.d.ts @@ -68,7 +68,7 @@ declare global { setBaseLayer(layer: any): void; getLayers(): any; setLayers(layers: any): void; - addLayer(layer: any): void; + addLayer(layer: any, opt_idx?: number): void; removeLayer(layer: any): void; getViewModel(): map.ViewModel; @@ -304,6 +304,12 @@ declare global { constructor(options: ImageTileProviderOptions); } + /** Holds map objects for an `H.map.layer.ObjectLayer` of its own. */ + class LocalObjectProvider extends Provider { + constructor(options?: Record); + getRootGroup(): { addObject(object: any): any; removeObject(object: any): any }; + } + interface ImageTileProviderOptions { uri?: string; min?: number; diff --git a/src/polygon/HerePolygonOverlayRenderer.ts b/src/polygon/HerePolygonOverlayRenderer.ts index 492bfbd..0c8a25a 100644 --- a/src/polygon/HerePolygonOverlayRenderer.ts +++ b/src/polygon/HerePolygonOverlayRenderer.ts @@ -16,6 +16,7 @@ import { import type { HereActualPolygon } from '../HereTypeAlias'; import { HereViewHolder } from '../HereViewHolder'; import { toGeoCoordinates } from '../GeoPoint'; +import { spatialStackZIndex } from '../helpers'; export class HerePolygonOverlayRenderer extends AbstractPolygonOverlayRenderer< HereViewHolder, @@ -107,6 +108,6 @@ function buildLineString( } function coerceZIndex(zIndex: number): number { - // Mirrors `state.zIndex.coerceIn(0, 511)` in Android. - return Math.max(0, Math.min(511, zIndex)); + // Polygon band of the shared stack (was `coerceIn(0, 511)`, shared with circles). + return spatialStackZIndex('polygon', zIndex); } diff --git a/src/polyline/HerePolylineOverlayRenderer.ts b/src/polyline/HerePolylineOverlayRenderer.ts index e1343f6..1edc272 100644 --- a/src/polyline/HerePolylineOverlayRenderer.ts +++ b/src/polyline/HerePolylineOverlayRenderer.ts @@ -16,6 +16,7 @@ import { import type { HereActualPolyline } from '../HereTypeAlias'; import { HereViewHolder } from '../HereViewHolder'; import { toGeoCoordinates } from '../GeoPoint'; +import { spatialStackZIndex } from '../helpers'; export class HerePolylineOverlayRenderer extends AbstractPolylineOverlayRenderer< HereViewHolder, @@ -92,6 +93,6 @@ function createStyle(state: PolylineState): H.map.StyleOptions { } function coerceZIndex(zIndex: number): number { - // Mirrors `(state.zIndex + 512).coerceIn(512, 1023)` in Android. - return Math.max(512, Math.min(1023, zIndex + 512)); + // Polyline band, above the circles (was `(zIndex + 512).coerceIn(512, 1023)`). + return spatialStackZIndex('polyline', zIndex); } diff --git a/src/raster/HereRasterLayerOverlayRenderer.ts b/src/raster/HereRasterLayerOverlayRenderer.ts index 7bc580b..9e1d7df 100644 --- a/src/raster/HereRasterLayerOverlayRenderer.ts +++ b/src/raster/HereRasterLayerOverlayRenderer.ts @@ -28,6 +28,7 @@ import { } from '@mapconductor/js-sdk-core'; import type { HereRasterLayerHandle } from './HereRasterLayerHandle'; import type { HereViewHolder } from '../HereViewHolder'; +import { addStackedLayer } from '../helpers'; interface TileSpec { readonly urlTemplate: string; @@ -98,7 +99,7 @@ export class HereRasterLayerOverlayRenderer const tileLayer = new H.map.layer.TileLayer(provider); const attached = state.visible !== false; if (attached) { - this.holder.map.addLayer(tileLayer); + this.addToMap(tileLayer, state); } return { @@ -121,13 +122,23 @@ export class HereRasterLayerOverlayRenderer const shouldAttach = state.visible !== false; if (shouldAttach === handle.attached) return; if (shouldAttach) { - this.holder.map.addLayer(handle.layer); + this.addToMap(handle.layer, state); } else { this.holder.map.removeLayer(handle.layer); } handle.attached = shouldAttach; } + /** + * Under the ground-image layer (rasters are the lowest band), ordered by zIndex. Marker tiles + * go above it; they still cannot reach above the polygons, which HERE draws in its object + * layer over every layer. + */ + private addToMap(layer: H.map.layer.TileLayer, state: RasterLayerState): void { + const markerTiles = state.id === 'mc-marker-tiles'; + addStackedLayer(this.holder.map, layer, markerTiles ? 'marker' : 'rasterLayer', markerTiles ? 0 : state.zIndex); + } + private async removeLayer( entity: RasterLayerEntity, ): Promise {