diff --git a/api/android-for-longdo.api b/api/android-for-longdo.api index bbab706..9a4b484 100644 --- a/api/android-for-longdo.api +++ b/api/android-for-longdo.api @@ -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 { @@ -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); + public final int parse(java.lang.String); + public final java.lang.Object await(java.util.List, long, long, kotlin.jvm.functions.Function2, ? extends java.lang.Object>, kotlin.coroutines.Continuation); +} +public final class com.mapconductor.longdo.PageLayerWaitKt { + public static final java.lang.Object awaitPageLayers(com.longdo.sdk3.LongdoMap, java.util.List, kotlin.coroutines.Continuation); +} public final class com.mapconductor.longdo.circle.LongdoCircleController extends com.mapconductor.core.circle.CircleController { public com.mapconductor.longdo.circle.LongdoCircleController(com.mapconductor.longdo.circle.LongdoCircleOverlayRenderer, com.mapconductor.core.circle.CircleManagerInterface); public com.mapconductor.longdo.circle.LongdoCircleController(com.mapconductor.longdo.circle.LongdoCircleOverlayRenderer, com.mapconductor.core.circle.CircleManagerInterface, int, kotlin.jvm.internal.DefaultConstructorMarker); @@ -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, java.lang.String); +} public final class com.mapconductor.longdo.polygon.LongdoPolygonController extends com.mapconductor.core.polygon.PolygonController { public com.mapconductor.longdo.polygon.LongdoPolygonController(com.mapconductor.longdo.polygon.LongdoPolygonOverlayRenderer, com.mapconductor.core.polygon.PolygonManagerInterface); public com.mapconductor.longdo.polygon.LongdoPolygonController(com.mapconductor.longdo.polygon.LongdoPolygonOverlayRenderer, com.mapconductor.core.polygon.PolygonManagerInterface, int, kotlin.jvm.internal.DefaultConstructorMarker); diff --git a/src/main/java/com/mapconductor/longdo/LongdoMapViewController.kt b/src/main/java/com/mapconductor/longdo/LongdoMapViewController.kt index 594159d..3ea96ad 100644 --- a/src/main/java/com/mapconductor/longdo/LongdoMapViewController.kt +++ b/src/main/java/com/mapconductor/longdo/LongdoMapViewController.kt @@ -22,8 +22,11 @@ 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 @@ -31,6 +34,7 @@ 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 @@ -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])を橋渡しするマップコントローラ。 @@ -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 と // マーカー追従が理由も出ずに落ちる。 @@ -271,6 +293,8 @@ class LongdoMapViewController( override suspend fun compositionMarkers(data: List) { 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 } @@ -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 @@ -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) }) } /** @@ -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) diff --git a/src/main/java/com/mapconductor/longdo/LongdoMapViewControllerRaster.kt b/src/main/java/com/mapconductor/longdo/LongdoMapViewControllerRaster.kt index 2429862..0e84dad 100644 --- a/src/main/java/com/mapconductor/longdo/LongdoMapViewControllerRaster.kt +++ b/src/main/java/com/mapconductor/longdo/LongdoMapViewControllerRaster.kt @@ -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 @@ -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) } diff --git a/src/main/java/com/mapconductor/longdo/LongdoOverlayStacking.kt b/src/main/java/com/mapconductor/longdo/LongdoOverlayStacking.kt index b561c02..1be98d4 100644 --- a/src/main/java/com/mapconductor/longdo/LongdoOverlayStacking.kt +++ b/src/main/java/com/mapconductor/longdo/LongdoOverlayStacking.kt @@ -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) } diff --git a/src/main/java/com/mapconductor/longdo/PageLayerWait.kt b/src/main/java/com/mapconductor/longdo/PageLayerWait.kt new file mode 100644 index 0000000..8ca9e01 --- /dev/null +++ b/src/main/java/com/mapconductor/longdo/PageLayerWait.kt @@ -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 { + 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, + 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): Boolean = + PageLayerWait.await(ids) { js -> + suspendCancellableCoroutine { continuation -> + run(js) { result -> if (continuation.isActive) continuation.resume(result) } + } + } diff --git a/src/main/java/com/mapconductor/longdo/circle/LongdoCircleOverlayRenderer.kt b/src/main/java/com/mapconductor/longdo/circle/LongdoCircleOverlayRenderer.kt index deb66bd..f1ddd1e 100644 --- a/src/main/java/com/mapconductor/longdo/circle/LongdoCircleOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/longdo/circle/LongdoCircleOverlayRenderer.kt @@ -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 /** @@ -23,9 +24,17 @@ import org.json.JSONObject class LongdoCircleOverlayRenderer( private val longdoMap: LongdoMap, ) : CircleOverlayRendererInterface { + /** + * 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, - ): List = data.map { addCircle(it.state) } + ): List { + 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>, diff --git a/src/main/java/com/mapconductor/longdo/groundimage/LongdoGroundImageOverlayRenderer.kt b/src/main/java/com/mapconductor/longdo/groundimage/LongdoGroundImageOverlayRenderer.kt index f1fc141..3ab1b1d 100644 --- a/src/main/java/com/mapconductor/longdo/groundimage/LongdoGroundImageOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/longdo/groundimage/LongdoGroundImageOverlayRenderer.kt @@ -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 @@ -31,9 +32,17 @@ import android.util.Base64 class LongdoGroundImageOverlayRenderer( private val longdoMap: LongdoMap, ) : GroundImageOverlayRendererInterface { + /** + * 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, - ): List = data.map { addGroundImage(it.state) } + ): List { + val handles = data.map { addGroundImage(it.state) } + longdoMap.awaitPageLayers(handles.mapNotNull { it?.layerId }) + return handles + } override suspend fun onChange( data: List>, diff --git a/src/main/java/com/mapconductor/longdo/marker/MarkerIdIndex.kt b/src/main/java/com/mapconductor/longdo/marker/MarkerIdIndex.kt new file mode 100644 index 0000000..65d7804 --- /dev/null +++ b/src/main/java/com/mapconductor/longdo/marker/MarkerIdIndex.kt @@ -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? = null + private var ids: Set = emptySet() + + @Synchronized + fun contains( + markers: List, + id: String, + ): Boolean { + if (markers !== list) { + ids = markers.mapTo(HashSet(markers.size * 2)) { it.id } + list = markers + } + return id in ids + } +} diff --git a/src/main/java/com/mapconductor/longdo/polygon/LongdoPolygonOverlayRenderer.kt b/src/main/java/com/mapconductor/longdo/polygon/LongdoPolygonOverlayRenderer.kt index 1f7b81d..db2305d 100644 --- a/src/main/java/com/mapconductor/longdo/polygon/LongdoPolygonOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/longdo/polygon/LongdoPolygonOverlayRenderer.kt @@ -10,6 +10,7 @@ import com.mapconductor.core.polygon.PolygonOverlayRendererInterface import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polygon.unionHoles import com.mapconductor.longdo.LongdoOverlayStacking +import com.mapconductor.longdo.awaitPageLayers import org.json.JSONObject /** @@ -23,9 +24,17 @@ import org.json.JSONObject class LongdoPolygonOverlayRenderer( private val longdoMap: LongdoMap, ) : PolygonOverlayRendererInterface { + /** + * Returns once the layers are on the page's map, so the polygons' + * `onAdded` comes after they are (see [com.mapconductor.longdo.PageLayerWait]). + */ override suspend fun onAdd( data: List, - ): List = data.map { addPolygon(it.state) } + ): List { + val handles = data.map { addPolygon(it.state) } + longdoMap.awaitPageLayers(handles.filterNotNull().flatMap { listOf(it.fillLayerId, it.lineLayerId) }) + return handles + } override suspend fun onChange( data: List>, diff --git a/src/main/java/com/mapconductor/longdo/polyline/LongdoPolylineController.kt b/src/main/java/com/mapconductor/longdo/polyline/LongdoPolylineController.kt index e503349..dff3eef 100644 --- a/src/main/java/com/mapconductor/longdo/polyline/LongdoPolylineController.kt +++ b/src/main/java/com/mapconductor/longdo/polyline/LongdoPolylineController.kt @@ -14,8 +14,8 @@ class LongdoPolylineController( override val renderer: LongdoPolylineOverlayRenderer, polylineManager: PolylineManagerInterface = PolylineManager(), ) : PolylineController(polylineManager, renderer) { - /** 既知のポリラインを再適用する(地図 `ready` 後の復元用)。 */ + /** 既知のポリラインを再適用する(地図 `ready` 後の復元用)。タイルで描く線(ハンドル無し)は飛ばす。 */ fun reapply() { - renderer.reapply(polylineManager.allEntities().map { it.polyline }) + renderer.reapply(polylineManager.allEntities().mapNotNull { it.polyline as LongdoPolylineHandle? }) } } diff --git a/src/main/java/com/mapconductor/longdo/polyline/LongdoPolylineOverlayRenderer.kt b/src/main/java/com/mapconductor/longdo/polyline/LongdoPolylineOverlayRenderer.kt index bce5e88..43a2e40 100644 --- a/src/main/java/com/mapconductor/longdo/polyline/LongdoPolylineOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/longdo/polyline/LongdoPolylineOverlayRenderer.kt @@ -9,6 +9,7 @@ import com.mapconductor.core.polyline.PolylineEntityInterface import com.mapconductor.core.polyline.PolylineOverlayRendererInterface import com.mapconductor.core.polyline.PolylineState import com.mapconductor.longdo.LongdoOverlayStacking +import com.mapconductor.longdo.awaitPageLayers import org.json.JSONObject /** @@ -21,9 +22,17 @@ import org.json.JSONObject class LongdoPolylineOverlayRenderer( private val longdoMap: LongdoMap, ) : PolylineOverlayRendererInterface { + /** + * Returns once the layers are on the page's map, so the polylines' + * `onAdded` comes after they are (see [com.mapconductor.longdo.PageLayerWait]). + */ override suspend fun onAdd( data: List, - ): List = data.map { addLine(it.state) } + ): List { + val handles = data.map { addLine(it.state) } + longdoMap.awaitPageLayers(handles.mapNotNull { it?.layerId }) + return handles + } override suspend fun onChange( data: List>, diff --git a/src/test/java/com/mapconductor/longdo/PageLayerWaitTest.kt b/src/test/java/com/mapconductor/longdo/PageLayerWaitTest.kt new file mode 100644 index 0000000..c8884ea --- /dev/null +++ b/src/test/java/com/mapconductor/longdo/PageLayerWaitTest.kt @@ -0,0 +1,109 @@ +package com.mapconductor.longdo + +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * [PageLayerWait] returns only once the page's map has every layer, which is + * what holds an overlay's `onAdded` until it is really on the map. + * + * The page is faked: [FakePage] answers the script the way the real one does, + * from a set of layer ids that tests add to between polls. + */ +class PageLayerWaitTest { + /** Answers [PageLayerWait.script] from [layers], counting the calls. */ + private class FakePage { + val layers = mutableSetOf() + val asked = mutableListOf>() + var onAsk: (Int) -> Unit = {} + + fun evaluate(js: String): String { + val ids = + Regex("var ids=\\[(.*?)\\];") + .find(js)!! + .groupValues[1] + .split(',') + .filter { it.isNotEmpty() } + .map { it.trim('"') } + asked += ids + onAsk(asked.size) + val present = ids.takeWhile { it in layers }.size + // evaluateJavascript hands a string value back JSON-quoted. + return "\"$present\"" + } + } + + @Test + fun returnsAtOnceForNoLayers() = + runBlocking { + var calls = 0 + assertTrue( + PageLayerWait.await(emptyList()) { + calls++ + "0" + }, + ) + assertEquals(0, calls) + } + + @Test + fun waitsUntilEveryLayerIsOnTheMap() = + runBlocking { + val page = FakePage() + // The layers arrive over three polls, as the SDK's queue empties. + page.onAsk = { n -> + if (n == 2) page.layers += "a" + if (n == 3) page.layers += listOf("b", "c") + } + val done = PageLayerWait.await(listOf("a", "b", "c"), intervalMs = 1) { page.evaluate(it) } + assertTrue(done) + assertEquals(listOf("a", "b", "c"), page.layers.sorted()) + // Asks again only for the layers still missing. + assertEquals(listOf(listOf("a", "b", "c"), listOf("a", "b", "c"), listOf("b", "c")), page.asked) + } + + @Test + fun givesUpAfterTheTimeout() = + runBlocking { + val page = FakePage() + page.layers += "a" + val done = PageLayerWait.await(listOf("a", "never"), timeoutMs = 100, intervalMs = 5) { page.evaluate(it) } + assertFalse(done) + assertTrue(page.asked.size > 1) + } + + @Test + fun aFailedAskCountsAsNothingYet() = + runBlocking { + var calls = 0 + val done = + PageLayerWait.await(listOf("a"), intervalMs = 1) { + calls++ + when (calls) { + 1 -> throw IllegalStateException("bridge not loaded") + 2 -> null + else -> "\"1\"" + } + } + assertTrue(done) + assertEquals(3, calls) + } + + @Test + fun parseReadsTheCountQuotedOrNot() { + assertEquals(3, PageLayerWait.parse("\"3\"")) + assertEquals(3, PageLayerWait.parse("3")) + assertEquals(0, PageLayerWait.parse(null)) + assertEquals(0, PageLayerWait.parse("null")) + } + + @Test + fun idsAreQuotedForTheScript() { + val js = PageLayerWait.script(listOf("polygon-fill-a\"b", "x\\y")) + assertTrue(js, js.contains("[\"polygon-fill-a\\\"b\",\"x\\\\y\"]")) + assertTrue(js.contains(PageLayerWait.MAP_EXPR)) + } +}