diff --git a/docs/src/experimental/marker-native-strategy.md b/docs/src/experimental/marker-native-strategy.md index de68744a..325c90d9 100644 --- a/docs/src/experimental/marker-native-strategy.md +++ b/docs/src/experimental/marker-native-strategy.md @@ -94,20 +94,6 @@ val strategy = SimpleNativeParallelStrategy( ) ``` -#### NativeSpatialMarkerStrategy - -Advanced spatial rendering with clustering: - -```kotlin -import com.mapconductor.marker.nativestrategy.NativeSpatialMarkerStrategy - -val spatialStrategy = NativeSpatialMarkerStrategy( - clusteringEnabled = true, - clusterThreshold = 100, // Cluster when > 100 markers in area - geocell = HexGeocellImpl.defaultGeocell() -) -``` - ## Basic Usage ### Simple Native Manager diff --git a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/MultipleBubblesPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/MultipleBubblesPage.kt index d92f3880..1d856bdc 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/MultipleBubblesPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/MultipleBubblesPage.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -52,6 +53,11 @@ fun MultipleBubblesPage(onToggleSidebar: () -> Unit = {}) { } } + // Initially open the info bubble + LaunchedEffect(Unit) { + selectedMarkers = markerStates.map { it.id }.toSet() + } + DemoMapPageScaffold( menuItems = DefaultMapViewItems(initCameraPosition), onToggleSidebar = onToggleSidebar, diff --git a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/RichContentBubblePage.kt b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/RichContentBubblePage.kt index cfd23e28..db274e4e 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/RichContentBubblePage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/RichContentBubblePage.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -75,6 +76,10 @@ fun RichContentBubblePage(onToggleSidebar: () -> Unit = {}) { extra = locationInfo, ) + LaunchedEffect(Unit) { + selectedMarker = markerState + } + mapViewState?.let { MapViewContainer( modifier = Modifier.fillMaxSize(), @@ -89,8 +94,8 @@ fun RichContentBubblePage(onToggleSidebar: () -> Unit = {}) { info?.let { InfoBubble( marker = marker, - bubbleColor = Color.White, - borderColor = Color.Gray, + bubbleColor = if (isDarkTheme) Color.Black else Color.White, + borderColor = if (isDarkTheme) Color.Gray else Color.Black, contentPadding = 16.dp, cornerRadius = 12.dp, ) { @@ -108,7 +113,7 @@ fun RichContentBubblePage(onToggleSidebar: () -> Unit = {}) { Text( text = info.description, style = MaterialTheme.typography.bodyMedium, - color = Color.Gray, + color = if (isDarkTheme) Color.White else Color.Gray, ) Spacer(modifier = Modifier.height(8.dp)) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/SimpleTextBubblePage.kt b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/SimpleTextBubblePage.kt index 8140613a..63fb5b89 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/SimpleTextBubblePage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/SimpleTextBubblePage.kt @@ -61,6 +61,7 @@ fun SimpleTextBubblePage(onToggleSidebar: () -> Unit = {}) { InfoBubble(marker = marker) { Text( text = marker.extra as? String ?: "No information", + color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(4.dp), ) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/StyledInfoBubblePage.kt b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/StyledInfoBubblePage.kt index 68c800da..8a2e8351 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/StyledInfoBubblePage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/StyledInfoBubblePage.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -48,6 +49,10 @@ fun StyledInfoBubblePage(onToggleSidebar: () -> Unit = {}) { extra = "Point of Interest", ) + LaunchedEffect(Unit) { + selectedMarker = markerState + } + mapViewState?.let { MapViewContainer( modifier = Modifier.fillMaxSize(), diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficePage.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficePage.kt index dbb0abda..e333d159 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficePage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficePage.kt @@ -21,6 +21,7 @@ import com.mapconductor.here.HereActualMarker import com.mapconductor.mapbox.MapboxActualMarker import com.mapconductor.marker.nativestrategy.NativeParallelMarkerStrategy import com.mapconductor.marker.nativestrategy.NativeSpatialMarkerRenderingStrategy +import com.mapconductor.marker.nativestrategy.spatial.NativeRemoteSpatialMarkerStrategy @Composable fun PostOfficeMapPage( @@ -31,8 +32,7 @@ fun PostOfficeMapPage( val dataLoader = remember { PostOfficeDataLoader(context) } val strategies = remember { - val google = NativeParallelMarkerStrategy() -// val google = NativeSpatialMarkerStrategy() + val google = NativeRemoteSpatialMarkerStrategy(context) // val google = NativeParallelMarkerStrategy() val mapbox = NativeParallelMarkerStrategy() val here = NativeParallelMarkerStrategy() diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index a37a8049..30377262 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch open class MapViewScope { - val overflowScope = CoroutineScope(Dispatchers.IO) val markerAddSharedFlow = MutableSharedFlow(1000) val markerFlow = MutableStateFlow>(mutableMapOf()) val bubbleFlow = MutableStateFlow>(mutableMapOf()) @@ -67,42 +66,6 @@ fun CollectAndRenderOverlays( registry.getAll().forEach { overlay -> @Suppress("UNCHECKED_CAST") val typedOverlay = overlay as MapOverlay - -// LaunchedEffect(Unit) { -// typedOverlay.flow -// .debounce(100) // Debounce updates for 100ms to prevent excessive rendering -// .collect { items -> -// // typedOverlay.render(items, controller) -// -// if (items.isNotEmpty()) { -// return@collect -// } -// -// // Process items in chunks to prevent main thread blocking -// val chunks = items.values.chunked(500) // Process 50 items at a time -// -// chunks.forEach { chunk -> -// val chunkMap = -// chunk -// .associateBy { -// when (it) { -// is MarkerState -> it.id -// is CircleState -> it.id -// is PolylineState -> it.id -// is PolygonState -> it.id -// is GroundImageState -> it.id -// else -> it.toString() -// } -// }.toMutableMap() -// -// typedOverlay.render(chunkMap, controller) -// -// // Yield to allow other coroutines and UI updates -// yield() -// } -// } -// } - val flowState = typedOverlay.flow.collectAsState() LaunchedEffect(flowState.value) { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt index 09c1f952..7b399e95 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt @@ -11,17 +11,6 @@ fun MapViewScope.Marker(state: MarkerState) { LaunchedEffect(state) { markerAddSharedFlow.emit(state) } -// DisposableEffect(Unit) { -// overflowScope.launch { -// markerAddSharedFlow.emit(state) -// } -// -// onDispose { -// val newMap = bubbleFlow.value.toMutableMap() -// newMap.remove(state.id) -// bubbleFlow.value = newMap -// } -// } } @Composable diff --git a/mapconductor-marker-native-strategy/src/main/java/com/mapconductor/marker/nativestrategy/NativeSpatialMarkerStrategy.kt b/mapconductor-marker-native-strategy/src/main/java/com/mapconductor/marker/nativestrategy/NativeSpatialMarkerStrategy.kt deleted file mode 100644 index 60d925a0..00000000 --- a/mapconductor-marker-native-strategy/src/main/java/com/mapconductor/marker/nativestrategy/NativeSpatialMarkerStrategy.kt +++ /dev/null @@ -1,187 +0,0 @@ -package com.mapconductor.marker.nativestrategy - -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.map.MapCameraPositionImpl -import com.mapconductor.core.marker.BitmapIcon -import com.mapconductor.core.marker.MarkerEntity -import com.mapconductor.core.marker.MarkerOverlayRenderer -import com.mapconductor.core.spherical.expandBounds -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit - -/** - * Advanced marker rendering strategy that leverages spatial indexing for optimal performance. - * - * This strategy uses the native spatial index (NativeMarkerIndex) to efficiently find markers - * within viewport bounds instead of iterating through all markers. This provides significant - * performance improvements, especially for large marker datasets (1000+ markers). - * - * Key optimizations: - * - Uses O(log n + k) spatial queries instead of O(n) full iteration - * - Leverages existing hex-based spatial index infrastructure - * - Reduces memory allocation and GC pressure - * - Supports both add/remove and add-only rendering modes - * - * Performance characteristics: - * - Small datasets (100-500 markers): 3-5x faster than default strategies - * - Medium datasets (1K-5K markers): 8-15x faster - * - Large datasets (10K+ markers): 15-50x faster - * - * @param expandMargin The margin for expanding viewport bounds (default 0.3 = 30% expansion) - * @param addOnlyMode If true, markers are never removed once rendered (like NativeAddOnlyMarkerStrategy) - * @param semaphore Semaphore for synchronizing rendering operations - */ -class NativeSpatialMarkerStrategy( - semaphore: Semaphore = Semaphore(1), - private val expandMargin: Double = 0.3, - private val addOnlyMode: Boolean = false, - geocell: HexGeocell = NativeHexGeocellImpl.defaultGeocell(), -) : NativeAbstractViewportStrategy(semaphore, geocell) { - override suspend fun onCameraChanged( - cameraPosition: MapCameraPositionImpl, - renderer: MarkerOverlayRenderer, - ) { - val visibleRegion = cameraPosition.visibleRegion ?: return - semaphore.withPermit { - // Expand bounds for better performance and smoother experience - val expandedBounds = expandBounds(visibleRegion.bounds, expandMargin) - - // Use native spatial query from the provided manager (keep consistent with onAdd/onUpdate) - val markersInBounds = markerManager.findMarkersInBounds(expandedBounds) - val markerIdsInBounds = markersInBounds.map { it.state.id } - val markersToRender = mutableListOf>() - val markersToRemove = mutableListOf>() - - // Fallback to iterating all entities if native spatial query fails (returns empty when it should return results) - val allEntities = markerManager.allEntities() - if (markersInBounds.isEmpty() && allEntities.isNotEmpty()) { - // Native spatial query likely failed, use fallback approach like SpatialMarkerStrategy - allEntities.forEach { entity -> - val isInViewport = expandedBounds.contains(entity.state.position) - - if (isInViewport && !entity.isRendered) { - markersToRender.add(entity) - entity.visible = true - } else if (!isInViewport && entity.isRendered && !addOnlyMode) { - markersToRemove.add(entity) - entity.visible = false - } else if (isInViewport) { - entity.visible = true - } else { - entity.visible = false - } - } - } else { - // Native spatial query worked, use the optimized path - markerIdsInBounds.forEach { markerId -> - markerManager.getEntity(markerId)?.let { entity -> - if (!entity.isRendered) { - markersToRender.add(entity) - entity.visible = true - } else { - entity.visible = true - } - } - } - - // Handle markers that left viewport (only in add/remove mode) - if (!addOnlyMode) { - allEntities.forEach { entity -> - if (entity.isRendered && !markerIdsInBounds.contains(entity.state.id)) { - markersToRemove.add(entity) - entity.visible = false - } - } - } - } - - // Remove markers that left the viewport - if (markersToRemove.isNotEmpty()) { - renderer.onRemove(markersToRemove) - markersToRemove.forEach { entity -> - entity.isRendered = false - entity.marker = null - } - } - - // Add markers that entered the viewport - if (markersToRender.isNotEmpty()) { - val addParams = - markersToRender.map { entity -> - object : MarkerOverlayRenderer.AddParams { - override val state = entity.state - override val bitmapIcon: BitmapIcon = - entity.state.icon?.toBitmapIcon() ?: defaultIcon - } - } - - val actualMarkers = renderer.onAdd(addParams) - actualMarkers.forEachIndexed { index, actualMarker -> - actualMarker?.let { - markersToRender[index].marker = it - markersToRender[index].isRendered = true - } - } - } - - if (markersToRender.isNotEmpty() || markersToRemove.isNotEmpty()) { - renderer.onPostProcess() - } - } - } -} - -/** - * Factory methods for creating commonly used spatial rendering strategies. - */ -object NativeSpatialMarkerRenderingStrategies { - /** - * Creates a spatial rendering strategy with add/remove mode. - * Optimized for map providers that handle marker add/remove operations efficiently. - * Uses moderate viewport expansion for balanced performance. - */ - fun withAddRemoveMode( - semaphore: Semaphore, - geocell: HexGeocell, - expandMargin: Double = 0.2, - ): NativeSpatialMarkerStrategy = - NativeSpatialMarkerStrategy( - expandMargin = expandMargin, - addOnlyMode = false, // Support add/remove for optimal memory usage - semaphore = semaphore, - geocell = geocell, - ) - - /** - * Creates a spatial rendering strategy with add-only mode. - * Optimized for map providers where marker removal operations are expensive. - * Uses larger viewport expansion for smoother experience. - */ - fun withAddOnlyMode( - semaphore: Semaphore, - geocell: HexGeocell, - expandMargin: Double = 0.5, - ): NativeSpatialMarkerStrategy = - NativeSpatialMarkerStrategy( - expandMargin = expandMargin, - addOnlyMode = true, // Add-only to avoid expensive remove operations - semaphore = semaphore, - geocell = geocell, - ) - - /** - * Creates a high-performance spatial rendering strategy for very large marker datasets. - * Uses aggressive viewport expansion and add-only mode for maximum performance. - */ - fun forLargeDatasets( - semaphore: Semaphore, - geocell: HexGeocell, - expandMargin: Double = 0.8, - ): NativeSpatialMarkerStrategy = - NativeSpatialMarkerStrategy( - expandMargin = expandMargin, - addOnlyMode = true, // Maximize performance for large datasets - semaphore = semaphore, - geocell = geocell, - ) -}