Marker Strategy (Experimental)
The mapconductor-marker-strategy module provides advanced marker rendering strategies for optimizing performance and user experience with large datasets. This experimental module offers multiple rendering approaches tailored to different use cases and performance requirements.
⚠️ Experimental Module: This module is experimental and APIs may change. Use in production with caution.
Overview
Section titled “Overview”The marker strategy module provides sophisticated rendering strategies that go beyond basic marker display:
- Viewport-Based Rendering: Only render markers visible in the current viewport
- Dynamic Add/Remove: Efficiently manage markers as the viewport changes
- Spatial Optimization: Advanced spatial indexing for large datasets
- Remote Data Integration: Support for server-side marker data
- Clustering Support: Group nearby markers for better performance
Installation
Section titled “Installation”Add the marker strategy module to your build.gradle:
dependencies { implementation "com.mapconductor:marker-strategy"
// Required: Core module implementation "com.mapconductor:mapconductor-bom:$version" // Required: Core module implementation "com.mapconductor:core"
// Choose your map provider implementation "com.mapconductor:for-googlemaps"}Provider Integration
Section titled “Provider Integration”mapconductor-marker-strategy renders markers through a provider-specific implementation registered by the provider module.
Built-in provider modules register this capability automatically via MapServiceRegistry (see Map Service Registry).
Core Strategies
Section titled “Core Strategies”DefaultMarkerStrategy
Section titled “DefaultMarkerStrategy”Optimal for Google Maps and ArcGIS providers that handle add/remove operations efficiently:
import com.mapconductor.marker.strategy.DefaultMarkerStrategy
val defaultStrategy = DefaultMarkerStrategy<GoogleMapActualMarker>( expandMargin = 0.2, // 20% viewport expansion semaphore = Semaphore(1), geocell = HexGeocell.defaultGeocell())Key Features
Section titled “Key Features”- Dynamic Add/Remove: Adds markers entering viewport, removes markers leaving viewport
- Viewport Expansion: Preloads markers slightly outside visible area
- Memory Efficient: Only keeps visible markers in memory
- Smooth Scrolling: Reduces pop-in/pop-out during map movement
SimpleMarkerStrategy
Section titled “SimpleMarkerStrategy”Lightweight strategy for smaller datasets or providers with different performance characteristics:
import com.mapconductor.marker.strategy.SimpleMarkerStrategy
val simpleStrategy = SimpleMarkerStrategy<MapboxActualMarker>( expandMargin = 0.15, geocell = HexGeocell.defaultGeocell())Key Features
Section titled “Key Features”- Simplified Logic: Less complex viewport management
- Lower Overhead: Minimal computational overhead
- Good for Mapbox: Optimized for providers that prefer simpler marker management
SpatialMarkerStrategy
Section titled “SpatialMarkerStrategy”Advanced strategy with spatial clustering and optimization:
import com.mapconductor.marker.strategy.SpatialMarkerStrategy
val spatialStrategy = SpatialMarkerStrategy<HereActualMarker>( clusteringEnabled = true, clusterRadius = 100, // Clustering radius (meters) maxMarkersPerCluster = 10, // Maximum markers in a cluster geocell = HexGeocell.defaultGeocell())Key Features
Section titled “Key Features”- Spatial Clustering: Groups nearby markers into clusters
- Density Management: Reduces visual clutter in dense areas
- Performance Scaling: Handles very large datasets efficiently
- Customizable Clustering: Configurable clustering parameters
Basic Usage
Section titled “Basic Usage”Setting Up Default Strategy
Section titled “Setting Up Default Strategy”@Composablefun DefaultStrategyExample() { val mapViewState = rememberGoogleMapViewState()
val markerStrategy = remember { DefaultMarkerStrategy<GoogleMapActualMarker>( expandMargin = 0.2 ) }
// Configure strategy with map controller LaunchedEffect(mapViewState) { // Strategy setup depends on map provider implementation // This is typically handled by the map provider's marker controller }
// Replace MapView with your chosen map provider, such as GoogleMapView, MapboxMapView GoogleMapView(state = mapViewState) { // Markers are managed by the strategy // Add markers programmatically through the strategy }}Adding Markers to Strategy
Section titled “Adding Markers to Strategy”@Composablefun StrategyMarkerManagement() { val markerStrategy = remember { DefaultMarkerStrategy<GoogleMapActualMarker>() }
LaunchedEffect(Unit) { // Add markers to the strategy's manager val markers = loadMarkerData() // Your marker data
markers.forEach { markerData -> val entity = MarkerEntityInterface( state = MarkerState( id = markerData.id, position = markerData.position, icon = DefaultIcon(fillColor = markerData.color) ) )
// Register with strategy's marker manager markerStrategy.markerManager.registerEntity(entity) } }
// Replace MapView with your chosen map provider, such as GoogleMapView, MapboxMapView GoogleMapView(state = mapViewState) { // Strategy handles marker rendering automatically }}Advanced Usage
Section titled “Advanced Usage”Dynamic Marker Loading
Section titled “Dynamic Marker Loading”@Composablefun DynamicLoadingExample() { val mapViewState = rememberGoogleMapViewState() var currentBounds by remember { mutableStateOf<GeoRectBounds?>(null) } var loadedMarkers by remember { mutableStateOf<Set<String>>(emptySet()) }
val strategy = remember { DefaultMarkerStrategy<GoogleMapActualMarker>( expandMargin = 0.3 // Larger margin for preloading ) }
// Load markers dynamically based on viewport LaunchedEffect(currentBounds) { currentBounds?.let { bounds -> val newMarkers = fetchMarkersForBounds(bounds) // Your API call
newMarkers.forEach { markerData -> if (markerData.id !in loadedMarkers) { val entity = MarkerEntityInterface( state = MarkerState( id = markerData.id, position = markerData.position, icon = DefaultIcon() ) )
strategy.markerManager.registerEntity(entity) loadedMarkers = loadedMarkers + markerData.id } } } }
// Replace MapView with your chosen map provider, such as GoogleMapView, MapboxMapView GoogleMapView( state = mapViewState, onCameraMove = { cameraPosition -> currentBounds = cameraPosition.visibleRegion?.bounds } ) { // Strategy manages dynamic marker loading }}Clustering Strategy
Section titled “Clustering Strategy”@Composablefun ClusteringStrategyExample() { val clusterStrategy = remember { SpatialMarkerStrategy<GoogleMapActualMarker>( clusteringEnabled = true, clusterRadius = 50.0, // 50-meter clustering maxMarkersPerCluster = 5, // Small clusters geocell = HexGeocell( baseHexSideLength = 100.0, // Fine-grained spatial index zoom = 18.0 ) ) }
// Add dense marker dataset LaunchedEffect(Unit) { val denseMarkers = generateDenseMarkerSet( center = GeoPoint.fromLatLong(37.7749, -122.4194), count = 500, radiusMeters = 200.0 // 200-meter radius )
denseMarkers.forEach { markerData -> val entity = MarkerEntityInterface( state = MarkerState( id = markerData.id, position = markerData.position, icon = DefaultIcon( fillColor = markerData.category.color, scale = 0.8f ) ) )
clusterStrategy.markerManager.registerEntity(entity) } }
// Replace MapView with your chosen map provider, such as GoogleMapView, MapboxMapView GoogleMapView(state = mapViewState) { // Clustering strategy automatically groups nearby markers }}Remote Spatial Strategy
Section titled “Remote Spatial Strategy”// Inline in English docs for now (implementation specific)
Strategy Comparison
Section titled “Strategy Comparison”Performance Characteristics
Section titled “Performance Characteristics”- DefaultMarkerStrategy
- Best for: Google Maps, ArcGIS
- Memory usage: Medium
- Network: None
- Complexity: Medium
- SimpleMarkerStrategy
- Best for: Mapbox, HERE
- Memory usage: Low
- Network: None
- Complexity: Low
- SpatialMarkerStrategy
- Best for: Large datasets
- Memory usage: High
- Network: None
- Complexity: High
- RemoteSpatialMarkerStrategy
- Best for: Server-side data
- Memory usage: Low
- Network: High
- Complexity: High
Use Case Guidelines
Section titled “Use Case Guidelines”Choose DefaultMarkerStrategy when:
Section titled “Choose DefaultMarkerStrategy when:”- Using Google Maps or ArcGIS
- Have moderate marker counts (1,000-50,000)
- Want smooth viewport-based rendering
- Markers are loaded locally
Choose SimpleMarkerStrategy when:
Section titled “Choose SimpleMarkerStrategy when:”- Using Mapbox or HERE Maps
- Have smaller marker counts (<10,000)
- Want minimal overhead
- Simple rendering requirements
Choose SpatialMarkerStrategy when:
Section titled “Choose SpatialMarkerStrategy when:”- Have very large marker datasets (50,000+)
- Need clustering functionality
- Want advanced spatial optimization
- Can afford higher memory usage
Choose RemoteSpatialMarkerStrategy when:
Section titled “Choose RemoteSpatialMarkerStrategy when:”- Markers are stored server-side
- Want on-demand loading
- Have network connectivity
- Need to minimize app memory usage
Custom Strategy Development
Section titled “Custom Strategy Development”Extending AbstractViewportStrategy
Section titled “Extending AbstractViewportStrategy”class CustomMarkerStrategy<ActualMarker>( semaphore: Semaphore = Semaphore(1), geocell: HexGeocellInterface = HexGeocell.defaultGeocell()) : AbstractViewportStrategy<ActualMarker>(semaphore, geocell) {
override suspend fun onCameraChanged( cameraPosition: MapCameraPosition, renderer: MarkerOverlayRendererInterface<ActualMarker> ) { semaphore.withPermit { // Custom rendering logic val visibleBounds = cameraPosition.visibleRegion?.bounds ?: return
// Your custom marker management here val markersToShow = determineVisibleMarkers(visibleBounds) val markersToHide = determineHiddenMarkers(visibleBounds)
// Use renderer to update display if (markersToHide.isNotEmpty()) { renderer.onRemove(markersToHide) }
if (markersToShow.isNotEmpty()) { val addParams = markersToShow.map { entity -> object : MarkerOverlayRendererInterface.AddParamsInterface { override val state: MarkerState = entity.state override val bitmapIcon: BitmapIcon = entity.state.icon?.toBitmapIcon() ?: defaultIcon } } renderer.onAdd(addParams) }
renderer.onPostProcess() } }
private fun determineVisibleMarkers(bounds: GeoRectBounds): List<MarkerEntityInterface<ActualMarker>> { // Your custom logic to determine which markers should be visible return markerManager.findMarkersInBounds(bounds) }
private fun determineHiddenMarkers(bounds: GeoRectBounds): List<MarkerEntityInterface<ActualMarker>> { // Your custom logic to determine which markers should be hidden return markerManager.allEntities().filter { entity -> entity.isRendered && !bounds.contains(entity.state.position) } }}Performance Optimization
Section titled “Performance Optimization”Strategy Configuration
Section titled “Strategy Configuration”// High-performance configurationval performanceStrategy = DefaultMarkerStrategy<ActualMarker>( expandMargin = 0.1, // Smaller margin for less preloading semaphore = Semaphore(2), // Allow some parallelism geocell = HexGeocell( baseHexSideLength = 1000.0, // Larger cells for better performance zoom = 15.0 // Lower resolution for speed ))
// Memory-optimized configurationval memoryStrategy = SimpleMarkerStrategy<ActualMarker>( expandMargin = 0.05, // Minimal expansion geocell = HexGeocell( baseHexSideLength = 2000.0, // Very large cells zoom = 12.0 // Lower resolution ))Monitoring Performance
Section titled “Monitoring Performance”@Composablefun StrategyPerformanceMonitoring() { val strategy = remember { DefaultMarkerStrategy<GoogleMapActualMarker>() } var performanceStats by remember { mutableStateOf<String>("") }
LaunchedEffect(Unit) { while (true) { delay(5000)
val stats = strategy.markerManager.getMemoryStats() performanceStats = buildString { appendLine("Entities: ${stats.entityCount}") appendLine("Memory: ${stats.estimatedMemoryKB} KB") appendLine("Spatial Index: ${stats.hasSpatialIndex}") } } }
Column { Text(performanceStats)
// Replace MapView with your chosen map provider, such as GoogleMapView, MapboxMapView GoogleMapView(state = mapViewState) { // Strategy-managed markers } }}Best Practices
Section titled “Best Practices”- Strategy Selection: Choose strategy based on your specific use case and map provider
- Viewport Margins: Balance preloading (larger margin) vs performance (smaller margin)
- Spatial Configuration: Tune geocell parameters for your data density
- Memory Monitoring: Monitor memory usage in production, especially with large datasets
- Testing: Test with realistic data volumes and usage patterns
- Fallback: Have simpler strategy as fallback for performance issues
Common Pitfalls
Section titled “Common Pitfalls”- Over-Engineering: Don’t use complex strategies for simple marker scenarios
- Memory Leaks: Ensure proper cleanup of strategy resources
- Provider Mismatch: Using wrong strategy for your map provider can hurt performance
- Excessive Preloading: Large expand margins can cause memory pressure
- Thread Safety: Strategies handle concurrency, but be careful with external modifications
Migration Guide
Section titled “Migration Guide”From Basic Marker Management
Section titled “From Basic Marker Management”// Before: Basic marker management@Composablefun BasicMarkers() { MapView(state = mapViewState) { markers.forEach { markerData -> Marker( position = markerData.position, icon = DefaultIcon() ) } }}
// After: Strategy-based management@Composablefun StrategyMarkers() { val strategy = remember { DefaultMarkerStrategy<ActualMarker>() }
LaunchedEffect(markers) { markers.forEach { markerData -> val entity = MarkerEntityInterface( state = MarkerState( id = markerData.id, position = markerData.position, icon = DefaultIcon() ) ) strategy.markerManager.registerEntity(entity) } }
// Replace MapView with your chosen map provider, such as GoogleMapView, MapboxMapView MapView(state = mapViewState) { // Strategy handles all marker rendering }}The marker strategy module provides sophisticated marker management capabilities for applications requiring high performance with large datasets or complex rendering requirements.