@@ -25,7 +25,17 @@ import org.microg.gms.maps.mapbox.model.AnnotationType.LINE
2525import org.microg.gms.maps.mapbox.utils.toMapbox
2626import org.microg.gms.utils.warnOnTransactionIssues
2727import 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
2837import com.google.android.gms.maps.model.PolylineOptions as GmsLineOptions
38+ import com.mapbox.mapboxsdk.geometry.LatLng as MapboxLatLng
2939
3040abstract 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