Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/util/AnnotationPlacer.kt +135 −2 Original line number Diff line number Diff line package earth.maps.cardinal.ui.util import android.util.Log import earth.maps.cardinal.data.LatLng import uniffi.ferrostar.GeographicCoordinate import uniffi.ferrostar.Route import javax.inject.Inject import kotlin.math.abs /** * Utility class for determining optimal placement of route duration annotations on a map. * * This class solves the problem of placing annotations (such as route duration text) on polylines * in a way that users can clearly identify which annotation applies to which route. The primary * challenge is avoiding placement on route segments where multiple routes overlap, as this would * create visual ambiguity. * * The algorithm works by: * 1. Analyzing all routes to identify overlapping segments * 2. Finding divergent segments (where only one route exists) for each route * 3. Selecting the longest divergent segment for optimal visibility * 4. Placing the annotation at the midpoint of that segment * * @constructor Creates an instance of AnnotationPlacer using dependency injection */ class AnnotationPlacer @Inject constructor() { fun placeAnnotations(routes: List<List<LatLng>>): List<LatLng> { return emptyList() /** * Determines optimal annotation placement points for a collection of routes. * * This method analyzes the geometry of all provided routes to find non-overlapping * segments where annotations can be placed without visual ambiguity. For each route, * it identifies the longest segment that doesn't overlap with any other route and * places the annotation at the midpoint of that segment. * * The algorithm follows these steps: * 1. Count overlapping points across all routes * 2. For each route, identify contiguous segments where overlap count == 1 (diverged) * 3. Track cumulative distance along each divergent segment * 4. Select the longest divergent segment for each route * 5. Place annotation at the midpoint of the selected segment * * @param routes A list of Route objects containing geographic coordinate data * @return A Map where each Route is mapped to its optimal annotation placement LatLng coordinate. * Routes with no divergent segments will be excluded from the result. * * @see Route for the route data structure * @see LatLng for the coordinate representation * * Example usage: * ```kotlin * val annotationPlacer = AnnotationPlacer() * val routes = listOf(route1, route2, route3) * val placements = annotationPlacer.placeAnnotations(routes) * * placements.forEach { (route, position) -> * // Place annotation at 'position' for 'route' * } * ``` */ fun placeAnnotations(routes: List<Route>): Map<Route, LatLng> { val routeOverlapCount: MutableMap<LatLng, Int> = mutableMapOf() val routesLatLng = routes.map { route -> route.geometry.map { LatLng(it.lat, it.lng) } } val placements: MutableMap<Route, LatLng> = mutableMapOf() // Step 1: Count how many routes pass through each point for (route in routesLatLng) { for (point in route) { routeOverlapCount.merge(point, 1, Int::plus) } } // Step 2: For each route, find divergent segments and place annotations routesLatLng.forEachIndexed { index, route -> val segments: MutableList<List<Pair<LatLng, Double>>> = mutableListOf() var currentSegment: MutableList<Pair<LatLng, Double>>? = null var currentSegmentLengthMeters: Double? = null // Step 2a: Identify contiguous divergent segments for (point in route) { val isDiverged = routeOverlapCount[point] == 1 if (isDiverged) { if (currentSegment != null) { // Continue current segment currentSegmentLengthMeters = currentSegmentLengthMeters?.plus( currentSegment.lastOrNull()?.first?.fastDistanceTo(point) ?: 0.0 ) currentSegment.add(Pair(point, currentSegmentLengthMeters ?: 0.0)) } else { // Start new segment currentSegment = mutableListOf(Pair(point, 0.0)) currentSegmentLengthMeters = 0.0 } } else { // End current segment if we hit an overlapping point if (currentSegment != null) { segments.add(currentSegment) currentSegment = null currentSegmentLengthMeters = null } } } // Add the final segment if the route ends while diverged if (currentSegment != null) { segments.add(currentSegment) } // Step 2b: Select the longest divergent segment val bestSegment = segments.sortedByDescending { segment -> segment.lastOrNull()?.second } .firstOrNull() if (bestSegment == null) { Log.w(TAG, "Route with zero divergent segments found, skipping") return@forEachIndexed } val bestSegmentLength = bestSegment.lastOrNull()?.second if (bestSegmentLength == null) { Log.w(TAG, "Empty divergent segment found, skipping. This is a logic error.") return@forEachIndexed } // Step 2c: Find the midpoint of the selected segment val segmentMidpoint = bestSegment.minByOrNull { abs(it.second - bestSegmentLength / 2.0) } if (segmentMidpoint == null) { Log.w(TAG, "Empty divergent segment found after sorting, skipping. This is a logic error.") return@forEachIndexed } // Step 2d: Store the placement for this route placements.put(routes[index], segmentMidpoint.first) } return placements } companion object { /** Tag used for logging debug and warning messages */ const val TAG = "AnnotationPlacer" } } Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/util/AnnotationPlacer.kt +135 −2 Original line number Diff line number Diff line package earth.maps.cardinal.ui.util import android.util.Log import earth.maps.cardinal.data.LatLng import uniffi.ferrostar.GeographicCoordinate import uniffi.ferrostar.Route import javax.inject.Inject import kotlin.math.abs /** * Utility class for determining optimal placement of route duration annotations on a map. * * This class solves the problem of placing annotations (such as route duration text) on polylines * in a way that users can clearly identify which annotation applies to which route. The primary * challenge is avoiding placement on route segments where multiple routes overlap, as this would * create visual ambiguity. * * The algorithm works by: * 1. Analyzing all routes to identify overlapping segments * 2. Finding divergent segments (where only one route exists) for each route * 3. Selecting the longest divergent segment for optimal visibility * 4. Placing the annotation at the midpoint of that segment * * @constructor Creates an instance of AnnotationPlacer using dependency injection */ class AnnotationPlacer @Inject constructor() { fun placeAnnotations(routes: List<List<LatLng>>): List<LatLng> { return emptyList() /** * Determines optimal annotation placement points for a collection of routes. * * This method analyzes the geometry of all provided routes to find non-overlapping * segments where annotations can be placed without visual ambiguity. For each route, * it identifies the longest segment that doesn't overlap with any other route and * places the annotation at the midpoint of that segment. * * The algorithm follows these steps: * 1. Count overlapping points across all routes * 2. For each route, identify contiguous segments where overlap count == 1 (diverged) * 3. Track cumulative distance along each divergent segment * 4. Select the longest divergent segment for each route * 5. Place annotation at the midpoint of the selected segment * * @param routes A list of Route objects containing geographic coordinate data * @return A Map where each Route is mapped to its optimal annotation placement LatLng coordinate. * Routes with no divergent segments will be excluded from the result. * * @see Route for the route data structure * @see LatLng for the coordinate representation * * Example usage: * ```kotlin * val annotationPlacer = AnnotationPlacer() * val routes = listOf(route1, route2, route3) * val placements = annotationPlacer.placeAnnotations(routes) * * placements.forEach { (route, position) -> * // Place annotation at 'position' for 'route' * } * ``` */ fun placeAnnotations(routes: List<Route>): Map<Route, LatLng> { val routeOverlapCount: MutableMap<LatLng, Int> = mutableMapOf() val routesLatLng = routes.map { route -> route.geometry.map { LatLng(it.lat, it.lng) } } val placements: MutableMap<Route, LatLng> = mutableMapOf() // Step 1: Count how many routes pass through each point for (route in routesLatLng) { for (point in route) { routeOverlapCount.merge(point, 1, Int::plus) } } // Step 2: For each route, find divergent segments and place annotations routesLatLng.forEachIndexed { index, route -> val segments: MutableList<List<Pair<LatLng, Double>>> = mutableListOf() var currentSegment: MutableList<Pair<LatLng, Double>>? = null var currentSegmentLengthMeters: Double? = null // Step 2a: Identify contiguous divergent segments for (point in route) { val isDiverged = routeOverlapCount[point] == 1 if (isDiverged) { if (currentSegment != null) { // Continue current segment currentSegmentLengthMeters = currentSegmentLengthMeters?.plus( currentSegment.lastOrNull()?.first?.fastDistanceTo(point) ?: 0.0 ) currentSegment.add(Pair(point, currentSegmentLengthMeters ?: 0.0)) } else { // Start new segment currentSegment = mutableListOf(Pair(point, 0.0)) currentSegmentLengthMeters = 0.0 } } else { // End current segment if we hit an overlapping point if (currentSegment != null) { segments.add(currentSegment) currentSegment = null currentSegmentLengthMeters = null } } } // Add the final segment if the route ends while diverged if (currentSegment != null) { segments.add(currentSegment) } // Step 2b: Select the longest divergent segment val bestSegment = segments.sortedByDescending { segment -> segment.lastOrNull()?.second } .firstOrNull() if (bestSegment == null) { Log.w(TAG, "Route with zero divergent segments found, skipping") return@forEachIndexed } val bestSegmentLength = bestSegment.lastOrNull()?.second if (bestSegmentLength == null) { Log.w(TAG, "Empty divergent segment found, skipping. This is a logic error.") return@forEachIndexed } // Step 2c: Find the midpoint of the selected segment val segmentMidpoint = bestSegment.minByOrNull { abs(it.second - bestSegmentLength / 2.0) } if (segmentMidpoint == null) { Log.w(TAG, "Empty divergent segment found after sorting, skipping. This is a logic error.") return@forEachIndexed } // Step 2d: Store the placement for this route placements.put(routes[index], segmentMidpoint.first) } return placements } companion object { /** Tag used for logging debug and warning messages */ const val TAG = "AnnotationPlacer" } }