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
4 changes: 3 additions & 1 deletion api/android-for-googlemaps.api
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public final class com.mapconductor.googlemaps.GoogleMapViewHolder implements co
public java.lang.Object getMap();
}
public final class com.mapconductor.googlemaps.GoogleMapViewKt {
public static final void GoogleMapView(com.mapconductor.googlemaps.GoogleMapViewState, androidx.compose.ui.Modifier, com.mapconductor.core.marker.MarkerTilingOptions, com.mapconductor.core.map.CameraRestriction, kotlin.jvm.functions.Function2<? super android.content.Context, ? super kotlin.coroutines.Continuation<? super java.lang.Boolean>, ? extends java.lang.Object>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapViewStateInterface<?>, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.features.GeoPoint, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.features.GeoPoint, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, kotlin.jvm.functions.Function3<? super com.mapconductor.googlemaps.GoogleMapViewScope, ? super androidx.compose.runtime.Composer, ? super java.lang.Integer, kotlin.Unit>, androidx.compose.runtime.Composer, int, int, int);
public static final void GoogleMapView(com.mapconductor.googlemaps.GoogleMapViewState, androidx.compose.ui.Modifier, com.mapconductor.core.marker.MarkerTilingOptions, com.mapconductor.core.map.CameraRestriction, kotlin.jvm.functions.Function2<? super android.content.Context, ? super kotlin.coroutines.Continuation<? super java.lang.Boolean>, ? extends java.lang.Object>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapViewStateInterface<?>, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.features.GeoPoint, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.features.GeoPoint, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.map.MapCameraPosition, kotlin.Unit>, com.mapconductor.core.map.MapViewStyle, kotlin.jvm.functions.Function1<? super java.util.List<java.lang.String>, kotlin.Unit>, kotlin.jvm.functions.Function3<? super com.mapconductor.googlemaps.GoogleMapViewScope, ? super androidx.compose.runtime.Composer, ? super java.lang.Integer, kotlin.Unit>, androidx.compose.runtime.Composer, int, int, int);
public static final com.mapconductor.googlemaps.GoogleMapViewController createGoogleMapViewController(com.mapconductor.googlemaps.GoogleMapViewHolder, com.mapconductor.core.marker.MarkerTilingOptions, com.mapconductor.core.map.MutableMapServiceRegistry);
}
public final class com.mapconductor.googlemaps.GoogleMapViewSaver extends com.mapconductor.compose.map.BaseMapViewSaver<com.mapconductor.googlemaps.GoogleMapViewState> {
Expand Down Expand Up @@ -308,6 +308,8 @@ public final class com.mapconductor.googlemaps.polyline.GoogleMapPolylineOverlay
public final class com.mapconductor.googlemaps.raster.GoogleMapRasterLayerController extends com.mapconductor.core.raster.RasterLayerController<com.google.android.gms.maps.model.TileOverlay> implements com.mapconductor.googlemaps.raster.GoogleMapRasterLayerControllerInterface {
public com.mapconductor.googlemaps.raster.GoogleMapRasterLayerController(com.mapconductor.core.raster.RasterLayerManagerInterface<com.google.android.gms.maps.model.TileOverlay>, com.mapconductor.googlemaps.raster.GoogleMapRasterLayerOverlayRenderer);
public com.mapconductor.googlemaps.raster.GoogleMapRasterLayerController(com.mapconductor.core.raster.RasterLayerManagerInterface, com.mapconductor.googlemaps.raster.GoogleMapRasterLayerOverlayRenderer, int, kotlin.jvm.internal.DefaultConstructorMarker);
public java.lang.Object onCameraChanged(com.mapconductor.core.map.MapCameraPosition, kotlin.coroutines.Continuation<? super kotlin.Unit>);
public void destroy();
}
public interface com.mapconductor.googlemaps.raster.GoogleMapRasterLayerControllerInterface extends com.mapconductor.core.controller.OverlayControllerInterface<com.mapconductor.core.raster.RasterLayerState, com.mapconductor.core.raster.RasterLayerEntityInterface<com.google.android.gms.maps.model.TileOverlay>> {
}
Expand Down
16 changes: 14 additions & 2 deletions src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import com.mapconductor.core.map.MapCameraPosition
import com.mapconductor.core.map.MapCameraPositionInterface
import com.mapconductor.core.map.MapCapability
import com.mapconductor.core.map.MapCapabilityStatus
import com.mapconductor.core.raster.RasterTilePreference
import com.mapconductor.core.raster.RasterTilePreferenceKey
import com.mapconductor.core.map.MapViewStyle
import com.mapconductor.core.map.MutableMapServiceRegistry
import com.mapconductor.core.marker.MarkerEventControllerInterface
import com.mapconductor.core.marker.MarkerOverlayRendererInterface
Expand All @@ -30,6 +29,8 @@ import com.mapconductor.core.marker.MarkerRenderingSupport
import com.mapconductor.core.marker.MarkerRenderingSupportKey
import com.mapconductor.core.marker.MarkerTilingOptions
import com.mapconductor.core.marker.StrategyMarkerController
import com.mapconductor.core.raster.RasterTilePreference
import com.mapconductor.core.raster.RasterTilePreferenceKey
import com.mapconductor.googlemaps.circle.GoogleMapCircleController
import com.mapconductor.googlemaps.circle.GoogleMapCircleOverlayRenderer
import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageController
Expand Down Expand Up @@ -60,6 +61,15 @@ fun GoogleMapView(
onCameraMoveStart: OnCameraMoveHandler? = null,
onCameraMove: OnCameraMoveHandler? = null,
onCameraMoveEnd: OnCameraMoveHandler? = null,
/**
* How the map looks, when the app states it rather than naming a design.
*
* A vector style *is* the basemap. `com.mapconductor:vectorstyle` builds
* one; what happens underneath depends on this backend and the app does
* not have to know.
*/
style: MapViewStyle? = null,
onStyleDiagnostics: ((List<String>) -> Unit)? = null,
content: (@Composable GoogleMapViewScope.() -> Unit)? = null,
) {
val scope = remember { GoogleMapViewScope() } // Use specific scope
Expand Down Expand Up @@ -171,6 +181,8 @@ fun GoogleMapView(
// Pass content if it needs to be rendered within the overlay providers in MapViewBase,
// or handle it here if it's specific to GoogleMapView structure before calling MapViewBase.
// For now, assuming content relates to overlay definitions.
style = style,
onStyleDiagnostics = onStyleDiagnostics,
content = content, // This might need adjustment based on how overlays are handled
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ internal fun GoogleMapViewController.handleCameraMove() {
}

internal fun GoogleMapViewController.handleCameraIdle() {
rasterLayerController.onCameraIdle()
val mapCameraPosition = getMapCameraPosition()
defaultCoroutine.launch { markerController.onCameraChanged(mapCameraPosition) }
emitCameraMoveEnd(getMapCameraPosition())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.mapconductor.googlemaps.raster

import com.mapconductor.core.InternalMapConductorApi
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.math.abs

/** Bounds local renders and discards obsolete zooms before they occupy a worker. */
@InternalMapConductorApi
internal class GoogleLocalTileRequestQueue(
private val width: Int = 4,
private val render: (String, () -> Boolean) -> ByteArray?,
) {
@InternalMapConductorApi
private class Job(val zoom: Int) {
val cancelled = AtomicBoolean(false)
}

private val lock = ReentrantLock()
private val changed = lock.newCondition()
private val pending = mutableListOf<Job>()
private val active = mutableSetOf<Job>()
private val retryAfterZoom = AtomicBoolean(false)
private var zoom: Double? = null
private var closed = false

init {
require(width > 0)
}

fun updateZoom(next: Double) {
if (!next.isFinite()) return
lock.withLock {
zoom = next
for (job in pending + active) {
if (abs(job.zoom - next) > 1.5 && job.cancelled.compareAndSet(false, true)) retryAfterZoom.set(true)
}
pending.removeAll { it.cancelled.get() }
changed.signalAll()
}
}

fun consumeRetryAfterZoom(): Boolean = retryAfterZoom.getAndSet(false)

fun close() {
lock.withLock {
closed = true
for (job in pending + active) job.cancelled.set(true)
pending.clear()
changed.signalAll()
}
}

fun renderTile(url: String, tileZoom: Int): ByteArray? {
val job = Job(tileZoom)
lock.withLock {
if (closed || zoom?.let { abs(tileZoom - it) > 1.5 } == true) return null
pending.add(job)
try {
while (!job.cancelled.get()) {
val current = zoom
val next = pending.minByOrNull { abs(it.zoom - (current ?: it.zoom.toDouble())) }
if (active.size < width && next === job) {
pending.remove(job)
active.add(job)
break
}
if (current != null && next != null) {
val closest = abs(next.zoom - current)
for (running in active) {
if (abs(running.zoom - current) > closest + 0.25 && running.cancelled.compareAndSet(false, true)) {
retryAfterZoom.set(true)
}
}
}
changed.await()
}
if (job.cancelled.get()) return null
} catch (_: InterruptedException) {
job.cancelled.set(true)
Thread.currentThread().interrupt()
return null
} finally {
pending.remove(job)
changed.signalAll()
}
}
return try {
val bytes = render(url) { job.cancelled.get() }
if (job.cancelled.get()) null else bytes
} finally {
lock.withLock {
active.remove(job)
changed.signalAll()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.mapconductor.googlemaps.raster

import com.google.android.gms.maps.model.TileOverlay
import com.mapconductor.core.controller.OverlayControllerInterface
import com.mapconductor.core.map.MapCameraPosition
import kotlinx.coroutines.withContext
import com.mapconductor.core.raster.RasterLayerController
import com.mapconductor.core.raster.RasterLayerEntityInterface
import com.mapconductor.core.raster.RasterLayerManager
Expand All @@ -18,4 +20,19 @@ internal class GoogleMapRasterLayerController(
rasterLayerManager: RasterLayerManagerInterface<TileOverlay> = RasterLayerManager(),
renderer: GoogleMapRasterLayerOverlayRenderer,
) : RasterLayerController<TileOverlay>(rasterLayerManager, renderer),
GoogleMapRasterLayerControllerInterface
GoogleMapRasterLayerControllerInterface {
private val tileRenderer = renderer

override suspend fun onCameraChanged(mapCameraPosition: MapCameraPosition) {
withContext(tileRenderer.coroutine.coroutineContext) {
tileRenderer.updateLocalZoom(mapCameraPosition.zoom)
}
}

internal fun onCameraIdle() { tileRenderer.refreshLocalTilesAfterZoom() }

override fun destroy() {
tileRenderer.cancelLocalRequests()
super.destroy()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.mapconductor.core.raster.RasterLayerOverlayRendererInterface
import com.mapconductor.core.raster.RasterLayerSource
import com.mapconductor.core.raster.RasterLayerState
import com.mapconductor.core.raster.TileScheme
import com.mapconductor.core.tileserver.TileServerRegistry
import com.mapconductor.googlemaps.GoogleMapViewHolder
import okhttp3.Headers
import okhttp3.OkHttpClient
Expand All @@ -32,6 +33,25 @@ class GoogleMapRasterLayerOverlayRenderer(
private val okHttpClient: OkHttpClient,
override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main),
) : RasterLayerOverlayRendererInterface<TileOverlay> {
private val localRequests = mutableMapOf<TileOverlay, GoogleLocalTileRequestQueue>()

internal fun updateLocalZoom(zoom: Double) {
localRequests.values.forEach { it.updateZoom(zoom) }
}

internal fun refreshLocalTilesAfterZoom() {
// Google backs off retryable null tiles. Reset that backoff after a
// zoom settles, so returning to a cancelled zoom does not wait seconds.
localRequests.forEach { (overlay, requests) ->
if (requests.consumeRetryAfterZoom()) overlay.clearTileCache()
}
}

internal fun cancelLocalRequests() {
localRequests.values.forEach { it.close() }
localRequests.clear()
}

private fun isMarkerTileRaster(state: RasterLayerState): Boolean = state.id.startsWith(MARKER_TILE_RASTER_ID_PREFIX)

private fun resolveOverlayZIndex(state: RasterLayerState): Float =
Expand All @@ -57,6 +77,7 @@ class GoogleMapRasterLayerOverlayRenderer(
val prev = params.prev
val next = params.current.state
if (prev.state.source != next.source || prev.state.debug != next.debug) {
localRequests.remove(prev.layer)?.close()
prev.layer.remove()
addLayer(next)
} else {
Expand All @@ -69,6 +90,7 @@ class GoogleMapRasterLayerOverlayRenderer(
override suspend fun onRemove(data: List<RasterLayerEntityInterface<TileOverlay>>) {
withContext(coroutine.coroutineContext) {
data.forEach { entity ->
localRequests.remove(entity.layer)?.close()
entity.layer.remove()
}
}
Expand Down Expand Up @@ -98,6 +120,16 @@ class GoogleMapRasterLayerOverlayRenderer(
}
val requestHeaders = headerBuilder.build()

// Only this registry's local routes bypass HTTP. Remote raster
// sources retain their headers, user agent and OkHttp behaviour.
val server = if (tileSpec.template.startsWith("http://127.0.0.1:")) TileServerRegistry.get() else null
val requests = if (server != null && tileSpec.template.startsWith("${server.baseUrl}/tiles/")) {
GoogleLocalTileRequestQueue { url, cancelled -> server.renderLocalTile(url, cancelled) }.also {
it.updateZoom(holder.map.cameraPosition.zoom.toDouble())
}
} else {
null
}
val provider =
object : TileProvider {
private var firstTileLogged = false
Expand All @@ -106,7 +138,7 @@ class GoogleMapRasterLayerOverlayRenderer(
x: Int,
y: Int,
zoom: Int,
): Tile {
): Tile? {
if (!firstTileLogged) {
firstTileLogged = true
}
Expand All @@ -123,6 +155,18 @@ class GoogleMapRasterLayerOverlayRenderer(
.replace("{y}", schemeY.toString())
.replace("{z}", zoom.toString())

if (requests != null) {
// 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
val rendered = if (state.debug) {
addDebugOverlay(bytes, tileSpec.tileSize, x, schemeY, zoom, tileSpec.scheme)
} else {
bytes
}
return Tile(tileSpec.tileSize, tileSpec.tileSize, rendered)
}

val request: Request =
Request
.Builder()
Expand Down Expand Up @@ -170,6 +214,9 @@ class GoogleMapRasterLayerOverlayRenderer(
.transparency(opacityToTransparency(state.opacity))
.visible(state.visible)
val overlay = holder.map.addTileOverlay(options)
if (requests != null) {
if (overlay != null) localRequests[overlay] = requests else requests.close()
}
return overlay
}

Expand Down
Loading
Loading