Skip to content

Commit 7c6e79c

Browse files
committed
WIP: Implement geodesic polylines
1 parent 4da3a34 commit 7c6e79c

File tree

1 file changed

+123
-2
lines changed
  • play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model

1 file changed

+123
-2
lines changed

play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,17 @@ import org.microg.gms.maps.mapbox.model.AnnotationType.LINE
2525
import org.microg.gms.maps.mapbox.utils.toMapbox
2626
import org.microg.gms.utils.warnOnTransactionIssues
2727
import java.util.LinkedList
28+
import kotlin.math.abs
29+
import kotlin.math.acos
30+
import kotlin.math.asin
31+
import kotlin.math.atan2
32+
import kotlin.math.ceil
33+
import kotlin.math.cos
34+
import kotlin.math.max
35+
import kotlin.math.sin
36+
import kotlin.math.sqrt
2837
import com.google.android.gms.maps.model.PolylineOptions as GmsLineOptions
38+
import com.mapbox.mapboxsdk.geometry.LatLng as MapboxLatLng
2939

3040
abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOptions, private val dpiFactor: Function0<Float>) : IPolylineDelegate.Stub() {
3141
internal var points: List<LatLng> = ArrayList(options.points)
@@ -93,6 +103,7 @@ abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOpti
93103

94104
override fun setGeodesic(geod: Boolean) {
95105
this.geodesic = geod
106+
update()
96107
}
97108

98109
override fun isGeodesic(): Boolean = geodesic
@@ -166,6 +177,108 @@ class PolylineImpl(private val map: GoogleMapImpl, id: String, options: GmsLineO
166177
override var annotations = computeAnnotations()
167178
override var removed: Boolean = false
168179

180+
private fun interpolateGeodesic(points: List<MapboxLatLng>): List<MapboxLatLng> {
181+
val maxSegmentMeters = 20_000.0
182+
val curvatureBoost = 0.75
183+
184+
if (points.size <= 1) return points.toList()
185+
186+
val r = 6_371_008.8 // mean Earth radius (meters)
187+
188+
fun toVec(latDeg: Double, lonDeg: Double): DoubleArray {
189+
val lat = Math.toRadians(latDeg)
190+
val lon = Math.toRadians(lonDeg)
191+
val cl = cos(lat)
192+
return doubleArrayOf(cl * cos(lon), cl * sin(lon), sin(lat))
193+
}
194+
195+
fun norm(v: DoubleArray): DoubleArray {
196+
val m = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
197+
return doubleArrayOf(v[0] / m, v[1] / m, v[2] / m)
198+
}
199+
200+
fun toLatLng(v: DoubleArray): MapboxLatLng {
201+
val x = v[0];
202+
val y = v[1];
203+
val z = v[2]
204+
val lat = asin(z)
205+
val lon = atan2(y, x)
206+
// wrap to [-180,180)
207+
var lonDeg = Math.toDegrees(lon)
208+
lonDeg = ((lonDeg + 540.0) % 360.0) - 180.0
209+
return MapboxLatLng(Math.toDegrees(lat), lonDeg)
210+
}
211+
212+
fun centralAngle(a: DoubleArray, b: DoubleArray): Double {
213+
val dot = (a[0] * b[0] + a[1] * b[1] + a[2] * b[2]).coerceIn(-1.0, 1.0)
214+
return acos(dot)
215+
}
216+
217+
val out = ArrayList<MapboxLatLng>(points.size * 4)
218+
219+
for (i in 0 until points.lastIndex) {
220+
val a = points[i]
221+
val b = points[i + 1]
222+
val va = norm(toVec(a.latitude, a.longitude))
223+
val vb = norm(toVec(b.latitude, b.longitude))
224+
val omega = centralAngle(va, vb)
225+
226+
// Base segment count from distance
227+
val distance = omega * r
228+
var steps = max(1, ceil(distance / maxSegmentMeters).toInt())
229+
230+
// Heuristic curvature boost (how "curvy" it *looks* in Web-Mercator)
231+
val meanLatRad = Math.toRadians((a.latitude + b.latitude) / 2.0)
232+
val dLonRad = abs(
233+
// shortest ∆lon across antimeridian
234+
((Math.toRadians(b.longitude - a.longitude) + Math.PI) % (2 * Math.PI)) - Math.PI
235+
)
236+
val mercatorCurviness = abs(sin(meanLatRad)) * (dLonRad / Math.PI) // 0..1
237+
val boost = 1.0 + curvatureBoost * mercatorCurviness
238+
steps = max(1, ceil(steps * boost).toInt())
239+
240+
// Emit points along the great-circle (slerp)
241+
val sinOmega = sin(omega)
242+
// Add first point (or skip if already added as previous segment's end)
243+
if (i == 0) out.add(MapboxLatLng(a.latitude, a.longitude))
244+
245+
if (omega == 0.0 || sinOmega == 0.0) {
246+
// identical points: skip interpolation
247+
out.add(MapboxLatLng(b.latitude, b.longitude))
248+
continue
249+
}
250+
251+
for (k in 1..steps) {
252+
val t = k.toDouble() / steps
253+
val s1 = sin((1 - t) * omega) / sinOmega
254+
val s2 = sin(t * omega) / sinOmega
255+
val vx = s1 * va[0] + s2 * vb[0]
256+
val vy = s1 * va[1] + s2 * vb[1]
257+
val vz = s1 * va[2] + s2 * vb[2]
258+
var p = toLatLng(doubleArrayOf(vx, vy, vz))
259+
260+
if (out.isNotEmpty() && abs(p.longitude - out.last().longitude) > 180) {
261+
// Make sure the current point crosses the antimeridian not normalized,
262+
// i.e. going from +179° to +181° instead of +179° to -179°.
263+
// This avoids a long horizontal line across the map.
264+
val lon = if (out.last().longitude > 0) p.longitude + 360 else p.longitude - 360
265+
p = MapboxLatLng(p.latitude, lon)
266+
}
267+
268+
// Avoid duplicating the joint point on the next segment
269+
if (k < steps || i == points.lastIndex - 1) {
270+
out.add(p)
271+
}
272+
}
273+
}
274+
return out
275+
}
276+
277+
private fun List<MapboxLatLng>.mapToGeodesicIfNeeded(): List<MapboxLatLng> {
278+
if (!geodesic) return this
279+
return interpolateGeodesic(this)
280+
}
281+
169282
private fun computeAnnotations(): List<AnnotationTracker<Line, LineOptions>> {
170283
val pointsQueue = LinkedList(points)
171284
val result = mutableListOf<AnnotationTracker<Line, LineOptions>>()
@@ -184,13 +297,21 @@ class PolylineImpl(private val map: GoogleMapImpl, id: String, options: GmsLineO
184297
.withLineColor(ColorUtils.colorToRgbaString(span.style.color))
185298
.withLineOpacity(if (visible and span.style.isVisible) 1f else 0f)
186299
.withLineWidth((span.style.width) / map.dpiFactor)
187-
.withLatLngs(spanPoints.map { it.toMapbox() })
300+
.withLatLngs(
301+
spanPoints
302+
.map { it.toMapbox() }
303+
.mapToGeodesicIfNeeded()
304+
)
188305
result.add(AnnotationTracker(options))
189306
}
190307

191308
if (pointsQueue.isNotEmpty()) {
192309
val options = baseAnnotationOptions
193-
.withLatLngs(pointsQueue.map { it.toMapbox() })
310+
.withLatLngs(
311+
pointsQueue
312+
.map { it.toMapbox() }
313+
.mapToGeodesicIfNeeded()
314+
)
194315
result.add(AnnotationTracker(options))
195316
}
196317

0 commit comments

Comments
 (0)