From 0281eb9075bee40d3a264f9c3e6d6685d27d1df1 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 23:35:42 -0800 Subject: [PATCH] Stack overlays in one band order, whatever order they arrive in Polygons, circles, polylines, markers and ground images were added with addLayer (top of the style), so a re-sent ground image covered the polygons. Rasters went below the first label, except with Standard (basemap imported, no top-level symbol layer), where they went on top and covered every overlay; a raster with zIndex > 0 was put at the raw style index .at(zIndex), among the basemap's own layers. All overlay layers now get their LayerPosition from MapboxOverlayStacking (ios-sdk-core's OverlayStacking): raster layers < ground images < polygons < circles < polylines < markers (marker tiles and cluster layers in the marker band), zIndex within a band, equal keys in the order passed. Rasters stay under the first basemap label where the style has one. A raster's zIndex change moves it within the band. Requires ios-sdk-core fix/overlay-stacking-order (OverlayStacking). --- .../MapboxOverlayStacking.swift | 37 ++++++++++++++++ .../circle/CircleLayer.swift | 3 +- .../MapboxGroundImageOverlayRenderer.swift | 4 +- .../marker/MapboxMarkerController.swift | 3 +- .../marker/MarkerLayer.swift | 3 +- .../polygon/PolygonLayer.swift | 5 ++- .../polyline/PolylineLayer.swift | 3 +- .../MapboxRasterLayerOverlayRenderer.swift | 43 ++++++++++--------- 8 files changed, 73 insertions(+), 28 deletions(-) create mode 100644 Sources/MapConductorForMapbox/MapboxOverlayStacking.swift diff --git a/Sources/MapConductorForMapbox/MapboxOverlayStacking.swift b/Sources/MapConductorForMapbox/MapboxOverlayStacking.swift new file mode 100644 index 0000000..745f508 --- /dev/null +++ b/Sources/MapConductorForMapbox/MapboxOverlayStacking.swift @@ -0,0 +1,37 @@ +import Foundation +import MapboxMaps +@_spi(MapConductorDriver) import MapConductorCore + +/// オーバーレイのレイヤを、重なり順の帯(``OverlayBand``)の位置へ置くための `LayerPosition`。 +/// +/// 下から ラスター < グラウンドイメージ < ポリゴン < サークル < ポリライン < マーカー。 +/// 追加した順・送り直した順・デザイン変更の後の作り直しの順に左右されない。 +/// 以前は各描画担当が `addLayer(_:)`(一番上に積む)で足していたので、後から足したもの +/// (clearOverlays の後に送り直したグラウンドイメージなど)が上に来ていた。 +/// +/// ラスターだけはベースマップのラベルの下に置く(ラベルを覆わないため。従来どおり)。 +/// Standard スタイルのようにベースマップが import でトップレベルのレイヤを持たないときは +/// 天井が見つからず、ほかのオーバーレイより下という帯の条件だけで置く。 +enum MapboxOverlayStacking { + static func position( + for key: OverlayStackKey, + in mapboxMap: MapboxMap, + excluding layerId: String? = nil, + rasterZIndex: [String: Int] = [:] + ) -> LayerPosition { + let layers = mapboxMap.allLayerIdentifiers.filter { $0.id != layerId } + let keys: [OverlayStackKey?] = layers.map { info in + guard let band = OverlayStacking.band(ofStyleLayerId: info.id) else { return nil } + return OverlayStackKey(band, zIndex: band == .rasterLayer ? (rasterZIndex[info.id] ?? 0) : 0) + } + let ceiling = key.band == .rasterLayer + ? layers.firstIndex { $0.type == .symbol && OverlayStacking.band(ofStyleLayerId: $0.id) == nil } + : nil + let index = OverlayStacking.insertionIndex(for: key, among: keys, ceiling: ceiling) + return index >= layers.count ? .default : .below(layers[index].id) + } + + static func add(_ layer: any Layer, _ key: OverlayStackKey, to mapboxMap: MapboxMap) { + try? mapboxMap.addLayer(layer, layerPosition: position(for: key, in: mapboxMap)) + } +} diff --git a/Sources/MapConductorForMapbox/circle/CircleLayer.swift b/Sources/MapConductorForMapbox/circle/CircleLayer.swift index 715efeb..7aa4fbb 100644 --- a/Sources/MapConductorForMapbox/circle/CircleLayer.swift +++ b/Sources/MapConductorForMapbox/circle/CircleLayer.swift @@ -1,5 +1,6 @@ import Foundation import MapboxMaps +@_spi(MapConductorDriver) import MapConductorCore /// Draws circles as "fill (FillLayer) + stroke (LineLayer)". /// @@ -46,7 +47,7 @@ final class CircleLayer { if !mapboxMap.layerExists(withId: layerId) { var fillLayer = FillLayer(id: layerId, source: sourceId) fillLayer.fillColor = .expression(Exp(.toColor) { Exp(.get) { Prop.fillColor } }) - try? mapboxMap.addLayer(fillLayer) + MapboxOverlayStacking.add(fillLayer, OverlayStackKey(.circle), to: mapboxMap) } if !mapboxMap.layerExists(withId: strokeLayerId) { diff --git a/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageOverlayRenderer.swift b/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageOverlayRenderer.swift index 393e9d0..e81c60c 100644 --- a/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageOverlayRenderer.swift +++ b/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageOverlayRenderer.swift @@ -1,6 +1,6 @@ import Foundation import MapboxMaps -import MapConductorCore +@_spi(MapConductorDriver) import MapConductorCore import UIKit @MainActor @@ -115,7 +115,7 @@ final class MapboxGroundImageOverlayRenderer: AbstractGroundImageOverlayRenderer var layer = RasterLayer(id: layerId, source: sourceId) layer.rasterOpacity = .constant(opacity.clampedOpacity) - try? mapboxMap.addLayer(layer) + MapboxOverlayStacking.add(layer, OverlayStackKey(.groundImage), to: mapboxMap) } private func removeSourceAndLayerIfExists(mapboxMap: MapboxMap, sourceId: String, layerId: String) { diff --git a/Sources/MapConductorForMapbox/marker/MapboxMarkerController.swift b/Sources/MapConductorForMapbox/marker/MapboxMarkerController.swift index 643608a..2851a38 100644 --- a/Sources/MapConductorForMapbox/marker/MapboxMarkerController.swift +++ b/Sources/MapConductorForMapbox/marker/MapboxMarkerController.swift @@ -359,7 +359,8 @@ final class MapboxMarkerController: AbstractMarkerController { @@ -45,7 +45,16 @@ final class MapboxRasterLayerOverlayRenderer: AbstractRasterLayerOverlayRenderer NSLog("[MapConductor] RasterLayer debug mode: id=%@", current.state.id) } if finger.zIndex != prevFinger.zIndex { - try? mapboxMap.moveLayer(withId: layer.layerId, to: current.state.zIndex > 0 ? .at(current.state.zIndex) : .default) + zIndexByLayerId[layer.layerId] = current.state.zIndex + try? mapboxMap.moveLayer( + withId: layer.layerId, + to: MapboxOverlayStacking.position( + for: OverlayStacking.rasterKey(stateId: current.state.id, zIndex: current.state.zIndex), + in: mapboxMap, + excluding: layer.layerId, + rasterZIndex: zIndexByLayerId + ) + ) } if finger.opacity != prevFinger.opacity { try? mapboxMap.updateLayer(withId: layer.layerId, type: MapboxMaps.RasterLayer.self) { l in @@ -113,28 +122,22 @@ final class MapboxRasterLayerOverlayRenderer: AbstractRasterLayerOverlayRenderer try? mapboxMap.addLayer(layer, layerPosition: layerPosition) } - /// A raster overlay goes above the basemap's geometry but **below its - /// labels**. + /// 帯の順の位置(ラスターの帯の中は zIndex、ベースマップのラベルの下)。``MapboxOverlayStacking`` を参照。 /// - /// Added at the top of the style instead, it covers the place names, road - /// names and shields the backend draws -- a vector tile layer's own roads - /// run straight through them, which is what "the labels are under the - /// lines" looks like. Every raster overlay we add has the same problem, so - /// the rule lives here rather than in each of them. - /// - /// An explicit zIndex still wins: a caller that has said where the layer - /// goes has said it for a reason. Our own layers are skipped when looking - /// for the anchor -- markers are a symbol layer too, and anchoring to them - /// would put the raster back above the labels. + /// 以前は `zIndex > 0` をスタイル全体の生の添字(`.at(zIndex)`)として使っていて、 + /// ベースマップのレイヤの間(多くは地図の下)へ入っていた。 private func layerPosition(for state: RasterLayerState, mapboxMap: MapboxMap) -> LayerPosition { - if state.zIndex > 0 { return .at(state.zIndex) } - let anchor = mapboxMap.allLayerIdentifiers.first { - $0.type == .symbol && !$0.id.hasPrefix("mapconductor-") - } - guard let anchor else { return .default } - return .below(anchor.id) + zIndexByLayerId["mapconductor-raster-layer-\(state.id)"] = state.zIndex + return MapboxOverlayStacking.position( + for: OverlayStacking.rasterKey(stateId: state.id, zIndex: state.zIndex), + in: mapboxMap, + rasterZIndex: zIndexByLayerId + ) } + /// 差し込み位置を決めるための、載せているラスターの zIndex(スタイルのレイヤは zIndex を持たない)。 + private var zIndexByLayerId: [String: Int] = [:] + private func removeIfExists(mapboxMap: MapboxMap, sourceId: String, layerId: String) { if mapboxMap.layerExists(withId: layerId) { try? mapboxMap.removeLayer(withId: layerId) } if mapboxMap.sourceExists(withId: sourceId) { try? mapboxMap.removeSource(withId: sourceId) }