From 11b424edb29c482dd5b2a7a0705a0df206b7c3fc Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Fri, 30 Oct 2026 01:01:44 -0700 Subject: [PATCH] Size 3D tiled markers by the camera's level, not the tile's The 3D SceneView picks a level per tile from the ground it covers on the globe, so one screen mixes two levels (z=10 and z=11 side by side on a Pixel at the post office sample). A tile one level deeper is shown at half the size, and its pins came out half as large as the 2D map's: 26px against 41px, next to correct ones. Tiled icons on the 3D view are now drawn at 2^(tile level - camera level), and the app's icon scale callback is asked for the camera's level, as on the 2D map. The layer is redrawn when the camera's level changes. On the same device every pin now measures 40-41px, matching the 2D map and Google. --- .../com/mapconductor/arcgis/ArcGISMapView.kt | 1 + .../arcgis/marker/ArcGISMarkerController.kt | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt b/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt index 4e54992..3365460 100644 --- a/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt +++ b/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt @@ -446,6 +446,7 @@ internal fun getMarkerController( markerManager = markerManager, renderer = renderer, markerTiling = markerTiling, + scaleIconsByLevel = holder.usesSceneView, ) return controller } diff --git a/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerController.kt b/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerController.kt index 4d78148..2132b60 100644 --- a/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerController.kt +++ b/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerController.kt @@ -21,6 +21,8 @@ import com.mapconductor.core.raster.RasterLayerState import com.mapconductor.core.raster.TileScheme import com.mapconductor.core.tileserver.TileServerRegistry import java.util.UUID +import kotlin.math.pow +import kotlin.math.roundToInt import android.os.SystemClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -31,6 +33,11 @@ class ArcGISMarkerController( markerManager: MarkerManager, renderer: ArcGISMarkerRenderer, private val markerTiling: MarkerTilingOptions, + /** + * Size tiled icons by how far a tile's level is from the camera's (3D + * `SceneView`). See [levelIconScale]. + */ + private val scaleIconsByLevel: Boolean = false, val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), ) : AbstractMarkerController( markerManager = markerManager, @@ -48,6 +55,10 @@ class ArcGISMarkerController( @Volatile private var lastKnownZoom: Double = 0.0 + /** The level the 2D map would draw at the current camera; see [levelIconScale]. */ + @Volatile + private var cameraLevel: Int? = null + private val tileServer = TileServerRegistry.get(forceNoStoreCache = true) private var markerTileRenderer: MarkerTileRenderer? = null private var markerTileGroupId: String? = null @@ -205,6 +216,36 @@ class ArcGISMarkerController( // 判定と昇格は debounce したうえで切り替え器の中で走る(パン中は動かない)。 viewportSwitch.onCameraChanged(mapCameraPosition) lastKnownZoom = mapCameraPosition.zoom + if (scaleIconsByLevel) { + val level = mapCameraPosition.zoom.roundToInt() + if (level != cameraLevel) { + cameraLevel = level + // Every tile's icon size depends on the camera's level. + semaphore.withPermit { updateRasterLayerSource() } + } + } + } + + /** + * Icon scale for a tile at [tileLevel], from the app's callback. + * + * The 2D map draws one level per camera, so a tile's level is the camera's + * and the callback's scale goes in as it is. The 3D `SceneView` picks a + * level per tile from the ground it covers on the globe and mixes two + * levels on one screen (a Pixel at the post office sample asked for z=10 + * and z=11 side by side). A tile one level deeper is shown at half the + * size, and its pins came out half as large as the 2D map's (26px against + * 41px) next to correct ones. Drawn at 2^(tile level - camera level), every + * pin comes out the size the 2D map gives it at that camera, and the + * callback is asked for the camera's level as the 2D map does. + */ + private fun levelIconScale( + state: MarkerState, + tileLevel: Int, + ): Double { + val level = cameraLevel?.takeIf { scaleIconsByLevel } ?: tileLevel + val base = markerTiling.iconScaleCallback?.invoke(state, level) ?: 1.0 + return base * 2.0.pow(tileLevel - level) } /** @@ -291,7 +332,8 @@ class ArcGISMarkerController( tileSize = 256, cacheSizeBytes = markerTiling.cacheSize, debugTileOverlay = markerTiling.debugTileOverlay, - iconScaleCallback = markerTiling.iconScaleCallback, + iconScaleCallback = + if (scaleIconsByLevel) ::levelIconScale else markerTiling.iconScaleCallback, declutterPx = markerTiling.declutterPx, // ArcGIS displays one 96dpi/256px LOD tile over 256dp on screen, // matching the renderer's density-scaled output 1:1 — no extra