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
17 changes: 17 additions & 0 deletions api/android-for-longdo.api
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ public final class com.mapconductor.longdo.LongdoOverlayStacking {
public static final com.mapconductor.longdo.LongdoOverlayStacking INSTANCE;
public final java.lang.String addLayerJs(java.lang.String, com.mapconductor.core.controller.OverlayKind, int);
public final java.lang.String addPolygonTileLayerJs(java.lang.String);
public final java.lang.String addPolylineTileLayerJs(java.lang.String);
public final java.lang.String addMarkerTileLayerJs(java.lang.String);
}
public final class com.mapconductor.longdo.LongdoOverlayState {
Expand Down Expand Up @@ -234,6 +235,18 @@ public final class com.mapconductor.longdo.NativeCamera {
public int hashCode();
public boolean equals(java.lang.Object);
}
public final class com.mapconductor.longdo.PageLayerWait {
public static final com.mapconductor.longdo.PageLayerWait INSTANCE;
public static final java.lang.String MAP_EXPR;
public static final long TIMEOUT_MS;
public static final long INTERVAL_MS;
public final java.lang.String script(java.util.List<java.lang.String>);
public final int parse(java.lang.String);
public final java.lang.Object await(java.util.List<java.lang.String>, long, long, kotlin.jvm.functions.Function2<? super java.lang.String, ? super kotlin.coroutines.Continuation<? super java.lang.String>, ? extends java.lang.Object>, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
}
public final class com.mapconductor.longdo.PageLayerWaitKt {
public static final java.lang.Object awaitPageLayers(com.longdo.sdk3.LongdoMap, java.util.List<java.lang.String>, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
}
public final class com.mapconductor.longdo.circle.LongdoCircleController extends com.mapconductor.core.circle.CircleController<com.mapconductor.longdo.circle.LongdoCircleHandle> {
public com.mapconductor.longdo.circle.LongdoCircleController(com.mapconductor.longdo.circle.LongdoCircleOverlayRenderer, com.mapconductor.core.circle.CircleManagerInterface<com.mapconductor.longdo.circle.LongdoCircleHandle>);
public com.mapconductor.longdo.circle.LongdoCircleController(com.mapconductor.longdo.circle.LongdoCircleOverlayRenderer, com.mapconductor.core.circle.CircleManagerInterface, int, kotlin.jvm.internal.DefaultConstructorMarker);
Expand Down Expand Up @@ -354,6 +367,10 @@ public final class com.mapconductor.longdo.marker.LongdoMarkerTileRenderer {
}
final class com.mapconductor.longdo.marker.LongdoMarkerTileRenderer$Companion {
}
public final class com.mapconductor.longdo.marker.MarkerIdIndex {
public com.mapconductor.longdo.marker.MarkerIdIndex();
public final synchronized boolean contains(java.util.List<com.mapconductor.core.marker.MarkerState>, java.lang.String);
}
public final class com.mapconductor.longdo.polygon.LongdoPolygonController extends com.mapconductor.core.polygon.PolygonController<com.mapconductor.longdo.polygon.LongdoPolygonHandle> {
public com.mapconductor.longdo.polygon.LongdoPolygonController(com.mapconductor.longdo.polygon.LongdoPolygonOverlayRenderer, com.mapconductor.core.polygon.PolygonManagerInterface<com.mapconductor.longdo.polygon.LongdoPolygonHandle>);
public com.mapconductor.longdo.polygon.LongdoPolygonController(com.mapconductor.longdo.polygon.LongdoPolygonOverlayRenderer, com.mapconductor.core.polygon.PolygonManagerInterface, int, kotlin.jvm.internal.DefaultConstructorMarker);
Expand Down
63 changes: 58 additions & 5 deletions src/main/java/com/mapconductor/longdo/LongdoMapViewController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@ import com.mapconductor.core.polygon.PolygonCapableInterface
import com.mapconductor.core.polygon.PolygonTiler
import com.mapconductor.core.polygon.polygonTileRasterLayerState
import com.mapconductor.core.polyline.OnPolylineEventHandler
import com.mapconductor.core.polyline.POLYLINE_TILE_LAYER_ID
import com.mapconductor.core.polyline.PolylineCapableInterface
import com.mapconductor.core.polyline.PolylineState
import com.mapconductor.core.polyline.PolylineTiler
import com.mapconductor.core.polyline.polylineTileRasterLayerState
import com.mapconductor.core.raster.RasterLayerCapableInterface
import com.mapconductor.core.raster.RasterLayerState
import com.mapconductor.longdo.circle.LongdoCircleController
import com.mapconductor.longdo.circle.LongdoCircleOverlayRenderer
import com.mapconductor.longdo.groundimage.LongdoGroundImageController
import com.mapconductor.longdo.groundimage.LongdoGroundImageOverlayRenderer
import com.mapconductor.longdo.marker.LongdoMarkerTileRenderer
import com.mapconductor.longdo.marker.MarkerIdIndex
import com.mapconductor.longdo.polygon.LongdoPolygonController
import com.mapconductor.longdo.polygon.LongdoPolygonOverlayRenderer
import com.mapconductor.longdo.polyline.LongdoPolylineController
Expand All @@ -41,7 +45,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
* MapConductor コアと Longdo Map API3 SDK([LongdoMap])を橋渡しするマップコントローラ。
Expand Down Expand Up @@ -114,12 +120,28 @@ class LongdoMapViewController(
PolygonTiler().apply {
onTemplate = { template, tileSize ->
if (template != null) {
mountRasterLayer(polygonTileRasterLayerState(template, tileSize))
// Returns once the tile layer is on the page's map: the tiled
// polygons' onAdded is called on the return.
mountRasterLayerOnMap(polygonTileRasterLayerState(template, tileSize))
} else {
unmountRasterLayer(POLYGON_TILE_LAYER_ID)
}
}
}

// Many polylines too: the same raster layer path, in the polyline band.
polylineController.tiler =
PolylineTiler().apply {
onTemplate = { template, tileSize ->
if (template != null) {
// Returns once the tile layer is on the page's map: the tiled
// polylines' onAdded is called on the return.
mountRasterLayerOnMap(polylineTileRasterLayerState(template, tileSize))
} else {
unmountRasterLayer(POLYLINE_TILE_LAYER_ID)
}
}
}
// ホルダーの投影にカメラを供給する。ホルダーはコントローラより先に作られるので
// ここで繋ぐ。これが無いと toScreenOffset が常に null を返し、InfoBubble と
// マーカー追従が理由も出ずに落ちる。
Expand Down Expand Up @@ -271,6 +293,8 @@ class LongdoMapViewController(
override suspend fun compositionMarkers(data: List<MarkerState>) {
if (shouldUseMarkerLayer(data.size)) {
renderTiledMarkers(data)
// The tiled markers' onAdded waits for their tile layer (see [PageLayerWait]).
markerTileState?.let { longdoMap.awaitPageLayers(listOf(rasterLayerId(it.id))) }
} else {
_markers.value = data
}
Expand All @@ -285,12 +309,17 @@ class LongdoMapViewController(
}
}

override fun hasMarker(state: MarkerState): Boolean =
if (markerTileRenderer?.markers?.isNotEmpty() == true) {
markerTileRenderer?.markers?.any { it.id == state.id } ?: false
override fun hasMarker(state: MarkerState): Boolean {
val tiled = markerTileRenderer?.markers
return if (tiled?.isNotEmpty() == true) {
tiledMarkerIds.contains(tiled, state.id)
} else {
_markers.value.any { it.id == state.id }
overlayMarkerIds.contains(_markers.value, state.id)
}
}

private val tiledMarkerIds = MarkerIdIndex()
private val overlayMarkerIds = MarkerIdIndex()

@Deprecated("Use MarkerState.onDragStart instead.")
override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) = Unit
Expand Down Expand Up @@ -324,6 +353,9 @@ class LongdoMapViewController(
appliedRasters.remove(id)
}
data.forEach { applyRaster(it) }
// Returns once the layers are on the page's map, so their onAdded comes after
// (see [PageLayerWait]). A source Longdo cannot show is not applied, nor waited for.
longdoMap.awaitPageLayers(data.filter { it.id in appliedRasters }.map { rasterLayerId(it.id) })
}

/**
Expand Down Expand Up @@ -363,6 +395,27 @@ class LongdoMapViewController(
mainCoroutine.launch { applyRaster(state) }
}

/**
* [mountRasterLayer] that returns once the layer is on the page's map, for
* the polygon tiles: the tiled polygons' `onAdded` is called on the return.
* [mountRasterLayer] only queues the script (and before `ready` only keeps
* the layer), so they were reported before the map had their tiles' layer.
* Applied here rather than launched, so that a replaced source is gone
* before the layer is looked for.
*/
internal suspend fun mountRasterLayerOnMap(state: RasterLayerState) {
withContext(Dispatchers.Main.immediate) {
if (mapReady) {
mountedRasterIds.add(state.id)
applyRaster(state)
} else {
mountRasterLayer(state)
mapLoaded.first { it }
}
}
longdoMap.awaitPageLayers(listOf(rasterLayerId(state.id)))
}

override fun unmountRasterLayer(id: String) {
mountedRasterIds.remove(id)
pendingMountedRasters.remove(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.mapconductor.longdo

import com.mapconductor.core.controller.OverlayKind
import com.mapconductor.core.polygon.POLYGON_TILE_LAYER_ID
import com.mapconductor.core.polyline.POLYLINE_TILE_LAYER_ID
import com.mapconductor.core.raster.RasterHeaderRuleSet
import com.mapconductor.core.raster.RasterLayerSource
import com.mapconductor.core.raster.RasterLayerState
Expand Down Expand Up @@ -44,6 +45,8 @@ private fun rasterAddLayerJs(
): String =
if (state.id == POLYGON_TILE_LAYER_ID) {
LongdoOverlayStacking.addPolygonTileLayerJs(layerSpec)
} else if (state.id == POLYLINE_TILE_LAYER_ID) {
LongdoOverlayStacking.addPolylineTileLayerJs(layerSpec)
} else {
LongdoOverlayStacking.addLayerJs(layerSpec, OverlayKind.RasterLayer, state.zIndex)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ internal object LongdoOverlayStacking {
/** ポリゴンのタイル: ポリゴンの帯の一番下(画像になったポリゴンであって、地図の画像ではない)。 */
fun addPolygonTileLayerJs(layerSpec: String): String = addLayerJs(layerSpec, OverlayKind.Polygon, Int.MIN_VALUE)

/** ポリラインのタイル: ポリラインの帯の一番下(サークルの上、ネイティブのポリラインの下)。 */
fun addPolylineTileLayerJs(layerSpec: String): String = addLayerJs(layerSpec, OverlayKind.Polyline, Int.MIN_VALUE)

/** マーカーのタイル: マーカーの帯の一番下。 */
fun addMarkerTileLayerJs(layerSpec: String): String = addLayerJs(layerSpec, OverlayKind.Marker, Int.MIN_VALUE)
}
113 changes: 113 additions & 0 deletions src/main/java/com/mapconductor/longdo/PageLayerWait.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.mapconductor.longdo

import com.longdo.sdk3.LongdoMap
import android.util.Log
import kotlin.coroutines.resume
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull

/**
* Waits until the page's map has the layers an overlay was just given.
*
* Longdo is a page in a WebView: `LongdoMap.run` queues the script and returns,
* and an add that finds the style not ready retries from the page with
* `setTimeout`.
* So a renderer's `onAdd` returned before its layers were on the map, and
* the overlay's `onAdded` (asked for right after the hand-over) came while
* the map still showed nothing. Boston's 604 downtown polygons reported
* `onAdded` 0.1 s after the hand-over; the last of their layers reached the
* map 1.4 s after it (Lenovo TB520FU).
* Renderers call [await] at the end of `onAdd`, so `compositionXxx`
* returns, and `onAdded` is called, once the layers are there.
*
* Only that the layers are in the style is waited for, not their tiles or
* images (same as every other provider).
*/
internal object PageLayerWait {
/** The page's map, or a falsy value while there is none. */
const val MAP_EXPR = "window.map && map.Renderer"

/**
* Only a guard against a layer the page refused, which never shows up:
* a few hundred layers can take the page well over 10 s.
*/
const val TIMEOUT_MS = 60_000L
const val INTERVAL_MS = 100L

/**
* Script that answers how many of [ids], from the front, the map has
* (as text, e.g. `"3"`); `"0"` while there is no map.
*/
fun script(ids: List<String>): String {
val list = ids.joinToString(",", "[", "]") { quote(it) }
return "(function(){try{var m=$MAP_EXPR;if(!m)return '0';var ids=$list;" +
"for(var i=0;i<ids.length;i++){if(!m.getLayer(ids[i]))return String(i);}" +
"return String(ids.length);}catch(e){return '0';}})()"
}

/** Reads [script]'s answer from what the bridge returned (it may come back JSON-quoted). */
fun parse(result: String?): Int = result?.trim()?.trim('"')?.toIntOrNull() ?: 0

/**
* Returns once the map has every one of [ids], asking through [evaluate]
* every [intervalMs]. Gives up after [timeoutMs] (returns false and logs):
* a layer the page refused never shows up, and the overlay is reported
* then rather than never.
*/
suspend fun await(
ids: List<String>,
timeoutMs: Long = TIMEOUT_MS,
intervalMs: Long = INTERVAL_MS,
evaluate: suspend (String) -> String?,
): Boolean {
if (ids.isEmpty()) return true
var missing = ids
val done =
withTimeoutOrNull(timeoutMs) {
while (missing.isNotEmpty()) {
val present =
try {
parse(evaluate(script(missing)))
} catch (e: CancellationException) {
throw e
} catch (_: Exception) {
0
}
// Layers mostly arrive in the order they were added: ask only for the rest.
missing = missing.drop(present)
if (missing.isNotEmpty()) delay(intervalMs)
}
true
} ?: false
if (!done) {
runCatching { Log.w(TAG, "${missing.size} of ${ids.size} layers not on the map after $timeoutMs ms") }
}
return done
}

private fun quote(text: String): String =
buildString {
append('"')
for (c in text) {
when {
c == '"' -> append("\\\"")
c == '\\' -> append("\\\\")
c < ' ' || c == '\u2028' || c == '\u2029' -> append(String.format("\\u%04x", c.code))
else -> append(c)
}
}
append('"')
}

private const val TAG = "PageLayerWait"
}

/** [PageLayerWait.await] against this map's page. */
internal suspend fun LongdoMap.awaitPageLayers(ids: List<String>): Boolean =
PageLayerWait.await(ids) { js ->
suspendCancellableCoroutine { continuation ->
run(js) { result -> if (continuation.isActive) continuation.resume(result) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.mapconductor.core.controller.OverlayKind
import com.mapconductor.core.geometry.OverlayGeoJson
import com.mapconductor.core.geometry.circleToRing
import com.mapconductor.longdo.LongdoOverlayStacking
import com.mapconductor.longdo.awaitPageLayers
import org.json.JSONObject

/**
Expand All @@ -23,9 +24,17 @@ import org.json.JSONObject
class LongdoCircleOverlayRenderer(
private val longdoMap: LongdoMap,
) : CircleOverlayRendererInterface<LongdoCircleHandle> {
/**
* Returns once the layers are on the page's map, so the circles'
* `onAdded` comes after they are (see [com.mapconductor.longdo.PageLayerWait]).
*/
override suspend fun onAdd(
data: List<CircleOverlayRendererInterface.AddParamsInterface>,
): List<LongdoCircleHandle?> = data.map { addCircle(it.state) }
): List<LongdoCircleHandle?> {
val handles = data.map { addCircle(it.state) }
longdoMap.awaitPageLayers(handles.filterNotNull().flatMap { listOf(it.fillLayerId, it.lineLayerId) })
return handles
}

override suspend fun onChange(
data: List<CircleOverlayRendererInterface.ChangeParamsInterface<LongdoCircleHandle>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.mapconductor.core.groundimage.GroundImageEntityInterface
import com.mapconductor.core.groundimage.GroundImageOverlayRendererInterface
import com.mapconductor.core.groundimage.GroundImageState
import com.mapconductor.longdo.LongdoOverlayStacking
import com.mapconductor.longdo.awaitPageLayers
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import android.graphics.Bitmap
Expand All @@ -31,9 +32,17 @@ import android.util.Base64
class LongdoGroundImageOverlayRenderer(
private val longdoMap: LongdoMap,
) : GroundImageOverlayRendererInterface<LongdoGroundImageHandle> {
/**
* Returns once the layers are on the page's map, so the ground images'
* `onAdded` comes after they are (see [com.mapconductor.longdo.PageLayerWait]).
*/
override suspend fun onAdd(
data: List<GroundImageOverlayRendererInterface.AddParamsInterface>,
): List<LongdoGroundImageHandle?> = data.map { addGroundImage(it.state) }
): List<LongdoGroundImageHandle?> {
val handles = data.map { addGroundImage(it.state) }
longdoMap.awaitPageLayers(handles.mapNotNull { it?.layerId })
return handles
}

override suspend fun onChange(
data: List<GroundImageOverlayRendererInterface.ChangeParamsInterface<LongdoGroundImageHandle>>,
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/com/mapconductor/longdo/marker/MarkerIdIndex.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.mapconductor.longdo.marker

import com.mapconductor.core.marker.MarkerState

/**
* The ids of a marker list, rebuilt only when the list itself is replaced.
*
* `hasMarker` is asked once per marker after every composition (that is how
* `MarkerState.onAdded` finds out what is on the map) and on every in-place
* change. A linear search of the list made the first of those quadratic: 1e5
* markers, 1e10 comparisons.
*/
internal class MarkerIdIndex {
private var list: List<MarkerState>? = null
private var ids: Set<String> = emptySet()

@Synchronized
fun contains(
markers: List<MarkerState>,
id: String,
): Boolean {
if (markers !== list) {
ids = markers.mapTo(HashSet(markers.size * 2)) { it.id }
list = markers
}
return id in ids
}
}
Loading
Loading