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
8 changes: 4 additions & 4 deletions api/android-sdk-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public final class com.mapconductor.compose.OverlayProviderKt {
}
public final class com.mapconductor.compose.circle.CircleComposeKt {
public static final void Circle(com.mapconductor.compose.MapViewScope, com.mapconductor.core.circle.CircleState, androidx.compose.runtime.Composer, int);
public static final void Circles(com.mapconductor.compose.MapViewScope, java.util.List<com.mapconductor.core.circle.CircleState>, androidx.compose.runtime.Composer, int);
public static final void Circles(com.mapconductor.compose.MapViewScope, java.util.List<com.mapconductor.core.circle.CircleState>, kotlin.jvm.functions.Function1<? super java.lang.Integer, kotlin.Unit>, kotlin.jvm.functions.Function2<? super java.lang.Integer, ? super java.lang.Integer, kotlin.Unit>, kotlin.jvm.functions.Function1<? super java.lang.Integer, kotlin.Unit>, androidx.compose.runtime.Composer, int, int);
public static final void CircleTiling(com.mapconductor.compose.MapViewScope, com.mapconductor.core.circle.CircleTilingOptions, androidx.compose.runtime.Composer, int);
public static final void Circle-6pqZqJ8(com.mapconductor.compose.MapViewScope, com.mapconductor.core.features.GeoPointInterface, double, java.lang.String, boolean, boolean, long, float, long, java.lang.Integer, java.io.Serializable, kotlin.jvm.functions.Function1<? super com.mapconductor.core.circle.CircleEvent, kotlin.Unit>, androidx.compose.runtime.Composer, int, int, int);
}
Expand Down Expand Up @@ -80,15 +80,15 @@ public final class com.mapconductor.compose.marker.MarkerAnimationLayerKt {
}
public final class com.mapconductor.compose.marker.MarkerComposeKt {
public static final void Marker(com.mapconductor.compose.MapViewScope, com.mapconductor.core.marker.MarkerState, androidx.compose.runtime.Composer, int);
public static final void Markers(com.mapconductor.compose.MapViewScope, java.util.List<com.mapconductor.core.marker.MarkerState>, androidx.compose.runtime.Composer, int);
public static final void Markers(com.mapconductor.compose.MapViewScope, java.util.List<com.mapconductor.core.marker.MarkerState>, kotlin.jvm.functions.Function1<? super java.lang.Integer, kotlin.Unit>, kotlin.jvm.functions.Function2<? super java.lang.Integer, ? super java.lang.Integer, kotlin.Unit>, kotlin.jvm.functions.Function1<? super java.lang.Integer, kotlin.Unit>, androidx.compose.runtime.Composer, int, int);
public static final void Marker(com.mapconductor.compose.MapViewScope, com.mapconductor.core.features.GeoPointInterface, java.lang.String, java.lang.Integer, boolean, boolean, com.mapconductor.core.marker.MarkerIconInterface, com.mapconductor.core.marker.MarkerAnimation, java.io.Serializable, kotlin.jvm.functions.Function1<? super com.mapconductor.core.marker.MarkerState, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.marker.MarkerState, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.marker.MarkerState, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.marker.MarkerState, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.marker.MarkerState, kotlin.Unit>, kotlin.jvm.functions.Function1<? super com.mapconductor.core.marker.MarkerState, kotlin.Unit>, androidx.compose.runtime.Composer, int, int, int);
}
public final class com.mapconductor.compose.polygon.LocalPolygonCollectorKt {
public static final androidx.compose.runtime.ProvidableCompositionLocal<com.mapconductor.core.OverlayCollectorInterface<com.mapconductor.core.polygon.PolygonState>> getLocalPolygonCollector();
}
public final class com.mapconductor.compose.polygon.PolygonComponentKt {
public static final void Polygon(com.mapconductor.compose.MapViewScope, com.mapconductor.core.polygon.PolygonState, androidx.compose.runtime.Composer, int);
public static final void Polygons(com.mapconductor.compose.MapViewScope, java.util.List<com.mapconductor.core.polygon.PolygonState>, androidx.compose.runtime.Composer, int);
public static final void Polygons(com.mapconductor.compose.MapViewScope, java.util.List<com.mapconductor.core.polygon.PolygonState>, kotlin.jvm.functions.Function1<? super java.lang.Integer, kotlin.Unit>, kotlin.jvm.functions.Function2<? super java.lang.Integer, ? super java.lang.Integer, kotlin.Unit>, kotlin.jvm.functions.Function1<? super java.lang.Integer, kotlin.Unit>, androidx.compose.runtime.Composer, int, int);
public static final void Polygon-YoVAMyA(com.mapconductor.compose.MapViewScope, java.util.List<? extends com.mapconductor.core.features.GeoPointInterface>, java.util.List<? extends java.util.List<? extends com.mapconductor.core.features.GeoPointInterface>>, java.lang.String, long, float, long, boolean, int, java.io.Serializable, kotlin.jvm.functions.Function1<? super com.mapconductor.core.polygon.PolygonEvent, kotlin.Unit>, androidx.compose.runtime.Composer, int, int, int);
public static final void Polygon-IvR-a-0(com.mapconductor.compose.MapViewScope, com.mapconductor.core.features.GeoRectBounds, java.lang.String, long, float, long, boolean, int, java.io.Serializable, kotlin.jvm.functions.Function1<? super com.mapconductor.core.polygon.PolygonEvent, kotlin.Unit>, androidx.compose.runtime.Composer, int, int);
}
Expand All @@ -97,7 +97,7 @@ public final class com.mapconductor.compose.polyline.LocalPolylineCollectorKt {
}
public final class com.mapconductor.compose.polyline.PolylineComponentKt {
public static final void Polyline(com.mapconductor.compose.MapViewScope, com.mapconductor.core.polyline.PolylineState, androidx.compose.runtime.Composer, int);
public static final void Polylines(com.mapconductor.compose.MapViewScope, java.util.List<com.mapconductor.core.polyline.PolylineState>, androidx.compose.runtime.Composer, int);
public static final void Polylines(com.mapconductor.compose.MapViewScope, java.util.List<com.mapconductor.core.polyline.PolylineState>, kotlin.jvm.functions.Function1<? super java.lang.Integer, kotlin.Unit>, kotlin.jvm.functions.Function2<? super java.lang.Integer, ? super java.lang.Integer, kotlin.Unit>, kotlin.jvm.functions.Function1<? super java.lang.Integer, kotlin.Unit>, androidx.compose.runtime.Composer, int, int);
public static final void PolylineTiling(com.mapconductor.compose.MapViewScope, com.mapconductor.core.polyline.PolylineTilingOptions, androidx.compose.runtime.Composer, int);
public static final void Polyline-bbrV0mI(com.mapconductor.compose.MapViewScope, java.util.List<? extends com.mapconductor.core.features.GeoPointInterface>, java.lang.String, long, float, boolean, int, java.io.Serializable, kotlin.jvm.functions.Function1<? super com.mapconductor.core.polyline.PolylineEvent, kotlin.Unit>, androidx.compose.runtime.Composer, int, int);
public static final void Polyline-bbrV0mI(com.mapconductor.compose.MapViewScope, com.mapconductor.core.features.GeoRectBounds, java.lang.String, long, float, boolean, int, java.io.Serializable, kotlin.jvm.functions.Function1<? super com.mapconductor.core.polyline.PolylineEvent, kotlin.Unit>, androidx.compose.runtime.Composer, int, int);
Expand Down
173 changes: 173 additions & 0 deletions src/main/java/com/mapconductor/compose/AddProgress.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package com.mapconductor.compose

import android.os.SystemClock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import com.mapconductor.core.ComponentState
import com.mapconductor.core.InternalMapConductorApi
import com.mapconductor.core.OverlayAddedListeners
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

/**
* How far a collection (`Markers`, `Circles`, `Polylines`, `Polygons`) is with
* putting its states on the map: for a list of thousands the screen shows
* nothing until they are drawn, and these say that work is going on.
*
* "On the map" is what each state's `onAdded` means: drawn natively, or, for a
* state drawn in tiles, its tiles handed to the map (their images may still be
* loading).
*
* - `onAddStart(total)`: a list brought states that are not on the map yet;
* `total` is how many.
* - `onAddProgress(added, total)`: how many of them are on the map so far, at
* most every 100 ms, and once more with `added == total` before the end.
* - `onAddEnd(total)`: all of them are on the map. A state taken out of the
* list before it got there no longer counts against the end.
*
* A list that brings new states while an earlier one is still being added
* starts over: a new `onAddStart` with only the new states, and no `onAddEnd`
* for the earlier list. A list that only updates or removes states calls
* nothing. All three run on the main thread.
*/
@InternalMapConductorApi
internal class AddProgressTracker(
private val scope: CoroutineScope,
private val now: () -> Long = SystemClock::uptimeMillis,
) {
var onAddStart: ((total: Int) -> Unit)? = null
var onAddProgress: ((added: Int, total: Int) -> Unit)? = null
var onAddEnd: ((total: Int) -> Unit)? = null

/** State objects handed over and not on the map yet, by id. */
private var pending = HashMap<String, ComponentState>()
private var total = 0
private var added = 0
private var lastProgressAt = Long.MIN_VALUE / 2
private var timer: Job? = null

/**
* The states of [states] whose id is not in [prevIds], or none when no one
* listens. Call it off the main thread; it walks the whole list.
*/
fun <T : ComponentState> newStates(
states: List<T>,
prevIds: Set<String>,
wanted: Boolean,
): List<T> = if (wanted) states.filter { it.id !in prevIds } else emptyList()

/**
* [states] (ids [nextIds]) is about to be handed to the collector and
* [newStates] are the ones it adds. Call on the main thread, before the
* hand-over, so that no arrival comes ahead of it.
*/
fun track(
states: List<ComponentState>,
nextIds: Set<String>,
newStates: List<ComponentState>,
) {
if (pending.isNotEmpty()) {
val before = pending.size
pending.keys.retainAll(nextIds)
total -= before - pending.size
// A state that is still coming may be handed over as a new object
// with the same id; the map reports that object.
if (pending.isNotEmpty() && newStates.isEmpty()) {
for (state in states) if (state.id in pending) pending[state.id] = state
}
if (newStates.isEmpty()) finishIfDone()
}
if (newStates.isEmpty()) return
timer?.cancel()
timer = null
pending = HashMap<String, ComponentState>(newStates.size * 2).apply { for (s in newStates) put(s.id, s) }
total = newStates.size
added = 0
lastProgressAt = Long.MIN_VALUE / 2
onAddStart?.invoke(total)
}

/** States reached the map (any collection's, any map's: only ours count). */
fun arrived(states: List<ComponentState>) {
if (pending.isEmpty()) return
var hit = false
for (state in states) {
if (pending[state.id] === state) {
pending.remove(state.id)
added++
hit = true
}
}
if (!hit) return
if (pending.isEmpty()) finishIfDone() else reportProgress()
}

fun dispose() {
timer?.cancel()
timer = null
pending = HashMap()
}

private fun finishIfDone() {
if (pending.isNotEmpty()) return
timer?.cancel()
timer = null
onAddProgress?.invoke(total, total)
onAddEnd?.invoke(total)
}

private fun reportProgress() {
if (timer != null) return
val wait = lastProgressAt + PROGRESS_INTERVAL_MS - now()
if (wait <= 0) {
emitProgress()
} else {
timer =
scope.launch {
delay(wait)
timer = null
emitProgress()
}
}
}

private fun emitProgress() {
if (pending.isEmpty()) return
lastProgressAt = now()
onAddProgress?.invoke(added, total)
}

@InternalMapConductorApi
private companion object {
const val PROGRESS_INTERVAL_MS = 100L
}
}

/** The tracker for one collection, listening only while one of the callbacks is set. */
@InternalMapConductorApi
@Composable
internal fun rememberAddProgress(
onAddStart: ((total: Int) -> Unit)?,
onAddProgress: ((added: Int, total: Int) -> Unit)?,
onAddEnd: ((total: Int) -> Unit)?,
): AddProgressTracker {
val scope = rememberCoroutineScope()
val tracker = remember { AddProgressTracker(scope) }
SideEffect {
tracker.onAddStart = onAddStart
tracker.onAddProgress = onAddProgress
tracker.onAddEnd = onAddEnd
}
val wanted = onAddStart != null || onAddProgress != null || onAddEnd != null
DisposableEffect(tracker, wanted) {
val remove = if (wanted) OverlayAddedListeners.add(tracker::arrived) else null
onDispose { remove?.invoke() }
}
DisposableEffect(tracker) { onDispose { tracker.dispose() } }
return tracker
}
31 changes: 27 additions & 4 deletions src/main/java/com/mapconductor/compose/circle/CircleCompose.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mapconductor.compose.circle

import com.mapconductor.compose.rememberAddProgress
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
Expand Down Expand Up @@ -42,17 +43,39 @@ fun MapViewScope.Circle(state: CircleState) {
* [com.mapconductor.compose.polygon.Polygons]: one effect diffs the list
* against the ids this call added last time and publishes the result in a
* single collector update. Several Circles()/Circle() can share one map.
*
* [onAddStart], [onAddProgress] and [onAddEnd] show that a long list is being
* put on the map. `onAddStart(total)` comes when the list brings states the
* map does not have yet, `onAddProgress(added, total)` at most every 100 ms,
* and `onAddEnd(total)` once all of them are on the map, at the same moment as
* their `onAdded` (for tiled states: their tiles were handed to the map). A
* state removed before it got there no longer counts. A list that adds states
* while an earlier one is still coming starts over without an `onAddEnd` for
* the earlier one; a list that only updates or removes calls nothing. They run
* on the main thread.
*/
@Composable
fun MapViewScope.Circles(states: List<CircleState>) {
fun MapViewScope.Circles(
states: List<CircleState>,
onAddStart: ((total: Int) -> Unit)? = null,
onAddProgress: ((added: Int, total: Int) -> Unit)? = null,
onAddEnd: ((total: Int) -> Unit)? = null,
) {
val collector = LocalCircleCollector.current
val prevIdsState = remember { mutableStateOf<Set<String>>(emptySet()) }
val progress = rememberAddProgress(onAddStart, onAddProgress, onAddEnd)
val tracking = onAddStart != null || onAddProgress != null || onAddEnd != null

LaunchedEffect(states) {
val prevIds = prevIdsState.value
val (nextIds, added) =
withContext(Dispatchers.Default) {
overlayStateIds(states) to progress.newStates(states, prevIds, tracking)
}
prevIdsState.value = nextIds
// Before the hand-over, so that no arrival comes ahead of it.
progress.track(states, nextIds, added)
withContext(Dispatchers.Default) {
val prevIds = prevIdsState.value
val nextIds = overlayStateIds(states)
prevIdsState.value = nextIds
// Merge only the ids this Circles() owns, so several
// Circles()/Circle() can share one map's collector.
collector.flow.update { current ->
Expand Down
34 changes: 29 additions & 5 deletions src/main/java/com/mapconductor/compose/marker/MarkerCompose.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mapconductor.compose.marker

import com.mapconductor.compose.rememberAddProgress
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
Expand Down Expand Up @@ -37,22 +38,45 @@ fun MapViewScope.Marker(state: MarkerState) {
*
* This avoids large composition overhead (thousands of LaunchedEffects/DisposableEffects)
* by performing batched add/remove in a single effect.
*
* [onAddStart], [onAddProgress] and [onAddEnd] show that a long list is being
* put on the map. `onAddStart(total)` comes when the list brings states the
* map does not have yet, `onAddProgress(added, total)` at most every 100 ms,
* and `onAddEnd(total)` once all of them are on the map, at the same moment as
* their `onAdded` (for tiled states: their tiles were handed to the map). A
* state removed before it got there no longer counts. A list that adds states
* while an earlier one is still coming starts over without an `onAddEnd` for
* the earlier one; a list that only updates or removes calls nothing. They run
* on the main thread.
*/
@Composable
fun MapViewScope.Markers(states: List<MarkerState>) {
fun MapViewScope.Markers(
states: List<MarkerState>,
onAddStart: ((total: Int) -> Unit)? = null,
onAddProgress: ((added: Int, total: Int) -> Unit)? = null,
onAddEnd: ((total: Int) -> Unit)? = null,
) {
val collector = LocalMarkerCollector.current
val prevIdsState = remember { mutableStateOf<Set<String>>(emptySet()) }
val progress = rememberAddProgress(onAddStart, onAddProgress, onAddEnd)
val tracking = onAddStart != null || onAddProgress != null || onAddEnd != null

LaunchedEffect(states) {
val startedAt = SystemClock.elapsedRealtime()
markerTrace("collector effect start count=${states.size}")
val prevIds = prevIdsState.value
val (nextIds, added) =
withContext(Dispatchers.Default) {
states.asSequence().map { it.id }.toSet() to progress.newStates(states, prevIds, tracking)
}
markerTrace("collector ids created count=${nextIds.size}")
prevIdsState.value = nextIds
// Before the hand-over, so that no arrival comes ahead of it.
progress.track(states, nextIds, added)
// For very large marker sets, avoid per-marker SharedFlow emits which can backpressure and
// block rendering; instead publish the whole map in one StateFlow update.
withContext(Dispatchers.Default) {
val nextIds = states.asSequence().map { it.id }.toSet()
val removedIds = prevIdsState.value - nextIds
markerTrace("collector ids created count=${nextIds.size}")
prevIdsState.value = nextIds
val removedIds = prevIds - nextIds
// Merge only the ids this Markers() owns into the shared collector so
// several Markers()/Marker() can share one map without clobbering each
// other. Assigning the whole map (a full replace) dropped everyone
Expand Down
Loading
Loading