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
22 changes: 20 additions & 2 deletions api/android-for-here.api
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public final class com.mapconductor.here.HereMapViewControllerStore extends com.
public final void initSDK(android.content.Context);
}
public final class com.mapconductor.here.HereMapViewKt {
public static final void HereMapView(com.mapconductor.here.HereViewState, 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.MapProjection, kotlin.jvm.functions.Function3<? super com.mapconductor.here.HereViewScope, ? super androidx.compose.runtime.Composer, ? super java.lang.Integer, kotlin.Unit>, androidx.compose.runtime.Composer, int, int, int);
public static final void HereMapView(com.mapconductor.here.HereViewState, 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.MapProjection, 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.here.HereViewScope, ? super androidx.compose.runtime.Composer, ? super java.lang.Integer, kotlin.Unit>, androidx.compose.runtime.Composer, int, int, int);
}
public final class com.mapconductor.here.HereMapViewSaver extends com.mapconductor.compose.map.BaseMapViewSaver<com.mapconductor.here.HereViewState> {
public com.mapconductor.here.HereMapViewSaver();
Expand Down Expand Up @@ -400,11 +400,13 @@ final class com.mapconductor.here.raster.HereRasterLayerOverlayRenderer$TileSpec
public final java.lang.String getSourceName();
public final java.lang.String getLayerName();
public final java.util.List<java.lang.Integer> getStorageLevels();
public final java.lang.String getLocalTemplate();
public final com.here.sdk.mapview.datasource.TileUrlProviderCallback component1();
public final java.lang.String component2();
public final java.lang.String component3();
public final java.util.List<java.lang.Integer> component4();
public final com.mapconductor.here.raster.HereRasterLayerOverlayRenderer$TileSpec copy(com.here.sdk.mapview.datasource.TileUrlProviderCallback, java.lang.String, java.lang.String, java.util.List<java.lang.Integer>);
public final java.lang.String component5();
public final com.mapconductor.here.raster.HereRasterLayerOverlayRenderer$TileSpec copy(com.here.sdk.mapview.datasource.TileUrlProviderCallback, java.lang.String, java.lang.String, java.util.List<java.lang.Integer>, java.lang.String);
public java.lang.String toString();
public int hashCode();
public boolean equals(java.lang.Object);
Expand All @@ -417,6 +419,22 @@ final class com.mapconductor.here.raster.HereRasterTileProxyProvider implements
}
public final class com.mapconductor.here.raster.HereRasterTileProxyProvider$Companion {
}
final class com.mapconductor.here.raster.LocalRasterTileSource implements com.here.sdk.mapview.datasource.RasterTileSource {
public static final int RENDER_WIDTH;
public static final long TEN_YEARS_MS;
public com.mapconductor.here.raster.LocalRasterTileSource(com.mapconductor.core.tileserver.LocalTileServer, java.lang.String, java.util.List<java.lang.Integer>);
public com.here.sdk.mapview.datasource.TileSource$DataVersion getDataVersion(com.here.sdk.mapview.datasource.TileKey);
public void addListener(com.here.sdk.mapview.datasource.TileSource$Listener);
public void removeListener(com.here.sdk.mapview.datasource.TileSource$Listener);
public com.here.sdk.mapview.datasource.TilingScheme getTilingScheme();
public java.util.List<java.lang.Integer> getStorageLevels();
public com.here.sdk.mapview.datasource.TileSource$LoadTileRequestHandle loadTile(com.here.sdk.mapview.datasource.TileKey, com.here.sdk.mapview.datasource.RasterTileSource$LoadResultHandler);
}
final class com.mapconductor.here.raster.LocalRasterTileSource$Companion {
public final kotlinx.coroutines.CoroutineDispatcher getRenderDispatcher();
public final com.here.sdk.mapview.datasource.TileSource$DataVersion getVERSION();
public final com.here.sdk.mapview.datasource.TileSource$TileMetadata getMETADATA();
}
public final class com.mapconductor.here.zoom.ZoomAltitudeConverter extends com.mapconductor.core.zoom.AbstractZoomAltitudeConverter {
public static final com.mapconductor.here.zoom.ZoomAltitudeConverter$Companion Companion;
public static final double HERE_ZOOM_TO_GOOGLE_ZOOM_AT_EQUATOR;
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/mapconductor/here/HereMapView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.mapconductor.core.map.CameraRestriction
import com.mapconductor.core.map.MapCameraPosition
import com.mapconductor.core.map.MapCameraPositionInterface
import com.mapconductor.core.map.MapProjection
import com.mapconductor.core.map.MapViewStyle
import com.mapconductor.core.marker.MarkerEventControllerInterface
import com.mapconductor.core.marker.MarkerOverlayRendererInterface
import com.mapconductor.core.marker.MarkerRenderingStrategyInterface
Expand Down Expand Up @@ -63,6 +64,15 @@ fun HereMapView(
onCameraMove: OnCameraMoveHandler? = null,
onCameraMoveEnd: OnCameraMoveHandler? = null,
projection: MapProjection = MapProjection.Globe,
/**
* 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 HereViewScope.() -> Unit)? = null,
) {
// Warmup the tile server early to reduce latency for raster layers
Expand Down Expand Up @@ -254,6 +264,8 @@ fun HereMapView(
// Pass content if it needs to be rendered within the overlay providers in MapViewBase,
// or handle it here if it's specific to HereMapView 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 @@ -5,6 +5,9 @@ import com.here.sdk.mapview.MapLayer
import com.here.sdk.mapview.MapLayerBuilder
import com.here.sdk.mapview.datasource.RasterDataSource
import com.here.sdk.mapview.datasource.RasterDataSourceConfiguration
import com.here.sdk.mapview.datasource.RasterTileSource
import com.here.sdk.mapview.datasource.TileKey
import com.here.sdk.mapview.datasource.TileSource
import com.here.sdk.mapview.datasource.TileUrlProviderCallback
import com.here.sdk.mapview.datasource.TileUrlProviderFactory
import com.here.sdk.mapview.datasource.TilingScheme
Expand All @@ -20,6 +23,8 @@ import com.mapconductor.here.HereViewHolder
import java.io.ByteArrayOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
Expand All @@ -28,6 +33,10 @@ import android.util.Log
import android.util.LruCache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

class HereRasterLayerOverlayRenderer(
private val holder: HereViewHolder,
Expand Down Expand Up @@ -83,30 +92,46 @@ class HereRasterLayerOverlayRenderer(
}
val urlProvider = tileSpec.provider
val storageLevels = tileSpec.storageLevels
val dataSourceProvider =
RasterDataSourceConfiguration.Provider(
urlProvider,
TilingScheme.QUAD_TREE_MERCATOR,
storageLevels,
)
dataSourceProvider.hasAlphaChannel = true
val cache =
RasterDataSourceConfiguration.Cache(
holder.mapView.context.cacheDir.absolutePath,
)
val config =
RasterDataSourceConfiguration(
tileSpec.sourceName,
dataSourceProvider,
cache,
)
val dataSource = RasterDataSource(holder.mapView.mapContext, config)
// Tiles this process draws are handed over in process. HERE fetches
// through its own network stack and, while the device reports no
// connectivity, does not fetch at all -- not even from 127.0.0.1 --
// so a URL-fed source goes blank the moment the network is away.
// A tile source is asked for bytes and answered from the same
// providers, with no network in between.
val localTemplate = tileSpec.localTemplate
val dataSource =
if (localTemplate != null) {
RasterDataSource(
holder.mapView.mapContext,
tileSpec.sourceName,
LocalRasterTileSource(tileServer, localTemplate, storageLevels),
)
} else {
val dataSourceProvider =
RasterDataSourceConfiguration.Provider(
urlProvider,
TilingScheme.QUAD_TREE_MERCATOR,
storageLevels,
)
dataSourceProvider.hasAlphaChannel = true
val cache =
RasterDataSourceConfiguration.Cache(
holder.mapView.context.cacheDir.absolutePath,
)
val config =
RasterDataSourceConfiguration(
tileSpec.sourceName,
dataSourceProvider,
cache,
)
RasterDataSource(holder.mapView.mapContext, config)
}

return try {
val layer =
MapLayerBuilder()
.withName(tileSpec.layerName)
.withDataSource(config.name, MapContentType.RASTER_IMAGE)
.withDataSource(tileSpec.sourceName, MapContentType.RASTER_IMAGE)
.forMap(holder.mapView.hereMap)
.build()
layer.setEnabled(state.visible)
Expand Down Expand Up @@ -179,6 +204,13 @@ class HereRasterLayerOverlayRenderer(
sourceName = "raster-source-${state.id}",
layerName = "raster-layer-${state.id}",
storageLevels = buildStorageLevels(source.minZoom, source.maxZoom),
// The proxy route is local by construction; a direct
// template is local when it is one of the server's own.
localTemplate =
template.takeIf {
it.startsWith(tileServer.baseUrl + "/") &&
(routeId != null || source.scheme != TileScheme.TMS)
},
)
}
is RasterLayerSource.TileJson -> {
Expand Down Expand Up @@ -235,9 +267,96 @@ class HereRasterLayerOverlayRenderer(
val sourceName: String,
val layerName: String,
val storageLevels: List<Int>,
/** The XYZ template on this process's tile server, when the tiles come from there. */
val localTemplate: String? = null,
)
}

/**
* A HERE tile source answered from this process's tile server, with no HTTP
* hop and no dependence on the device's connectivity.
*/
private class LocalRasterTileSource(
private val tileServer: LocalTileServer,
private val template: String,
private val levels: List<Int>,
) : RasterTileSource {
override fun getDataVersion(tileKey: TileKey): TileSource.DataVersion = VERSION

override fun addListener(listener: TileSource.Listener) {}

override fun removeListener(listener: TileSource.Listener) {}

override fun getTilingScheme(): TilingScheme = TilingScheme.QUAD_TREE_MERCATOR

override fun getStorageLevels(): List<Int> = levels

override fun loadTile(
tileKey: TileKey,
handler: RasterTileSource.LoadResultHandler,
): TileSource.LoadTileRequestHandle {
// Exactly one answer per request, whatever happens. A request HERE
// cancels is still one it is waiting on: leave it unanswered and the
// slot is never freed, and after a few gestures HERE stops asking for
// tiles at all -- the map keeps the last picture and looks frozen.
val answered = AtomicBoolean(false)

fun answer(bytes: ByteArray?) {
if (!answered.compareAndSet(false, true)) return
if (bytes != null) handler.loaded(tileKey, bytes, METADATA) else handler.failed(tileKey)
}
// HERE counts rows from the south (Tokyo at z12 is row 2483, not
// 1612); the templates count from the north, as XYZ does.
val row = (1 shl tileKey.level) - 1 - tileKey.y
val url =
template
.replace("{z}", tileKey.level.toString())
.replace("{x}", tileKey.x.toString())
.replace("{y}", row.toString())
val job =
scope.launch {
answer(runCatching { tileServer.renderLocalTile(url) { !isActive } }.getOrNull())
}
// Cancelled, or the coroutine never ran: answered from a render
// thread rather than from whichever thread cancelled it.
job.invokeOnCompletion { if (!answered.get()) scope.launch { answer(null) } }
return TileSource.LoadTileRequestHandle { job.cancel() }
}

/**
* Renders run here and nowhere else.
*
* HERE asks for a screenful at once and every one of those is a tile to
* draw. On `Dispatchers.Default` they take every thread the rest of the
* SDK shares -- camera callbacks, overlays, the vector renderer itself --
* and the map stops answering gestures while it fills. This is the same
* width, and the same reasoning, as the tile server's own worker pool,
* which is what the HTTP path went through.
*/
private val scope = CoroutineScope(SupervisorJob() + renderDispatcher)

private companion object {
@OptIn(ExperimentalCoroutinesApi::class)
val renderDispatcher = Dispatchers.Default.limitedParallelism(RENDER_WIDTH)

/** Tile rendering is CPU-bound; more workers than cores adds contention, not throughput. */
const val RENDER_WIDTH = 8

val VERSION = TileSource.DataVersion(0, 0)

/**
* Tiles drawn here change only with their URL, so they are given a
* long life -- but a plausible one. A date near the end of `Long`
* does not survive the trip into the SDK: every tile came back
* already expired, the same one was asked for again every 400 ms,
* and after a pinch the map stopped asking for tiles at all.
*/
val METADATA = TileSource.TileMetadata(VERSION, Date(System.currentTimeMillis() + TEN_YEARS_MS))

const val TEN_YEARS_MS = 10L * 365 * 24 * 60 * 60 * 1000
}
}

data class HereRasterLayerHandle(
val dataSource: RasterDataSource,
val layer: MapLayer,
Expand Down
Loading