@@ -47,21 +47,18 @@ class GeoJsonLineStringRenderer : FeatureRenderer() {
4747 private var backgroundColor: Int? = null
4848 private var filterEpsilon = 0f
4949 private var reducedPaths = emptyList<PrecomputedLineString >()
50- private var labelGrid = PrecomputedLabelGrid (emptyList(), emptyList())
5150 private val lock = Any ()
5251
5352 init {
5453 setRunInBackgroundWhenChanged(this ::renderFeaturesInBackground)
5554 }
5655
5756 private fun render (
58- points : List <Coordinate >,
57+ pixels : List <PixelCoordinate >,
58+ originPixel : PixelCoordinate ,
5959 path : Path ,
60- bounds : Rectangle ,
61- projection : IMapViewProjection
60+ bounds : Rectangle
6261 ): FloatArray {
63- val originPixel = projection.toPixels(projection.center)
64- val pixels = points.map { projection.toPixels(it) }
6562 val segments = mutableListOf<Float >()
6663
6764 clipper.clip(pixels, bounds, segments, originPixel, rdpFilterEpsilon = filterEpsilon)
@@ -120,6 +117,8 @@ class GeoJsonLineStringRenderer : FeatureRenderer() {
120117 viewBounds.bottom - margin
121118 )
122119
120+ val originPixel = projection.toPixels(projection.center)
121+
123122 val precomputed = features.mapNotNull {
124123 val geometry = it.geometry as GeoJsonLineString
125124 val line = geometry.line
@@ -137,22 +136,35 @@ class GeoJsonLineStringRenderer : FeatureRenderer() {
137136 return @mapNotNull null
138137 }
139138
139+ val pixelPoints = coordinates.map { coordinate -> projection.toPixels(coordinate) }
140140 val path = pathPool.get()
141- val lineSegments = render(coordinates, path, actualViewBounds, projection )
141+ val lineSegments = render(pixelPoints, originPixel, path, actualViewBounds )
142142 if (lineSegments.isEmpty()) {
143143 pathPool.release(path)
144144 return @mapNotNull null
145145 }
146146
147+ val name = it.getName()
148+ val labelSegments = if (shouldRenderLabels && name != null ) {
149+ pixelPoints.windowed(2 ).map { segment ->
150+ val p1 = segment[0 ]
151+ val p2 = segment[1 ]
152+ LabelSegment (p1.midpoint(p2), p1.angleTo(p2))
153+ }
154+ } else {
155+ emptyList()
156+ }
157+
147158 PrecomputedLineString (
148159 it,
149160 coordinates,
150161 lineSegments,
151- it.getName() ,
162+ name ,
152163 it.getColor() ? : Color .TRANSPARENT ,
153164 it.getLineStyle() ? : LineStyle .Solid ,
154165 (it.getStrokeWeight()
155166 ? : DEFAULT_LINE_STRING_STROKE_WEIGHT_DP ) / DEFAULT_LINE_STRING_STROKE_WEIGHT_DP ,
167+ labelSegments,
156168 path,
157169 projection.center,
158170 projection.metersPerPixel
@@ -164,8 +176,7 @@ class GeoJsonLineStringRenderer : FeatureRenderer() {
164176 bounds.center.latitude
165177 )
166178
167- // Only show labels at zoom level 13+
168- if (zoomLevel >= 13 ) {
179+ val gridPoints = if (shouldRenderLabels && zoomLevel >= 13 ) {
169180 // Grid spacing based on zoom level (degrees)
170181 val resolution = when (zoomLevel) {
171182 13 -> 0.048
@@ -177,7 +188,6 @@ class GeoJsonLineStringRenderer : FeatureRenderer() {
177188 else -> 0.001
178189 }
179190
180- // Grid
181191 val latitudes = Interpolation .getMultiplesBetween(
182192 bounds.south,
183193 bounds.north,
@@ -189,9 +199,23 @@ class GeoJsonLineStringRenderer : FeatureRenderer() {
189199 resolution
190200 )
191201
192- labelGrid = PrecomputedLabelGrid (latitudes, longitudes)
202+ val points = mutableListOf<PixelCoordinate >()
203+ for (lat in latitudes) {
204+ for (lon in longitudes) {
205+ points.add(projection.toPixels(Coordinate (lat, lon)))
206+ }
207+ }
208+ points
193209 } else {
194- labelGrid = PrecomputedLabelGrid (emptyList(), emptyList())
210+ emptyList()
211+ }
212+
213+ if (shouldRenderLabels && gridPoints.isNotEmpty()) {
214+ val canvasWidth = (viewBounds.right - viewBounds.left).absoluteValue
215+ val canvasHeight = (viewBounds.bottom - viewBounds.top).absoluteValue
216+ val minSeparationPx = max(canvasWidth, canvasHeight) / 4f
217+ val maxLabels = 5
218+ computeLabelPlacements(precomputed, gridPoints, minSeparationPx, maxLabels, originPixel)
195219 }
196220
197221 synchronized(lock) {
@@ -271,68 +295,29 @@ class GeoJsonLineStringRenderer : FeatureRenderer() {
271295 // TODO: Adjust text size / wrapping based on name length
272296 drawer.textSize(drawer.sp(10f * map.layerScale))
273297 val strokeWeight = drawer.dp(2.5f * map.layerScale)
274- val minSeparationPx = max(drawer.canvas.width, drawer.canvas.height) / 4f
275298 val maxLabels = 5
276299
277- // World-aligned grid for label selection
278- val bounds = map.mapBounds
279- val zoomLevel = TileMath .distancePerPixelToZoom(
280- map.metersPerPixel.toDouble(),
281- bounds.center.latitude
282- )
283-
284- // Only show labels at zoom level 13+
285- if (zoomLevel < 13 ) return
286-
287- // TODO: Precompute all of this
288- val segmentCenters = ArrayList <PixelCoordinate >(path.points.size - 1 )
289- val segmentAngles = ArrayList <Float >(path.points.size - 1 )
290- for (i in 0 until (path.points.size - 1 )) {
291- val p1 = map.toPixel(path.points[i])
292- val p2 = map.toPixel(path.points[i + 1 ])
293- segmentCenters.add(p1.midpoint(p2))
294- segmentAngles.add(p1.angleTo(p2))
295- }
296-
297- val chosenSegments = HashSet <Int >()
298- val placedCenters = mutableListOf<PixelCoordinate >()
299-
300300 drawer.strokeWeight(strokeWeight)
301301 drawer.textMode(TextMode .Center )
302302 drawer.stroke(Color .WHITE )
303303 drawer.fill(Color .BLACK )
304304 drawer.noPathEffect()
305305
306- val grid = labelGrid
306+ val placements = path.labelPlacements
307+ if (placements.isEmpty()) return
307308
309+ val centerPixel = map.toPixel(path.origin)
308310 var labelsDrawn = 0
309- for (lat in grid.latitudes ) {
311+ for (placement in placements ) {
310312 if (labelsDrawn >= maxLabels) break
311- for (lon in grid.longitudes) {
312- if (labelsDrawn >= maxLabels) break
313- val gridPixel = map.toPixel(Coordinate (lat, lon))
314-
315- // Find nearest segment center to this grid point
316- val closestIndex =
317- SolMath .argmin(segmentCenters.map { it.distanceTo(gridPixel) })
318-
319- if (closestIndex >= 0 ) {
320- val center = segmentCenters[closestIndex]
321- if (chosenSegments.contains(closestIndex)) continue
322- if (placedCenters.any { it.distanceTo(center) < minSeparationPx }) continue
323-
324- chosenSegments.add(closestIndex)
325- placedCenters.add(center)
326- val angle = segmentAngles[closestIndex]
327- val drawAngle = if (angle.absoluteValue > 90 ) angle + 180 else angle
328-
329- drawer.push()
330- drawer.rotate(drawAngle, center.x, center.y)
331- drawer.text(path.name, center.x, center.y)
332- drawer.pop()
333- labelsDrawn++
334- }
335- }
313+ val center = placement.center + centerPixel
314+ val drawAngle = placement.angle
315+
316+ drawer.push()
317+ drawer.rotate(drawAngle, center.x, center.y)
318+ drawer.text(path.name, center.x, center.y)
319+ drawer.pop()
320+ labelsDrawn++
336321 }
337322 }
338323 }
@@ -343,7 +328,7 @@ class GeoJsonLineStringRenderer : FeatureRenderer() {
343328 (this .y + other.y) / 2
344329 )
345330 }
346-
331+
347332 private fun PixelCoordinate.angleTo (other : PixelCoordinate ): Float {
348333 return atan2(
349334 other.y - this .y,
@@ -359,14 +344,64 @@ class GeoJsonLineStringRenderer : FeatureRenderer() {
359344 val color : Int ,
360345 val lineStyle : LineStyle ,
361346 val thicknessScale : Float ,
347+ var labelSegments : List <LabelSegment >,
362348 val path : Path ,
363349 val origin : Coordinate ,
364- val renderedScale : Float
350+ val renderedScale : Float ,
351+ var labelPlacements : List <LabelPlacement > = emptyList()
352+ )
353+ class LabelSegment (
354+ val center : PixelCoordinate ,
355+ val angle : Float
365356 )
366357
367- class PrecomputedLabelGrid (
368- val latitudes : List < Double > ,
369- val longitudes : List < Double >,
358+ class LabelPlacement (
359+ val center : PixelCoordinate ,
360+ val angle : Float
370361 )
371362
363+ private fun computeLabelPlacements (
364+ paths : List <PrecomputedLineString >,
365+ gridPoints : List <PixelCoordinate >,
366+ minSeparationPx : Float ,
367+ maxLabels : Int ,
368+ originPixel : PixelCoordinate
369+ ) {
370+ for (path in paths) {
371+ val segments = path.labelSegments
372+ if (segments.isEmpty()) {
373+ path.labelPlacements = emptyList()
374+ continue
375+ }
376+
377+ val placements = mutableListOf<LabelPlacement >()
378+ val chosenSegments = HashSet <Int >()
379+ val placedCenters = mutableListOf<PixelCoordinate >()
380+
381+ for (gridPixel in gridPoints) {
382+ if (placements.size >= maxLabels) break
383+
384+ val closestIndex =
385+ SolMath .argmin(segments.map { it.center.distanceTo(gridPixel) })
386+
387+ if (closestIndex >= 0 ) {
388+ val center = segments[closestIndex].center - originPixel
389+ if (chosenSegments.contains(closestIndex)) continue
390+ if (placedCenters.any { it.distanceTo(center) < minSeparationPx }) continue
391+
392+ chosenSegments.add(closestIndex)
393+ placedCenters.add(center)
394+
395+ val angle = segments[closestIndex].angle
396+ val drawAngle = if (angle.absoluteValue > 90 ) angle + 180 else angle
397+
398+ placements.add(LabelPlacement (center, drawAngle))
399+ }
400+ }
401+
402+ path.labelPlacements = placements
403+ path.labelSegments = emptyList()
404+ }
405+ }
406+
372407}
0 commit comments