Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ import com.mapconductor.core.tileserver.TileServerRegistry
import com.mapconductor.googlemaps.GoogleMapActualMarker
import com.mapconductor.googlemaps.GoogleMapViewHolder
import java.util.UUID
import kotlin.math.floor
import kotlin.time.Duration.Companion.milliseconds
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -56,8 +60,13 @@ internal class GoogleMapMarkerController private constructor(
private val defaultMarkerIcon: BitmapIcon = DefaultMarkerIcon().toBitmapIcon()
private val tiledMarkerIds = LinkedHashSet<String>()

// タイルを描くスレッドからも読む([displayTileZoom])。
@Volatile
private var lastKnownZoom: Double = 0.0

/** 前回タイルを描き直したときの [displayTileZoom]。 */
private var lastDisplayTileZoom: Int? = null

// Tile rendering via RasterLayer
private val tileServer = TileServerRegistry.get()
private var markerTileRenderer: MarkerTileRenderer<GoogleMapActualMarker>? = null
Expand Down Expand Up @@ -139,11 +148,12 @@ internal class GoogleMapMarkerController private constructor(
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) {
Expand All @@ -161,13 +171,15 @@ internal class GoogleMapMarkerController private constructor(
tiling = true,
),
)
syncTiledOverlay()
scheduleTileRefresh()
return
}

if (wasTiled) {
tiledMarkerIds.remove(state.id)
}
// ネイティブマーカーとして描くときだけ焼き込む。タイル担当なら要らない。
val markerIcon = state.icon?.toBitmapIcon() ?: defaultMarkerIcon

val renderEntity =
MarkerEntity(
Expand Down Expand Up @@ -203,14 +215,33 @@ internal class GoogleMapMarkerController private constructor(
renderer.onPostProcess()

if (tiledMarkerIds.isNotEmpty()) {
syncTiledOverlay()
scheduleTileRefresh()
} else {
removeTileOverlay()
}
}
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 =
(renderer as GoogleMapMarkerRenderer).coroutine.launch {
delay(TILE_REFRESH_COALESCE)
semaphore.withPermit { syncTiledOverlay() }
}
}

override suspend fun clear() {
viewportSwitch.destroy()
semaphore.withPermit {
Expand All @@ -230,9 +261,20 @@ internal class GoogleMapMarkerController private constructor(
viewportSwitch.onCameraChanged(mapCameraPosition)
lastKnownZoom = mapCameraPosition.zoom

// Also update the MarkerTileRenderer's camera zoom for fractional zoom support
// Google Maps は切り捨てたズームのタイルを出す。マーカーとラベルを MapLibre 系と
// 同じ大きさに見せるため、タイルは四捨五入したズームに合わせて描いている
// ([MarkerTileRenderer] の displayTileZoom)。それが変わったら、地図が持っている
// タイルは大きさが合わなくなるので取り直させる。
val displayZoom = displayTileZoom()
if (displayZoom != lastDisplayTileZoom) {
lastDisplayTileZoom = displayZoom
if (markerTileRenderer != null) scheduleTileRefresh()
}
}

/** カメラのズームを四捨五入したズーム(MapLibre 系がタイルに使うズーム)。 */
private fun displayTileZoom(): Int = floor(lastKnownZoom + 0.5).toInt()

/**
* マーカータイルのラスターレイヤの表示だけを切り替える。
*
Expand All @@ -249,6 +291,8 @@ internal class GoogleMapMarkerController private constructor(

override fun destroy() {
viewportSwitch.destroy()
// 待っている描き直しが後から走ると、外したタイル経路をまた登録してしまう。
tileRefreshJob?.cancel()
// Clean up tile server registration
// Unregister this map's route only. Never stop the server here: it is
// a process-wide singleton shared by all map controllers and overlay
Expand Down Expand Up @@ -339,6 +383,7 @@ internal class GoogleMapMarkerController private constructor(
debugTileOverlay = markerTiling.debugTileOverlay,
iconScaleCallback = markerTiling.iconScaleCallback,
declutterPx = markerTiling.declutterPx,
displayTileZoom = ::displayTileZoom,
)
markerTileRenderer = tileRenderer

Expand Down Expand Up @@ -376,6 +421,9 @@ internal class GoogleMapMarkerController private constructor(
}

companion object {
/** 続けて来る変更を 1 回の描き直しにまとめる窓。1 フレーム分。 */
private val TILE_REFRESH_COALESCE = 16.milliseconds

private const val TAG = "GoogleMapMarkerController"

fun create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.concurrent.atomic.AtomicLong
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.os.SystemClock
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class GoogleMapRasterLayerOverlayRenderer(
Expand All @@ -35,6 +39,12 @@ class GoogleMapRasterLayerOverlayRenderer(
) : RasterLayerOverlayRendererInterface<TileOverlay> {
private val localRequests = mutableMapOf<TileOverlay, GoogleLocalTileRequestQueue>()

/** 各オーバーレイがタイルを最後に返した時刻。差し替えた古い方を外す時機に使う。 */
private val tileActivity = mutableMapOf<TileOverlay, AtomicLong>()

/** 差し替え済みで、新しい方のタイルが揃うのを待って外す古いオーバーレイ。 */
private val retiring = mutableSetOf<TileOverlay>()

internal fun updateLocalZoom(zoom: Double) {
localRequests.values.forEach { it.updateZoom(zoom) }
}
Expand Down Expand Up @@ -77,9 +87,12 @@ class GoogleMapRasterLayerOverlayRenderer(
val prev = params.prev
val next = params.current.state
if (prev.state.source != next.source || prev.state.debug != next.debug) {
// 先に外すと、新しいタイルが届くまでの間(マーカータイルで 0.3 秒ほど)
// レイヤーが丸ごと消えて点滅する。新しい方を足してから古い方を外す。
localRequests.remove(prev.layer)?.close()
prev.layer.remove()
addLayer(next)
val added = addLayer(next)
retireWhenReplaced(prev.layer, added)
added
} else {
updateLayer(prev.layer, next)
prev.layer
Expand All @@ -91,8 +104,41 @@ class GoogleMapRasterLayerOverlayRenderer(
withContext(coroutine.coroutineContext) {
data.forEach { entity ->
localRequests.remove(entity.layer)?.close()
tileActivity.remove(entity.layer)
entity.layer.remove()
}
retiring.forEach { it.remove() }
retiring.clear()
}
}

/**
* [old] を、[replacement] がタイルを返し終えたころに外す。
*
* Google Maps にはタイルを描き終えた通知が無いので、新しい方がタイルを 1 枚以上返し、
* そのあと [RETIRE_QUIET_MS] のあいだ頼まれなくなったら揃ったとみなす。地図が
* 何も頼まない(見えている範囲にタイルが無い)こともあるので、[RETIRE_TIMEOUT_MS] で
* 打ち切って外す。
*/
private fun retireWhenReplaced(
old: TileOverlay,
replacement: TileOverlay?,
) {
retiring.add(old)
val activity = replacement?.let { tileActivity[it] }
coroutine.launch {
val start = SystemClock.elapsedRealtime()
while (true) {
val now = SystemClock.elapsedRealtime()
if (now - start >= RETIRE_TIMEOUT_MS) break
val last = activity?.get() ?: 0L
if (last > 0L && now - last >= RETIRE_QUIET_MS) break
delay(RETIRE_POLL_MS)
}
if (retiring.remove(old)) {
tileActivity.remove(old)
old.remove()
}
}
}

Expand Down Expand Up @@ -130,6 +176,7 @@ class GoogleMapRasterLayerOverlayRenderer(
} else {
null
}
val activity = AtomicLong(0L)
val provider =
object : TileProvider {
private var firstTileLogged = false
Expand Down Expand Up @@ -159,6 +206,7 @@ class GoogleMapRasterLayerOverlayRenderer(
// A cancelled/failed tile is retryable. NO_TILE would
// cache a permanent hole when this zoom is revisited.
val bytes = runCatching { requests.renderTile(url, zoom) }.getOrNull() ?: return null
activity.set(SystemClock.elapsedRealtime())
val rendered = if (state.debug) {
addDebugOverlay(bytes, tileSpec.tileSize, x, schemeY, zoom, tileSpec.scheme)
} else {
Expand Down Expand Up @@ -197,6 +245,7 @@ class GoogleMapRasterLayerOverlayRenderer(
imageBytes
}

activity.set(SystemClock.elapsedRealtime())
// Return a new Tile with the image data
return Tile(tileSpec.tileSize, tileSpec.tileSize, renderedBytes)
}
Expand All @@ -214,6 +263,7 @@ class GoogleMapRasterLayerOverlayRenderer(
.transparency(opacityToTransparency(state.opacity))
.visible(state.visible)
val overlay = holder.map.addTileOverlay(options)
if (overlay != null) tileActivity[overlay] = activity
if (requests != null) {
if (overlay != null) localRequests[overlay] = requests else requests.close()
}
Expand Down Expand Up @@ -262,6 +312,14 @@ class GoogleMapRasterLayerOverlayRenderer(

private companion object {
private const val TAG = "GoogleMapRasterLayerOverlayRenderer"

/** 新しいオーバーレイへの依頼がこれだけ途切れたら、古い方を外す。 */
private const val RETIRE_QUIET_MS = 150L

/** 新しい方が何も頼まれなくても、古い方はこれだけ待って外す。 */
private const val RETIRE_TIMEOUT_MS = 1_500L

private const val RETIRE_POLL_MS = 30L
private const val MARKER_TILE_RASTER_ID_PREFIX = "marker-tile-"
}

Expand Down
Loading