From 56d167e095719b986b0ae5a37042ae3038f88f62 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sun, 8 Nov 2026 00:17:19 -0800 Subject: [PATCH] Stack overlays in one fixed order HERE draws the map's own object layer over every layer in map.getLayers(). In it, spatial objects are ordered by zIndex, an H.map.Overlay bitmap is drawn over the spatials whatever its zIndex (the ground image had -100,000,000 and still covered the polygons and the polyline), and markers over both. - Ground images move to an ObjectLayer of their own in map.getLayers(), above the rasters: under the polygons, circles and polylines now. - Polygons, circles and polylines get separate zIndex bands (overlayStackZ, 1024 wide). Circles shared the polygons' 0..511, so the one added later was on top. - Rasters go in under the ground-image layer, by zIndex. Needs the js-sdk-core release with overlayStackZ / overlayStackBeforeId. --- api/react-for-here.api.d.ts | 13 +++++++ src/circle/HereCircleOverlayRenderer.ts | 5 +-- .../HereGroundImageOverlayRenderer.ts | 27 +++++++++----- src/helpers.ts | 36 +++++++++++++++++++ src/here.d.ts | 8 ++++- src/polygon/HerePolygonOverlayRenderer.ts | 5 +-- src/polyline/HerePolylineOverlayRenderer.ts | 5 +-- src/raster/HereRasterLayerOverlayRenderer.ts | 15 ++++++-- 8 files changed, 96 insertions(+), 18 deletions(-) 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 {