From 6a100bf4561c9b7b81fbc7351e73124e4a99e062 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Thu, 5 Nov 2026 14:58:14 -0800 Subject: [PATCH 1/2] Redraw tiles once per burst of tiled-marker changes Same change as android-for-maplibre: relabelling street trees changes a few dozen tiled markers at once, and each update() replaced the raster source, so the map fetched every visible tile again per marker (on MapLibre, Pixel 5a: 45 replacements, 494 tile renders, last tile 6.1 s after the change). - Changes arriving within a frame share one tile refresh; a pending refresh is cancelled on destroy() so it cannot register the tile route again. - The tiling check reads the entity count without copying all entities. - The icon is baked only when the marker is drawn natively. - onPostProcess runs only when a native marker became tiled (it copies every entity to redraw the native ones). --- .../marker/OpenMobileMapsMarkerController.kt | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/mapconductor/openmobilemaps/marker/OpenMobileMapsMarkerController.kt b/src/main/java/com/mapconductor/openmobilemaps/marker/OpenMobileMapsMarkerController.kt index f5d8684..6f17c67 100644 --- a/src/main/java/com/mapconductor/openmobilemaps/marker/OpenMobileMapsMarkerController.kt +++ b/src/main/java/com/mapconductor/openmobilemaps/marker/OpenMobileMapsMarkerController.kt @@ -21,8 +21,11 @@ import com.mapconductor.core.tileserver.TileServerRegistry import com.mapconductor.openmobilemaps.OpenMobileMapsActualMarker import com.mapconductor.openmobilemaps.OpenMobileMapsMarkerOverlayRenderer import java.util.UUID +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.sync.withPermit @@ -123,11 +126,12 @@ class OpenMobileMapsMarkerController( if (currentFinger == prevFinger) return semaphore.withPermit { + // 件数だけが要る。allEntities() は全件の写しを作るので、ラベルの付け替えのように + // 数十本が続けて変わると、そのたびに 14 万件を写すことになる。 val tilingEnabled = - markerTiling.enabled && markerManager.allEntities().size >= markerManager.minMarkerCount + markerTiling.enabled && markerManager.getMemoryStats().entityCount >= markerManager.minMarkerCount val wantsTiled = tilingEnabled && !state.draggable && state.getAnimation() == null val wasTiled = tiledMarkerIds.contains(state.id) - val markerIcon = state.icon?.toBitmapIcon() ?: defaultMarkerIcon if (wantsTiled) { if (!wasTiled) { @@ -145,14 +149,19 @@ class OpenMobileMapsMarkerController( tiling = true, ), ) - renderer.onPostProcess() - syncTiledOverlay() + // タイル担当のままなら、ネイティブのマーカーには何も起きていない。onPostProcess は + // 全件を写してネイティブ分を描き直すので(14 万件で 1 回 50ms)、ネイティブから + // タイルへ移ったときだけ呼ぶ。 + if (!wasTiled) renderer.onPostProcess() + scheduleTileRefresh() return@withPermit } if (wasTiled) { tiledMarkerIds.remove(state.id) } + // ネイティブマーカーとして描くときだけ焼き込む。タイル担当なら要らない。 + val markerIcon = state.icon?.toBitmapIcon() ?: defaultMarkerIcon val params = object : MarkerOverlayRendererInterface.ChangeParamsInterface { @@ -183,7 +192,7 @@ class OpenMobileMapsMarkerController( renderer.onPostProcess() if (tiledMarkerIds.isNotEmpty()) { - syncTiledOverlay() + scheduleTileRefresh() } else { removeTileOverlay() } @@ -191,6 +200,25 @@ class OpenMobileMapsMarkerController( viewportSwitch.requestReapply() } + private var tileRefreshJob: Job? = null + + /** + * 1 本ずつの変更からの描き直しを 1 回にまとめる。 + * + * ラベルの付け替えのように数十本が続けて変わると、1 本ごとにラスターソースを差し替え、 + * そのたびに地図が見えているタイルを全部取り直していた(MapLibre・Pixel 5a で 45 回、 + * 描き直したタイル 494 枚、最後のタイルまで 6 秒)。続けて来た変更は + * [TILE_REFRESH_COALESCE] の間に受けて、まとめて 1 回だけ差し替える。 + */ + private fun scheduleTileRefresh() { + if (tileRefreshJob?.isActive == true) return + tileRefreshJob = + coroutine.launch { + delay(TILE_REFRESH_COALESCE) + semaphore.withPermit { syncTiledOverlay() } + } + } + override suspend fun clear() { viewportSwitch.destroy() semaphore.withPermit { @@ -226,6 +254,8 @@ class OpenMobileMapsMarkerController( override fun destroy() { viewportSwitch.destroy() + // 待っている描き直しが後から走ると、外したタイル経路をまた登録してしまう。 + tileRefreshJob?.cancel() // タイルサーバはプロセス共有のシングルトン。**stop してはいけない** // (他の地図やオーバーレイ拡張のタイルまで止まる)。自分の経路だけ外す。 markerTileGroupId?.let { tileServer.unregister(it) } @@ -319,4 +349,9 @@ class OpenMobileMapsMarkerController( rasterLayerCallback?.onRasterLayerUpdate(null) markerTileRasterLayerState = null } + + private companion object { + /** 続けて来る変更を 1 回の描き直しにまとめる窓。1 フレーム分。 */ + val TILE_REFRESH_COALESCE = 16.milliseconds + } } From e228268f0275eb73850591925632fa3b194d6e43 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Thu, 5 Nov 2026 16:06:25 -0800 Subject: [PATCH 2/2] Show raster overlays at the rounded camera zoom OpenMobileMaps picks the floored level, so at 17.75 the marker tile layer showed z17 stretched by 1.68x, and street-tree icons and labels came out twice as large as on MapLibre, Mapbox, HERE and ArcGIS, which show z18 at 0.84x. Raster overlays now switch levels half a step early (zoom level scale factor sqrt 2), which makes the pick round to the nearest level. The base map keeps the default. --- api/android-for-openmobilemaps.api | 4 ++-- .../mapconductor/openmobilemaps/OpenMobileMapsOverlays.kt | 3 +++ .../openmobilemaps/tile/WebMercatorTileLayerConfig.kt | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/api/android-for-openmobilemaps.api b/api/android-for-openmobilemaps.api index 963de70..5e95939 100644 --- a/api/android-for-openmobilemaps.api +++ b/api/android-for-openmobilemaps.api @@ -343,8 +343,8 @@ public final class com.mapconductor.openmobilemaps.marker.OpenMobileMapsMarkerCo public final class com.mapconductor.openmobilemaps.tile.WebMercatorTileLayerConfig extends io.openmobilemaps.mapscore.shared.map.layers.tiled.Tiled2dMapLayerConfig { public static final double WORLD_WIDTH_METERS; public static final double HALF_WORLD; - public com.mapconductor.openmobilemaps.tile.WebMercatorTileLayerConfig(java.lang.String, java.lang.String, int, int, int, com.mapconductor.core.raster.TileScheme, int, boolean); - public com.mapconductor.openmobilemaps.tile.WebMercatorTileLayerConfig(java.lang.String, java.lang.String, int, int, int, com.mapconductor.core.raster.TileScheme, int, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker); + public com.mapconductor.openmobilemaps.tile.WebMercatorTileLayerConfig(java.lang.String, java.lang.String, int, int, int, com.mapconductor.core.raster.TileScheme, int, boolean, boolean); + public com.mapconductor.openmobilemaps.tile.WebMercatorTileLayerConfig(java.lang.String, java.lang.String, int, int, int, com.mapconductor.core.raster.TileScheme, int, boolean, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker); public int getCoordinateSystemIdentifier(); public java.lang.String getLayerName(); public java.lang.String getTileUrl(int, int, int, int); diff --git a/src/main/java/com/mapconductor/openmobilemaps/OpenMobileMapsOverlays.kt b/src/main/java/com/mapconductor/openmobilemaps/OpenMobileMapsOverlays.kt index 8f7939c..51886c4 100644 --- a/src/main/java/com/mapconductor/openmobilemaps/OpenMobileMapsOverlays.kt +++ b/src/main/java/com/mapconductor/openmobilemaps/OpenMobileMapsOverlays.kt @@ -674,6 +674,9 @@ class OpenMobileMapsRasterLayerOverlayRenderer( // ラスターオーバーレイは透過前提なので、粗い親レベルを重ね描きしない numDrawPreviousLayers = 0, maskTile = true, + // 切り捨てだと、ズーム 17.75 で z17 を 1.68 倍に伸ばして出す。MapLibre 系は z18 を + // 0.84 倍で出すので、マーカータイルのアイコンとラベルが 2 倍違って見えた。 + roundToNearestLevel = true, ) val layer = Tiled2dMapRasterLayerInterface.create(config, loaders) layer.setAlpha(state.opacity) diff --git a/src/main/java/com/mapconductor/openmobilemaps/tile/WebMercatorTileLayerConfig.kt b/src/main/java/com/mapconductor/openmobilemaps/tile/WebMercatorTileLayerConfig.kt index 8bdcd0a..d05243b 100644 --- a/src/main/java/com/mapconductor/openmobilemaps/tile/WebMercatorTileLayerConfig.kt +++ b/src/main/java/com/mapconductor/openmobilemaps/tile/WebMercatorTileLayerConfig.kt @@ -35,6 +35,8 @@ import kotlin.math.pow * @param urlTemplate `{z}` `{x}` `{y}` を含む URL。 * @param tileSize タイル 1 枚の一辺(**dp**)。OSM 系は 256、@2x 系は 512。 * @param scheme [TileScheme.TMS] なら y を反転する。 + * @param roundToNearestLevel true ならカメラのズームを四捨五入したレベルのタイルを出す + * (MapLibre 系・Mapbox・HERE・ArcGIS と同じ)。false は OpenMobileMaps の既定どおり切り捨てる。 */ class WebMercatorTileLayerConfig( private val layerName: String, @@ -45,6 +47,7 @@ class WebMercatorTileLayerConfig( private val scheme: TileScheme = TileScheme.XYZ, private val numDrawPreviousLayers: Int = 2, private val maskTile: Boolean = false, + private val roundToNearestLevel: Boolean = false, ) : Tiled2dMapLayerConfig() { override fun getCoordinateSystemIdentifier(): Int = CoordinateSystemIdentifiers.EPSG3857() @@ -94,7 +97,8 @@ class WebMercatorTileLayerConfig( override fun getZoomInfo(): Tiled2dMapZoomInfo = Tiled2dMapZoomInfo( - 1.0f, + // √2 で次のレベルへ半段早く切り替わり、切り捨てが四捨五入になる。 + if (roundToNearestLevel) 1.4142135f else 1.0f, numDrawPreviousLayers, 0, false,