From aa64a6263270c621f22759444fe615ecb4f19109 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 14 Sep 2025 00:40:35 +0200 Subject: [PATCH 1/6] Prepare for multiple annotations per polyline --- .../microg/gms/maps/mapbox/LiteGoogleMap.kt | 4 +- .../maps/mapbox/model/AnnotationTracker.kt | 9 ++++ .../microg/gms/maps/mapbox/model/Circle.kt | 53 ++++++++++--------- .../microg/gms/maps/mapbox/model/Marker.kt | 5 +- .../microg/gms/maps/mapbox/model/Markup.kt | 20 +++---- .../microg/gms/maps/mapbox/model/Polygon.kt | 6 ++- .../microg/gms/maps/mapbox/model/Polyline.kt | 6 +-- 7 files changed, 61 insertions(+), 42 deletions(-) create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/AnnotationTracker.kt diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/LiteGoogleMap.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/LiteGoogleMap.kt index f4395633c6..764907204d 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/LiteGoogleMap.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/LiteGoogleMap.kt @@ -261,7 +261,7 @@ class LiteGoogleMapImpl(context: Context, var options: GoogleMapOptions) : Abstr PropertyFactory.lineCap(Property.LINE_CAP_ROUND) ) ).withSource( - GeoJsonSource(polyline.id, polyline.annotationOptions.geometry) + GeoJsonSource(polyline.id, polyline.baseAnnotationOptions.geometry) ) } @@ -281,7 +281,7 @@ class LiteGoogleMapImpl(context: Context, var options: GoogleMapOptions) : Abstr withProperties(PropertyFactory.linePattern(name)) styleBuilder.withImage(name, it.makeBitmap(circle.strokeColor, circle.strokeWidth, dpi)) } - }).withSource(GeoJsonSource("${circle.id}s", circle.line.annotationOptions.geometry)) + }).withSource(GeoJsonSource("${circle.id}s", circle.line.annotations.first().options.geometry)) } // Add markers diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/AnnotationTracker.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/AnnotationTracker.kt new file mode 100644 index 0000000000..203558bff0 --- /dev/null +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/AnnotationTracker.kt @@ -0,0 +1,9 @@ +package org.microg.gms.maps.mapbox.model + +import com.mapbox.mapboxsdk.plugins.annotation.Annotation +import com.mapbox.mapboxsdk.plugins.annotation.Options + +data class AnnotationTracker, S : Options>( + val options: S, + var annotation: T? = null +) diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt index 5cc551212d..e82a2ec009 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt @@ -21,7 +21,6 @@ import android.util.Log import com.google.android.gms.dynamic.IObjectWrapper import com.google.android.gms.dynamic.ObjectWrapper import com.google.android.gms.dynamic.unwrap -import com.google.android.gms.maps.model.Dash import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.PatternItem import com.google.android.gms.maps.model.internal.ICircleDelegate @@ -36,9 +35,9 @@ import com.mapbox.turf.TurfMeta import com.mapbox.turf.TurfTransformation import org.microg.gms.maps.mapbox.GoogleMapImpl import org.microg.gms.maps.mapbox.LiteGoogleMapImpl -import org.microg.gms.maps.mapbox.utils.toPoint import org.microg.gms.maps.mapbox.getName import org.microg.gms.maps.mapbox.makeBitmap +import org.microg.gms.maps.mapbox.utils.toPoint import com.google.android.gms.maps.model.CircleOptions as GmsCircleOptions val NORTH_POLE: Point = Point.fromLngLat(0.0, 90.0) @@ -64,31 +63,31 @@ abstract class AbstractCircle( internal var tag: Any? = null internal val line: Markup = object : Markup { - override var annotation: Line? = null - override val annotationOptions: LineOptions - get() = LineOptions() - .withGeometry( - LineString.fromLngLats( - makeOutlineLatLngs() - ) - ).withLineWidth(strokeWidth / dpiFactor()) - .withLineColor(ColorUtils.colorToRgbaString(strokeColor)) - .withLineOpacity(if (visible) 1f else 0f) - .apply { - strokePattern?.let { - withLinePattern(it.getName(strokeColor, strokeWidth)) - } - } + override var annotations: List> = listOf( + AnnotationTracker( + LineOptions() + .withGeometry( + LineString.fromLngLats( + makeOutlineLatLngs() + ) + ).withLineWidth(strokeWidth / dpiFactor()) + .withLineColor(ColorUtils.colorToRgbaString(strokeColor)) + .withLineOpacity(if (visible) 1f else 0f) + .apply { + strokePattern?.let { + withLinePattern(it.getName(strokeColor, strokeWidth)) + } + }) + ) override var removed: Boolean = false } val annotationOptions: FillOptions - get() = - FillOptions() - .withGeometry(makePolygon()) - .withFillColor(ColorUtils.colorToRgbaString(fillColor)) - .withFillOpacity(if (visible && !wrapsAroundPoles()) 1f else 0f) + get() = FillOptions() + .withGeometry(makePolygon()) + .withFillColor(ColorUtils.colorToRgbaString(fillColor)) + .withFillOpacity(if (visible && !wrapsAroundPoles()) 1f else 0f) internal abstract fun update() @@ -262,9 +261,13 @@ abstract class AbstractCircle( class CircleImpl(private val map: GoogleMapImpl, private val id: String, options: GmsCircleOptions) : AbstractCircle(id, options, { map.dpiFactor }), Markup { - override var annotation: Fill? = null + override var annotations = + listOf(AnnotationTracker(annotationOptions)) override var removed: Boolean = false + private val annotation: Fill? + get() = annotations.firstOrNull()?.annotation + override fun update() { val polygon = makePolygon() @@ -275,7 +278,7 @@ class CircleImpl(private val map: GoogleMapImpl, private val id: String, options it.fillOpacity = if (visible && !wrapsAroundPoles()) 1f else 0f } - line.annotation?.let { + line.annotations.firstOrNull()?.annotation?.let { it.latLngs = makeOutlineLatLngs().map { point -> com.mapbox.mapboxsdk.geometry.LatLng( point.latitude(), @@ -288,7 +291,7 @@ class CircleImpl(private val map: GoogleMapImpl, private val id: String, options (strokePattern ?: emptyList()).let { pattern -> val bitmapName = pattern.getName(strokeColor, strokeWidth) map.addBitmap(bitmapName, pattern.makeBitmap(strokeColor, strokeWidth)) - line.annotation?.linePattern = bitmapName + line.annotations.firstOrNull()?.annotation?.linePattern = bitmapName } map.lineManager?.let { line.update(it) } diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt index bbb89993c9..7bc9453e24 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt @@ -170,9 +170,12 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options internal var rotation: Float = options.rotation override var draggable: Boolean = options.isDraggable - override var annotation: Symbol? = null + override var annotations = listOf(AnnotationTracker(annotationOptions)) override var removed: Boolean = false + private val annotation: Symbol? + get() = annotations.firstOrNull()?.annotation + override fun remove() { removed = true map.symbolManager?.let { update(it) } diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Markup.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Markup.kt index 179c5a0b07..f4d03dc8d6 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Markup.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Markup.kt @@ -16,25 +16,25 @@ package org.microg.gms.maps.mapbox.model -import android.util.Log import com.mapbox.mapboxsdk.plugins.annotation.Annotation import com.mapbox.mapboxsdk.plugins.annotation.AnnotationManager import com.mapbox.mapboxsdk.plugins.annotation.Options interface Markup, S : Options> { - var annotation: T? - val annotationOptions: S + var annotations: List> var removed: Boolean fun update(manager: AnnotationManager<*, T, S, *, *, *>) { synchronized(this) { - if (removed && annotation != null) { - manager.delete(annotation) - annotation = null - } else if (annotation != null) { - manager.update(annotation) - } else if (!removed) { - annotation = manager.create(annotationOptions) + for (tracker in annotations) { + if (removed && tracker.annotation != null) { + manager.delete(tracker.annotation) + tracker.annotation = null + } else if (tracker.annotation != null) { + manager.update(tracker.annotation) + } else if (!removed) { + tracker.annotation = manager.create(tracker.options) + } } } } diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt index c5c205ed23..85cfb5fec5 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt @@ -203,9 +203,13 @@ class PolygonImpl(private val map: GoogleMapImpl, id: String, options: PolygonOp ) }).toMutableList() - override var annotation: Fill? = null + override var annotations = + listOf(AnnotationTracker(annotationOptions)) override var removed: Boolean = false + protected val annotation: Fill? + get() = annotations[0].annotation + override fun remove() { removed = true map.fillManager?.let { update(it) } diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt index 4405c21c77..cdee511b21 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt @@ -35,7 +35,7 @@ abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOpti internal var geodesic = options.isGeodesic internal var zIndex = options.zIndex - val annotationOptions: LineOptions + val baseAnnotationOptions: LineOptions get() = LineOptions() .withLatLngs(points.map { it.toMapbox() }) .withLineWidth(width / dpiFactor.invoke()) @@ -151,7 +151,7 @@ abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOpti class PolylineImpl(private val map: GoogleMapImpl, id: String, options: GmsLineOptions) : AbstractPolylineImpl(id, options, { map.dpiFactor }), Markup { - override var annotation: Line? = null + override var annotations = listOf(AnnotationTracker(baseAnnotationOptions)) override var removed: Boolean = false override fun remove() { @@ -160,7 +160,7 @@ class PolylineImpl(private val map: GoogleMapImpl, id: String, options: GmsLineO } override fun update() { - annotation?.apply { + annotations.firstOrNull()?.annotation?.apply { latLngs = points.map { it.toMapbox() } lineWidth = width / map.dpiFactor setLineColor(color) From 468a3cb4ece441e21aae31b42084cabc1ecea654 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 14 Sep 2025 01:49:37 +0200 Subject: [PATCH 2/6] Implement multiple-span polylines --- .../maps/mapbox/model/AnnotationTracker.kt | 2 +- .../microg/gms/maps/mapbox/model/Polyline.kt | 53 ++++++++++++++++--- .../android/gms/maps/model/StrokeStyle.java | 20 +++++++ 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/AnnotationTracker.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/AnnotationTracker.kt index 203558bff0..6310c231d9 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/AnnotationTracker.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/AnnotationTracker.kt @@ -4,6 +4,6 @@ import com.mapbox.mapboxsdk.plugins.annotation.Annotation import com.mapbox.mapboxsdk.plugins.annotation.Options data class AnnotationTracker, S : Options>( - val options: S, + var options: S, var annotation: T? = null ) diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt index cdee511b21..404fd50c9c 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt @@ -19,6 +19,7 @@ import org.microg.gms.maps.mapbox.GoogleMapImpl import org.microg.gms.maps.mapbox.LiteGoogleMapImpl import org.microg.gms.maps.mapbox.utils.toMapbox import org.microg.gms.utils.warnOnTransactionIssues +import java.util.LinkedList import com.google.android.gms.maps.model.PolylineOptions as GmsLineOptions abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOptions, private val dpiFactor: Function0) : IPolylineDelegate.Stub() { @@ -34,10 +35,10 @@ abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOpti internal var endCap: Cap = options.endCap internal var geodesic = options.isGeodesic internal var zIndex = options.zIndex + internal var spans = options.spans val baseAnnotationOptions: LineOptions get() = LineOptions() - .withLatLngs(points.map { it.toMapbox() }) .withLineWidth(width / dpiFactor.invoke()) .withLineColor(ColorUtils.colorToRgbaString(color)) .withLineOpacity(if (visible) 1f else 0f) @@ -151,20 +152,58 @@ abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOpti class PolylineImpl(private val map: GoogleMapImpl, id: String, options: GmsLineOptions) : AbstractPolylineImpl(id, options, { map.dpiFactor }), Markup { - override var annotations = listOf(AnnotationTracker(baseAnnotationOptions)) + override var annotations = computeAnnotations() override var removed: Boolean = false + private fun computeAnnotations(): List> { + val pointsQueue = LinkedList(points) + val result = mutableListOf>() + + for (span in spans) { + val spanPoints = mutableListOf() + + var i = 0 + while (i < span.segments && pointsQueue.isNotEmpty()) { + spanPoints.add(pointsQueue.removeFirst()) + i++ + } + + val options = baseAnnotationOptions + // TODO: implement gradient support + .withLineColor(ColorUtils.colorToRgbaString(span.style.color)) + .withLineOpacity(if (visible and span.style.isVisible) 1f else 0f) + .withLineWidth((span.style.width) / map.dpiFactor) + .withLatLngs(spanPoints.map { it.toMapbox() }) + result.add(AnnotationTracker(options)) + } + + if (pointsQueue.isNotEmpty()) { + val options = baseAnnotationOptions + .withLatLngs(pointsQueue.map { it.toMapbox() }) + result.add(AnnotationTracker(options)) + } + + return result + } + override fun remove() { removed = true map.lineManager?.let { update(it) } } override fun update() { - annotations.firstOrNull()?.annotation?.apply { - latLngs = points.map { it.toMapbox() } - lineWidth = width / map.dpiFactor - setLineColor(color) - lineOpacity = if (visible) 1f else 0f + computeAnnotations().forEachIndexed { i, it -> + if (i < annotations.size) { + annotations[i].options = it.options + annotations[i].annotation?.apply { + latLngs = it.options.latLngs + lineWidth = it.options.lineWidth + lineColor = it.options.lineColor + lineOpacity = it.options.lineOpacity + } + } else { + annotations = annotations + it + } } map.lineManager?.let { update(it) } } diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/StrokeStyle.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/StrokeStyle.java index edc91583a3..392e226c26 100644 --- a/play-services-maps/src/main/java/com/google/android/gms/maps/model/StrokeStyle.java +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/StrokeStyle.java @@ -20,4 +20,24 @@ public class StrokeStyle extends AutoSafeParcelable { private StampStyle stamp; public static final Creator CREATOR = new AutoCreator<>(StrokeStyle.class); + + public float getWidth() { + return width; + } + + public int getColor() { + return color; + } + + public int getToColor() { + return toColor; + } + + public boolean isVisible() { + return isVisible; + } + + public StampStyle getStamp() { + return stamp; + } } From fcae3854bcc20df4cd2b6ea1dc74929d0439c10e Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 14 Sep 2025 03:33:25 +0200 Subject: [PATCH 3/6] Implement Z-index handling --- .../org/microg/gms/maps/mapbox/GoogleMap.kt | 392 ++++++++++++------ .../microg/gms/maps/mapbox/model/Circle.kt | 31 +- .../microg/gms/maps/mapbox/model/LayerKind.kt | 8 + .../microg/gms/maps/mapbox/model/Marker.kt | 25 +- .../microg/gms/maps/mapbox/model/Polygon.kt | 25 +- .../microg/gms/maps/mapbox/model/Polyline.kt | 18 +- .../gms/maps/mapbox/model/TileOverlay.kt | 1 + .../gms/maps/mapbox/utils/ComparablePair.kt | 18 + 8 files changed, 363 insertions(+), 155 deletions(-) create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/LayerKind.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparablePair.kt diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt index e542fb6a53..567ab0e39e 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt @@ -20,49 +20,99 @@ import android.annotation.SuppressLint import android.content.Context import android.graphics.Bitmap import android.location.Location -import android.os.* -import androidx.annotation.IdRes -import androidx.annotation.Keep +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Parcel import android.util.Log import android.view.Gravity import android.view.View import android.widget.FrameLayout import android.widget.RelativeLayout +import androidx.annotation.IdRes +import androidx.annotation.Keep import androidx.collection.LongSparseArray import com.google.android.gms.dynamic.IObjectWrapper import com.google.android.gms.dynamic.ObjectWrapper +import com.google.android.gms.dynamic.unwrap +import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.GoogleMapOptions -import com.google.android.gms.maps.internal.* -import com.google.android.gms.maps.model.* +import com.google.android.gms.maps.internal.ICancelableCallback +import com.google.android.gms.maps.internal.ILocationSourceDelegate +import com.google.android.gms.maps.internal.IOnCameraChangeListener +import com.google.android.gms.maps.internal.IOnCameraIdleListener +import com.google.android.gms.maps.internal.IOnCameraMoveCanceledListener +import com.google.android.gms.maps.internal.IOnCameraMoveListener +import com.google.android.gms.maps.internal.IOnCameraMoveStartedListener +import com.google.android.gms.maps.internal.IOnMapLoadedCallback +import com.google.android.gms.maps.internal.IOnMapReadyCallback +import com.google.android.gms.maps.internal.IOnMarkerDragListener +import com.google.android.gms.maps.internal.IProjectionDelegate +import com.google.android.gms.maps.internal.ISnapshotReadyCallback +import com.google.android.gms.maps.internal.IUiSettingsDelegate +import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.CircleOptions -import com.google.android.gms.maps.model.internal.* +import com.google.android.gms.maps.model.GroundOverlayOptions +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.google.android.gms.maps.model.MapStyleOptions +import com.google.android.gms.maps.model.MarkerOptions +import com.google.android.gms.maps.model.PolygonOptions +import com.google.android.gms.maps.model.PolylineOptions +import com.google.android.gms.maps.model.TileOverlayOptions +import com.google.android.gms.maps.model.internal.ICircleDelegate +import com.google.android.gms.maps.model.internal.IGroundOverlayDelegate +import com.google.android.gms.maps.model.internal.IMarkerDelegate +import com.google.android.gms.maps.model.internal.IPolygonDelegate +import com.google.android.gms.maps.model.internal.IPolylineDelegate +import com.google.android.gms.maps.model.internal.ITileOverlayDelegate import com.mapbox.mapboxsdk.LibraryLoader import com.mapbox.mapboxsdk.Mapbox import com.mapbox.mapboxsdk.R +import com.mapbox.mapboxsdk.WellKnownTileServer import com.mapbox.mapboxsdk.camera.CameraUpdate +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory import com.mapbox.mapboxsdk.constants.MapboxConstants import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions import com.mapbox.mapboxsdk.location.LocationComponentOptions +import com.mapbox.mapboxsdk.location.engine.LocationEngine import com.mapbox.mapboxsdk.location.modes.CameraMode import com.mapbox.mapboxsdk.location.modes.RenderMode import com.mapbox.mapboxsdk.maps.MapView import com.mapbox.mapboxsdk.maps.MapboxMap -import com.mapbox.mapboxsdk.maps.Style -import com.mapbox.mapboxsdk.plugins.annotation.* +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback import com.mapbox.mapboxsdk.plugins.annotation.Annotation +import com.mapbox.mapboxsdk.plugins.annotation.AnnotationManager +import com.mapbox.mapboxsdk.plugins.annotation.Fill +import com.mapbox.mapboxsdk.plugins.annotation.FillManager +import com.mapbox.mapboxsdk.plugins.annotation.FillOptions +import com.mapbox.mapboxsdk.plugins.annotation.Line +import com.mapbox.mapboxsdk.plugins.annotation.LineManager +import com.mapbox.mapboxsdk.plugins.annotation.LineOptions +import com.mapbox.mapboxsdk.plugins.annotation.OnSymbolDragListener +import com.mapbox.mapboxsdk.plugins.annotation.Symbol +import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager import com.mapbox.mapboxsdk.style.layers.Property.LINE_CAP_ROUND -import com.google.android.gms.dynamic.unwrap -import com.google.android.gms.maps.GoogleMap -import com.mapbox.mapboxsdk.WellKnownTileServer +import org.microg.gms.maps.mapbox.model.AbstractMarker +import org.microg.gms.maps.mapbox.model.BitmapDescriptorFactoryImpl +import org.microg.gms.maps.mapbox.model.CircleImpl +import org.microg.gms.maps.mapbox.model.GroundOverlayImpl import org.microg.gms.maps.mapbox.model.InfoWindow +import org.microg.gms.maps.mapbox.model.LayerKind +import org.microg.gms.maps.mapbox.model.LayerKind.FILL +import org.microg.gms.maps.mapbox.model.LayerKind.LINE +import org.microg.gms.maps.mapbox.model.LayerKind.SYMBOL +import org.microg.gms.maps.mapbox.model.MarkerImpl +import org.microg.gms.maps.mapbox.model.Markup +import org.microg.gms.maps.mapbox.model.PolygonImpl +import org.microg.gms.maps.mapbox.model.PolylineImpl +import org.microg.gms.maps.mapbox.model.TileOverlayImpl import org.microg.gms.maps.mapbox.model.getInfoWindowViewFor -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory -import com.mapbox.mapboxsdk.location.engine.* -import com.mapbox.mapboxsdk.maps.OnMapReadyCallback -import org.microg.gms.maps.mapbox.model.* +import org.microg.gms.maps.mapbox.utils.ComparablePair import org.microg.gms.maps.mapbox.utils.MultiArchLoader import org.microg.gms.maps.mapbox.utils.toGms import org.microg.gms.maps.mapbox.utils.toMapbox +import java.util.TreeMap import java.util.concurrent.atomic.AtomicBoolean private fun LongSparseArray.values() = (0 until size()).mapNotNull { valueAt(it) } @@ -99,17 +149,19 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG private var cameraIdleListener: IOnCameraIdleListener? = null private var markerDragListener: IOnMarkerDragListener? = null - var lineManager: LineManager? = null - val pendingLines = mutableSetOf>() + private val allocatedZLayers: TreeMap, String> = TreeMap() + + var lineManagers: MutableMap = mutableMapOf() + val pendingLines = mutableSetOf>>() var lineId = 0L - var fillManager: FillManager? = null - val pendingFills = mutableSetOf>() + var fillManagers: MutableMap = mutableMapOf() + val pendingFills = mutableSetOf>>() val circles = mutableMapOf() var fillId = 0L - var symbolManager: SymbolManager? = null - val pendingMarkers = mutableSetOf() + var symbolManagers: MutableMap = mutableMapOf() + val pendingMarkers = mutableSetOf>() val markers = mutableMapOf() var markerId = 0L @@ -302,9 +354,10 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG override fun addPolyline(options: PolylineOptions): IPolylineDelegate? { val line = PolylineImpl(this, "l${lineId++}", options) synchronized(this) { - val lineManager = lineManager + val lineManager = getLineManagerForZIndex(line.zIndex) + Log.d(TAG, "addPolyline zIndex=${line.zIndex}, manager=$lineManager") if (lineManager == null) { - pendingLines.add(line) + pendingLines.add(Pair(line.zIndex, line)) } else { line.update(lineManager) } @@ -316,16 +369,16 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG override fun addPolygon(options: PolygonOptions): IPolygonDelegate? { val fill = PolygonImpl(this, "p${fillId++}", options) synchronized(this) { - val fillManager = fillManager + val fillManager = getFillManagerForZIndex(fill.zIndex) if (fillManager == null) { - pendingFills.add(fill) + pendingFills.add(Pair(fill.zIndex, fill)) } else { fill.update(fillManager) } - val lineManager = lineManager + val lineManager = getLineManagerForZIndex(fill.zIndex) if (lineManager == null) { - pendingLines.addAll(fill.strokes) + pendingLines.addAll(fill.strokes.map { Pair(fill.zIndex, it) }) } else { for (stroke in fill.strokes) stroke.update(lineManager) } @@ -336,9 +389,9 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG override fun addMarker(options: MarkerOptions): IMarkerDelegate { val marker = MarkerImpl(this, "m${markerId++}", options) synchronized(this) { - val symbolManager = symbolManager + val symbolManager = getSymbolManagerForZIndex(marker.zIndex) if (symbolManager == null) { - pendingMarkers.add(marker) + pendingMarkers.add(Pair(marker.zIndex, marker)) } else { marker.update(symbolManager) } @@ -359,15 +412,15 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG override fun addCircle(options: CircleOptions): ICircleDelegate { val circle = CircleImpl(this, "c${fillId++}", options) synchronized(this) { - val fillManager = fillManager + val fillManager = getFillManagerForZIndex(circle.zIndex) if (fillManager == null) { - pendingFills.add(circle) + pendingFills.add(Pair(circle.zIndex, circle)) } else { circle.update(fillManager) } - val lineManager = lineManager + val lineManager = getLineManagerForZIndex(circle.zIndex) if (lineManager == null) { - pendingLines.add(circle.line) + pendingLines.add(Pair(circle.zIndex, circle.line)) } else { circle.line.update(lineManager) } @@ -382,9 +435,9 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG } override fun clear() { - lineManager?.let { clear(it) } - fillManager?.let { clear(it) } - symbolManager?.let { clear(it) } + lineManagers.forEach { (_, v) -> clear(v) } + fillManagers.forEach { (_, v) -> clear(v) } + symbolManagers.forEach { (_, v) -> clear(v) } } fun > clear(manager: AnnotationManager<*, T, *, *, *, *>) { @@ -408,22 +461,22 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG } fun applyMapStyle() { - val lines = lineManager?.annotations?.values() - val fills = fillManager?.annotations?.values() - val symbols = symbolManager?.annotations?.values() - val update: (Style) -> Unit = { - lines?.let { runCatching { lineManager?.update(it) } } - fills?.let { runCatching { fillManager?.update(it) } } - symbols?.let { runCatching { symbolManager?.update(it) } } + map?.setStyle(getStyle(mapContext, storedMapType, mapStyle)) { + lineManagers.forEach { (_, manager) -> + val lines = manager.annotations.values() + runCatching { manager.update(lines) } + } + symbolManagers.forEach { (_, manager) -> + val symbols = manager.annotations.values() + runCatching { manager.update(symbols) } + } + fillManagers.forEach { (_, manager) -> + val fills = manager.annotations.values() + runCatching { manager.update(fills) } + } } - map?.setStyle( - getStyle(mapContext, storedMapType, mapStyle), - update - ) - map?.let { BitmapDescriptorFactoryImpl.registerMap(it) } - } override fun setWatermarkEnabled(watermark: Boolean) = afterInitialize { @@ -602,11 +655,150 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG private fun hasSymbolAt(latlng: com.mapbox.mapboxsdk.geometry.LatLng): Boolean { val point = map?.projection?.toScreenLocation(latlng) ?: return false - val features = map?.queryRenderedFeatures(point, symbolManager?.layerId) - ?: return false - return features.isNotEmpty() + return symbolManagers.values.any { manager -> + map + ?.queryRenderedFeatures(point, manager.layerId) + ?.isNotEmpty() + ?: false + } } + private data class LayerBuilderContext( + val belowLayerId: String?, + val aboveLayerId: String? + ) + + private inline fun getOrCreateLayerForZIndexImpl( + zIndex: Float, + layerTypeMap: MutableMap, + layerKind: LayerKind, + layerBuilder: LayerBuilderContext.() -> Pair + ): LayerType? { + layerTypeMap[zIndex]?.let { return it } + if (mapView == null || map == null || map?.style == null) return null + + synchronized(mapLock) { + val layerKey = ComparablePair(-zIndex, layerKind) + val belowId = allocatedZLayers.lowerEntry(layerKey)?.value + var aboveId = allocatedZLayers.higherEntry(layerKey)?.value + if (aboveId == belowId) aboveId = null + val (newLayer, newLayerId) = LayerBuilderContext( + belowId, + aboveId + ).layerBuilder() + + allocatedZLayers[layerKey] = newLayerId + layerTypeMap[zIndex] = newLayer + return newLayer + } + } + + fun getFillManagerForZIndex(zIndex: Float): FillManager? = + getOrCreateLayerForZIndexImpl(zIndex, fillManagers, FILL) { + FillManager( + mapView!!, + map!!, + map!!.style!!, + belowLayerId, + aboveLayerId + ).apply { + addClickListener { fill -> + try { + circles[fill.id]?.let { circle -> + if (circle.isClickable) { + circleClickListener?.let { + it.onCircleClick(circle) + return@addClickListener true + } + } + } + } catch (e: Exception) { + Log.w(TAG, e) + } + false + } + }.let { it to it.layerId } + } + + fun getSymbolManagerForZIndex(zIndex: Float): SymbolManager? = + getOrCreateLayerForZIndexImpl(zIndex, symbolManagers, SYMBOL) { + SymbolManager( + mapView!!, + map!!, + map!!.style!!, + belowLayerId, + aboveLayerId + ).apply { + iconAllowOverlap = true + addClickListener { + val marker = markers[it.id] + try { + if (markers[it.id]?.let { markerClickListener?.onMarkerClick(it) } == true) { + return@addClickListener true + } + } catch (e: Exception) { + Log.w(TAG, e) + return@addClickListener false + } + + marker?.let { showInfoWindow(it) } == true + } + addDragListener(object : OnSymbolDragListener { + override fun onAnnotationDragStarted(annotation: Symbol?) { + try { + markers[annotation?.id]?.let { + markerDragListener?.onMarkerDragStart( + it + ) + } + } catch (e: Exception) { + Log.w(TAG, e) + } + } + + override fun onAnnotationDrag(annotation: Symbol?) { + try { + annotation?.let { symbol -> + markers[symbol.id]?.let { marker -> + marker.setPositionWhileDragging(symbol.latLng.toGms()) + markerDragListener?.onMarkerDrag(marker) + } + } + } catch (e: Exception) { + Log.w(TAG, e) + } + } + + override fun onAnnotationDragFinished(annotation: Symbol?) { + mapView?.post { + try { + markers[annotation?.id]?.let { + markerDragListener?.onMarkerDragEnd( + it + ) + } + } catch (e: Exception) { + Log.w(TAG, e) + } + } + } + }) + }.let { it to it.layerId } + } + + fun getLineManagerForZIndex(zIndex: Float): LineManager? = + getOrCreateLayerForZIndexImpl(zIndex, lineManagers, LINE) { + LineManager( + mapView!!, + map!!, + map!!.style!!, + belowLayerId, + aboveLayerId + ).apply { + lineCap = LINE_CAP_ROUND + }.let { it to it.layerId } + } + private fun initMap(map: MapboxMap) { if (this.map != null) return this.map = map @@ -698,86 +890,18 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG map.getStyle { mapView?.let { view -> if (loaded) return@let - val symbolManager: SymbolManager - val lineManager: LineManager - val fillManager: FillManager - - synchronized(mapLock) { - fillManager = FillManager(view, map, it) - symbolManager = SymbolManager(view, map, it) - lineManager = LineManager(view, map, it) - lineManager.lineCap = LINE_CAP_ROUND - - this.symbolManager = symbolManager - this.lineManager = lineManager - this.fillManager = fillManager - } - symbolManager.iconAllowOverlap = true - symbolManager.addClickListener { - val marker = markers[it.id] - try { - if (markers[it.id]?.let { markerClickListener?.onMarkerClick(it) } == true) { - return@addClickListener true - } - } catch (e: Exception) { - Log.w(TAG, e) - return@addClickListener false - } - - marker?.let { showInfoWindow(it) } == true - } - symbolManager.addDragListener(object : OnSymbolDragListener { - override fun onAnnotationDragStarted(annotation: Symbol?) { - try { - markers[annotation?.id]?.let { markerDragListener?.onMarkerDragStart(it) } - } catch (e: Exception) { - Log.w(TAG, e) - } - } - - override fun onAnnotationDrag(annotation: Symbol?) { - try { - annotation?.let { symbol -> - markers[symbol.id]?.let { marker -> - marker.setPositionWhileDragging(symbol.latLng.toGms()) - markerDragListener?.onMarkerDrag(marker) - } - } - } catch (e: Exception) { - Log.w(TAG, e) - } - } - override fun onAnnotationDragFinished(annotation: Symbol?) { - mapView?.post { - try { - markers[annotation?.id]?.let { markerDragListener?.onMarkerDragEnd(it) } - } catch (e: Exception) { - Log.w(TAG, e) - } - } - } - }) - fillManager.addClickListener { fill -> - try { - circles[fill.id]?.let { circle -> - if (circle.isClickable) { - circleClickListener?.let { - it.onCircleClick(circle) - return@addClickListener true - } - } - } - } catch (e: Exception) { - Log.w(TAG, e) - } - false + pendingFills.forEach { (zIndex, fill) -> + fill.update(getFillManagerForZIndex(zIndex)!!) } - pendingFills.forEach { it.update(fillManager) } pendingFills.clear() - pendingLines.forEach { it.update(lineManager) } + pendingLines.forEach { (zIndex, line) -> + line.update(getLineManagerForZIndex(zIndex)!!) + } pendingLines.clear() - pendingMarkers.forEach { it.update(symbolManager) } + pendingMarkers.forEach { (zIndex, marker) -> + marker.update(getSymbolManagerForZIndex(zIndex)!!) + } pendingMarkers.clear() pendingBitmaps.forEach { map -> it.addImage(map.key, map.value) } @@ -858,13 +982,13 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG override fun onDestroy() { Log.d(TAG, "onDestroy"); userOnInitializedCallbackList.clear() - lineManager?.onDestroy() - lineManager = null - fillManager?.onDestroy() - fillManager = null + lineManagers.forEach { (_, manager) -> manager.onDestroy() } + lineManagers.clear() + fillManagers.forEach { (_, manager) -> manager.onDestroy() } + fillManagers.clear() circles.clear() - symbolManager?.onDestroy() - symbolManager = null + symbolManagers.forEach { (_, manager) -> manager.onDestroy() } + symbolManagers.clear() currentInfoWindow?.close() pendingMarkers.clear() markers.clear() diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt index e82a2ec009..b252d202a9 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt @@ -60,6 +60,7 @@ abstract class AbstractCircle( internal var visible: Boolean = options.isVisible internal var clickable: Boolean = options.isClickable internal var strokePattern: MutableList? = options.strokePattern + internal var zIndex: Float = options.zIndex internal var tag: Any? = null internal val line: Markup = object : Markup { @@ -188,12 +189,11 @@ abstract class AbstractCircle( override fun getFillColor(): Int = fillColor override fun setZIndex(zIndex: Float) { - Log.d(TAG, "unimplemented Method: setZIndex") + this.zIndex = zIndex } override fun getZIndex(): Float { - Log.d(TAG, "unimplemented Method: getZIndex") - return 0f + return zIndex } override fun setVisible(visible: Boolean) { @@ -268,6 +268,21 @@ class CircleImpl(private val map: GoogleMapImpl, private val id: String, options private val annotation: Fill? get() = annotations.firstOrNull()?.annotation + override fun setZIndex(zIndex: Float) { + val oldZIndex = this.zIndex + if (oldZIndex == zIndex) { + super.setZIndex(zIndex) + return + } + + remove() + super.setZIndex(zIndex) + removed = false + line.removed = false + map.getFillManagerForZIndex(zIndex)?.let { update(it) } + map.getLineManagerForZIndex(zIndex)?.let { line.update(it) } + } + override fun update() { val polygon = makePolygon() @@ -293,20 +308,20 @@ class CircleImpl(private val map: GoogleMapImpl, private val id: String, options map.addBitmap(bitmapName, pattern.makeBitmap(strokeColor, strokeWidth)) line.annotations.firstOrNull()?.annotation?.linePattern = bitmapName } - map.lineManager?.let { line.update(it) } + map.getLineManagerForZIndex(zIndex)?.let { line.update(it) } it.setLineColor(strokeColor) } - map.fillManager?.let { update(it) } - map.lineManager?.let { line.update(it) } + map.getFillManagerForZIndex(zIndex)?.let { update(it) } + map.getLineManagerForZIndex(zIndex)?.let { line.update(it) } } override fun remove() { removed = true line.removed = true - map.fillManager?.let { update(it) } - map.lineManager?.let { line.update(it) } + map.getFillManagerForZIndex(zIndex)?.let { update(it) } + map.getLineManagerForZIndex(zIndex)?.let { line.update(it) } } diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/LayerKind.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/LayerKind.kt new file mode 100644 index 0000000000..e0d2082c6f --- /dev/null +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/LayerKind.kt @@ -0,0 +1,8 @@ +package org.microg.gms.maps.mapbox.model + +enum class LayerKind { + LINE, + FILL, + SYMBOL, + RASTER, +} \ No newline at end of file diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt index 7bc9453e24..9ba70733ad 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt @@ -20,14 +20,13 @@ import android.os.Parcel import android.util.Log import com.google.android.gms.dynamic.IObjectWrapper import com.google.android.gms.dynamic.ObjectWrapper +import com.google.android.gms.dynamic.unwrap import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.MarkerOptions import com.google.android.gms.maps.model.internal.IMarkerDelegate import com.mapbox.mapboxsdk.plugins.annotation.AnnotationManager import com.mapbox.mapboxsdk.plugins.annotation.Symbol import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions -import com.google.android.gms.dynamic.unwrap -import com.google.android.gms.maps.model.BitmapDescriptorFactory import org.microg.gms.maps.mapbox.AbstractGoogleMap import org.microg.gms.maps.mapbox.GoogleMapImpl import org.microg.gms.maps.mapbox.LiteGoogleMapImpl @@ -178,7 +177,21 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options override fun remove() { removed = true - map.symbolManager?.let { update(it) } + map.getSymbolManagerForZIndex(zIndex)?.let { update(it) } + } + + override fun setZIndex(zIndex: Float) { + val oldZIndex = this.zIndex + if (oldZIndex == zIndex) { + super.setZIndex(zIndex) + return + } + + removed = true + map.getSymbolManagerForZIndex(zIndex)?.let { update(it) } + super.setZIndex(zIndex) + removed = false + map.getSymbolManagerForZIndex(zIndex)?.let { update(it) } } override fun update() { @@ -189,7 +202,7 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options it.symbolSortKey = zIndex icon?.applyTo(it, anchor, map.dpiFactor) } - map.symbolManager?.let { update(it) } + map.getSymbolManagerForZIndex(zIndex)?.let { update(it) } } override fun update(manager: AnnotationManager<*, Symbol, SymbolOptions, *, *, *>) { @@ -236,7 +249,7 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options override fun setDraggable(draggable: Boolean) { this.draggable = draggable - map.symbolManager?.let { update(it) } + map.getSymbolManagerForZIndex(zIndex)?.let { update(it) } } override fun isDraggable(): Boolean = draggable @@ -272,7 +285,7 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options override fun setRotation(rotation: Float) { this.rotation = rotation annotation?.iconRotate = rotation - map.symbolManager?.let { update(it) } + map.getSymbolManagerForZIndex(zIndex)?.let { update(it) } map.currentInfoWindow?.update() } diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt index 85cfb5fec5..5c30e3327e 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt @@ -33,6 +33,7 @@ abstract class AbstractPolygon(private val id: String, options: PolygonOptions) internal var strokePattern = ArrayList(options.strokePattern.orEmpty()) internal var visible: Boolean = options.isVisible internal var clickable: Boolean = options.isClickable + internal var zIndex: Float = options.zIndex internal var tag: IObjectWrapper? = null val annotationOptions: FillOptions @@ -111,12 +112,12 @@ abstract class AbstractPolygon(private val id: String, options: PolygonOptions) override fun getFillColor(): Int = fillColor override fun setZIndex(zIndex: Float) { - Log.d(TAG, "unimplemented Method: setZIndex") + this.zIndex = zIndex + update() } override fun getZIndex(): Float { - Log.d(TAG, "unimplemented Method: getZIndex") - return 0f + return zIndex } override fun setVisible(visible: Boolean) { @@ -212,10 +213,24 @@ class PolygonImpl(private val map: GoogleMapImpl, id: String, options: PolygonOp override fun remove() { removed = true - map.fillManager?.let { update(it) } + map.getFillManagerForZIndex(zIndex)?.let { update(it) } super.remove() } + override fun setZIndex(zIndex: Float) { + val oldZIndex = this.zIndex + if (oldZIndex == zIndex) { + super.setZIndex(zIndex) + return + } + + removed = true + map.getFillManagerForZIndex(zIndex)?.let { update(it) } + super.setZIndex(zIndex) + removed = false + map.getFillManagerForZIndex(zIndex)?.let { update(it) } + } + override fun update() { annotation?.let { it.latLngs = mutableListOf(points.map { it.toMapbox() }).plus(holes.map { it.map { it.toMapbox() } }) @@ -223,7 +238,7 @@ class PolygonImpl(private val map: GoogleMapImpl, id: String, options: PolygonOp it.fillOpacity = if (visible) 1f else 0f it.latLngs = mutableListOf(points.map { it.toMapbox() }).plus(this.holes.map { it.map { it.toMapbox() } }) } - map.fillManager?.let { update(it) } + map.getFillManagerForZIndex(zIndex)?.let { update(it) } } override fun addPolyline(id: String, options: PolylineOptions) { diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt index 404fd50c9c..d2fcab3a46 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt @@ -188,7 +188,7 @@ class PolylineImpl(private val map: GoogleMapImpl, id: String, options: GmsLineO override fun remove() { removed = true - map.lineManager?.let { update(it) } + map.getLineManagerForZIndex(zIndex)?.let { update(it) } } override fun update() { @@ -205,7 +205,21 @@ class PolylineImpl(private val map: GoogleMapImpl, id: String, options: GmsLineO annotations = annotations + it } } - map.lineManager?.let { update(it) } + map.getLineManagerForZIndex(zIndex)?.let { update(it) } + } + + override fun setZIndex(zIndex: Float) { + val oldZIndex = this.zIndex + if (oldZIndex == zIndex) { + super.setZIndex(zIndex) + return + } + + removed = true + map.getLineManagerForZIndex(zIndex)?.let { update(it) } + super.setZIndex(zIndex) + removed = false + map.getLineManagerForZIndex(zIndex)?.let { update(it) } } companion object { diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt index 0c8f5c68cf..4c08bf981f 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt @@ -17,6 +17,7 @@ class TileOverlayImpl(private val map: GoogleMapImpl, private val id: String, op private var visible = options.isVisible private var fadeIn = options.fadeIn private var transparency = options.transparency + private val provider = options.tileProvider override fun remove() { Log.d(TAG, "Not yet implemented: remove") diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparablePair.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparablePair.kt new file mode 100644 index 0000000000..ef423f275b --- /dev/null +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparablePair.kt @@ -0,0 +1,18 @@ +package org.microg.gms.maps.mapbox.utils + +data class ComparablePair, S : Comparable>(val first: T, val second: S) : + Comparable> { + override fun compareTo(other: ComparablePair): Int { + // Lexicographical order + val firstComparison = first.compareTo(other.first) + return if (firstComparison != 0) { + firstComparison + } else { + second.compareTo(other.second) + } + } + + override fun toString(): String { + return "($first, $second)" + } +} \ No newline at end of file From 16982de1dd1dcac980cfacd4550ad69ae3f412f0 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 14 Sep 2025 16:22:57 +0200 Subject: [PATCH 4/6] Respect requested line joint type --- .../org/microg/gms/maps/mapbox/model/Polyline.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt index d2fcab3a46..5352260946 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt @@ -9,11 +9,15 @@ import android.os.Parcel import com.google.android.gms.dynamic.IObjectWrapper import com.google.android.gms.dynamic.ObjectWrapper import com.google.android.gms.maps.model.Cap +import com.google.android.gms.maps.model.JointType import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.PatternItem import com.google.android.gms.maps.model.internal.IPolylineDelegate import com.mapbox.mapboxsdk.plugins.annotation.Line import com.mapbox.mapboxsdk.plugins.annotation.LineOptions +import com.mapbox.mapboxsdk.style.layers.Property.LINE_JOIN_BEVEL +import com.mapbox.mapboxsdk.style.layers.Property.LINE_JOIN_MITER +import com.mapbox.mapboxsdk.style.layers.Property.LINE_JOIN_ROUND import com.mapbox.mapboxsdk.utils.ColorUtils import org.microg.gms.maps.mapbox.GoogleMapImpl import org.microg.gms.maps.mapbox.LiteGoogleMapImpl @@ -39,6 +43,11 @@ abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOpti val baseAnnotationOptions: LineOptions get() = LineOptions() + .withLineJoin(when (jointType) { + JointType.BEVEL -> LINE_JOIN_BEVEL + JointType.DEFAULT -> LINE_JOIN_MITER + else -> LINE_JOIN_ROUND + }) .withLineWidth(width / dpiFactor.invoke()) .withLineColor(ColorUtils.colorToRgbaString(color)) .withLineOpacity(if (visible) 1f else 0f) @@ -111,6 +120,7 @@ abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOpti override fun setJointType(jointType: Int) { this.jointType = jointType + update() } override fun getJointType(): Int = jointType @@ -200,6 +210,7 @@ class PolylineImpl(private val map: GoogleMapImpl, id: String, options: GmsLineO lineWidth = it.options.lineWidth lineColor = it.options.lineColor lineOpacity = it.options.lineOpacity + lineJoin = it.options.lineJoin } } else { annotations = annotations + it From 85a788c8fb7d0dd1d19a2f4429d8d0cdd73ade45 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Tue, 16 Sep 2025 00:22:30 +0200 Subject: [PATCH 5/6] Implement geodesic polylines --- .../microg/gms/maps/mapbox/model/Polyline.kt | 125 +++++++++++++++++- 1 file changed, 123 insertions(+), 2 deletions(-) diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt index 5352260946..e87a52174f 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt @@ -24,7 +24,17 @@ import org.microg.gms.maps.mapbox.LiteGoogleMapImpl import org.microg.gms.maps.mapbox.utils.toMapbox import org.microg.gms.utils.warnOnTransactionIssues import java.util.LinkedList +import kotlin.math.abs +import kotlin.math.acos +import kotlin.math.asin +import kotlin.math.atan2 +import kotlin.math.ceil +import kotlin.math.cos +import kotlin.math.max +import kotlin.math.sin +import kotlin.math.sqrt import com.google.android.gms.maps.model.PolylineOptions as GmsLineOptions +import com.mapbox.mapboxsdk.geometry.LatLng as MapboxLatLng abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOptions, private val dpiFactor: Function0) : IPolylineDelegate.Stub() { internal var points: List = ArrayList(options.points) @@ -92,6 +102,7 @@ abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOpti override fun setGeodesic(geod: Boolean) { this.geodesic = geod + update() } override fun isGeodesic(): Boolean = geodesic @@ -165,6 +176,108 @@ class PolylineImpl(private val map: GoogleMapImpl, id: String, options: GmsLineO override var annotations = computeAnnotations() override var removed: Boolean = false + private fun interpolateGeodesic(points: List): List { + val maxSegmentMeters = 20_000.0 + val curvatureBoost = 0.75 + + if (points.size <= 1) return points.toList() + + val r = 6_371_008.8 // mean Earth radius (meters) + + fun toVec(latDeg: Double, lonDeg: Double): DoubleArray { + val lat = Math.toRadians(latDeg) + val lon = Math.toRadians(lonDeg) + val cl = cos(lat) + return doubleArrayOf(cl * cos(lon), cl * sin(lon), sin(lat)) + } + + fun norm(v: DoubleArray): DoubleArray { + val m = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) + return doubleArrayOf(v[0] / m, v[1] / m, v[2] / m) + } + + fun toLatLng(v: DoubleArray): MapboxLatLng { + val x = v[0]; + val y = v[1]; + val z = v[2] + val lat = asin(z) + val lon = atan2(y, x) + // wrap to [-180,180) + var lonDeg = Math.toDegrees(lon) + lonDeg = ((lonDeg + 540.0) % 360.0) - 180.0 + return MapboxLatLng(Math.toDegrees(lat), lonDeg) + } + + fun centralAngle(a: DoubleArray, b: DoubleArray): Double { + val dot = (a[0] * b[0] + a[1] * b[1] + a[2] * b[2]).coerceIn(-1.0, 1.0) + return acos(dot) + } + + val out = ArrayList(points.size * 4) + + for (i in 0 until points.lastIndex) { + val a = points[i] + val b = points[i + 1] + val va = norm(toVec(a.latitude, a.longitude)) + val vb = norm(toVec(b.latitude, b.longitude)) + val omega = centralAngle(va, vb) + + // Base segment count from distance + val distance = omega * r + var steps = max(1, ceil(distance / maxSegmentMeters).toInt()) + + // Heuristic curvature boost (how "curvy" it *looks* in Web-Mercator) + val meanLatRad = Math.toRadians((a.latitude + b.latitude) / 2.0) + val dLonRad = abs( + // shortest ∆lon across antimeridian + ((Math.toRadians(b.longitude - a.longitude) + Math.PI) % (2 * Math.PI)) - Math.PI + ) + val mercatorCurviness = abs(sin(meanLatRad)) * (dLonRad / Math.PI) // 0..1 + val boost = 1.0 + curvatureBoost * mercatorCurviness + steps = max(1, ceil(steps * boost).toInt()) + + // Emit points along the great-circle (slerp) + val sinOmega = sin(omega) + // Add first point (or skip if already added as previous segment's end) + if (i == 0) out.add(MapboxLatLng(a.latitude, a.longitude)) + + if (omega == 0.0 || sinOmega == 0.0) { + // identical points: skip interpolation + out.add(MapboxLatLng(b.latitude, b.longitude)) + continue + } + + for (k in 1..steps) { + val t = k.toDouble() / steps + val s1 = sin((1 - t) * omega) / sinOmega + val s2 = sin(t * omega) / sinOmega + val vx = s1 * va[0] + s2 * vb[0] + val vy = s1 * va[1] + s2 * vb[1] + val vz = s1 * va[2] + s2 * vb[2] + var p = toLatLng(doubleArrayOf(vx, vy, vz)) + + if (out.isNotEmpty() && abs(p.longitude - out.last().longitude) > 180) { + // Make sure the current point crosses the antimeridian not normalized, + // i.e. going from +179° to +181° instead of +179° to -179°. + // This avoids a long horizontal line across the map. + val lon = if (out.last().longitude > 0) p.longitude + 360 else p.longitude - 360 + p = MapboxLatLng(p.latitude, lon) + } + + // Avoid duplicating the joint point on the next segment + if (k < steps || i == points.lastIndex - 1) { + out.add(p) + } + } + } + return out + } + + private fun List.mapToGeodesicIfNeeded(): List { + if (!geodesic) return this + return interpolateGeodesic(this) + } + private fun computeAnnotations(): List> { val pointsQueue = LinkedList(points) val result = mutableListOf>() @@ -183,13 +296,21 @@ class PolylineImpl(private val map: GoogleMapImpl, id: String, options: GmsLineO .withLineColor(ColorUtils.colorToRgbaString(span.style.color)) .withLineOpacity(if (visible and span.style.isVisible) 1f else 0f) .withLineWidth((span.style.width) / map.dpiFactor) - .withLatLngs(spanPoints.map { it.toMapbox() }) + .withLatLngs( + spanPoints + .map { it.toMapbox() } + .mapToGeodesicIfNeeded() + ) result.add(AnnotationTracker(options)) } if (pointsQueue.isNotEmpty()) { val options = baseAnnotationOptions - .withLatLngs(pointsQueue.map { it.toMapbox() }) + .withLatLngs( + pointsQueue + .map { it.toMapbox() } + .mapToGeodesicIfNeeded() + ) result.add(AnnotationTracker(options)) } From 3c0eca512192a504b6bea4250a0c5cdd0aa22f9e Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 17 May 2026 02:22:03 +0200 Subject: [PATCH 6/6] maps: Implement TileOverlay PoC --- .../org/microg/gms/maps/mapbox/GoogleMap.kt | 99 ++++++++- .../gms/maps/mapbox/model/TileOverlay.kt | 44 +++- .../gms/maps/mapbox/utils/ComparablePair.kt | 18 -- .../gms/maps/mapbox/utils/ComparableTriple.kt | 23 ++ .../utils/TileOverlayRequestInterceptor.kt | 198 ++++++++++++++++++ 5 files changed, 349 insertions(+), 33 deletions(-) delete mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparablePair.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparableTriple.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/TileOverlayRequestInterceptor.kt diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt index 567ab0e39e..6be1c2e57c 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt @@ -93,6 +93,8 @@ import com.mapbox.mapboxsdk.plugins.annotation.OnSymbolDragListener import com.mapbox.mapboxsdk.plugins.annotation.Symbol import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager import com.mapbox.mapboxsdk.style.layers.Property.LINE_CAP_ROUND +import com.mapbox.mapboxsdk.style.layers.RasterLayer +import com.mapbox.mapboxsdk.style.sources.RasterSource import org.microg.gms.maps.mapbox.model.AbstractMarker import org.microg.gms.maps.mapbox.model.BitmapDescriptorFactoryImpl import org.microg.gms.maps.mapbox.model.CircleImpl @@ -108,8 +110,9 @@ import org.microg.gms.maps.mapbox.model.PolygonImpl import org.microg.gms.maps.mapbox.model.PolylineImpl import org.microg.gms.maps.mapbox.model.TileOverlayImpl import org.microg.gms.maps.mapbox.model.getInfoWindowViewFor -import org.microg.gms.maps.mapbox.utils.ComparablePair +import org.microg.gms.maps.mapbox.utils.ComparableTriple import org.microg.gms.maps.mapbox.utils.MultiArchLoader +import org.microg.gms.maps.mapbox.utils.TileOverlayRequestInterceptorModuleProvider import org.microg.gms.maps.mapbox.utils.toGms import org.microg.gms.maps.mapbox.utils.toMapbox import java.util.TreeMap @@ -148,8 +151,13 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG private var cameraMoveStartedListener: IOnCameraMoveStartedListener? = null private var cameraIdleListener: IOnCameraIdleListener? = null private var markerDragListener: IOnMarkerDragListener? = null + private val tileOverlayProvider = TileOverlayRequestInterceptorModuleProvider - private val allocatedZLayers: TreeMap, String> = TreeMap() + private val allocatedZLayers: TreeMap, String> = + TreeMap() + + var rasterLayers: MutableMap, RasterLayer> = mutableMapOf() + var pendingTileOverlays = mutableSetOf>() var lineManagers: MutableMap = mutableMapOf() val pendingLines = mutableSetOf>>() @@ -183,6 +191,13 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG init { BitmapDescriptorFactoryImpl.initialize(mapContext.resources, context.resources) LibraryLoader.setLibraryLoader(MultiArchLoader(mapContext, context)) + + // TODO: figure out how to remove this hack + Mapbox::class.java.getDeclaredField("moduleProvider").apply { + isAccessible = true + set(null, tileOverlayProvider) + } + runOnMainLooper { Mapbox.getInstance(mapContext, BuildConfig.MAPBOX_KEY, WellKnownTileServer.Mapbox) } @@ -406,7 +421,20 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG override fun addTileOverlay(options: TileOverlayOptions): ITileOverlayDelegate? { Log.d(TAG, "unimplemented Method: addTileOverlay") - return TileOverlayImpl(this, "t${tileId++}", options) + val providerId = + tileOverlayProvider.interceptor.registerProvider(options.tileProvider) + val overlay = TileOverlayImpl(this, "t${tileId++}", options, providerId) + + synchronized(this) { + val layer = getOrCreateRasterLayerForZIndex(options.zIndex, providerId) + if (layer == null) { + pendingTileOverlays.add(Triple(options.zIndex, providerId, overlay)) + } else { + overlay.update(layer) + } + } + + return overlay } override fun addCircle(options: CircleOptions): ICircleDelegate { @@ -668,6 +696,57 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG val aboveLayerId: String? ) + private fun getLayerBuilderContext(layerKey: ComparableTriple): LayerBuilderContext { + val belowId = allocatedZLayers.lowerEntry(layerKey)?.value + var aboveId = allocatedZLayers.higherEntry(layerKey)?.value + if (aboveId == belowId) aboveId = null + return LayerBuilderContext(belowId, aboveId) + } + + fun clearTileOverlayProviderCache(providerId: Int) = + tileOverlayProvider.interceptor.clearTileCache(providerId) + + fun removeRasterLayer(zIndex: Float, providerId: Int) { + rasterLayers.remove(zIndex to providerId)?.let { layer -> + synchronized(mapLock) { + map?.getStyle { + it.removeLayer(layer) + it.removeSource(layer.sourceId) + } + allocatedZLayers.remove(ComparableTriple(-zIndex, LayerKind.RASTER, providerId)) + } + } + } + + fun getOrCreateRasterLayerForZIndex(zIndex: Float, providerId: Int): RasterLayer? { + rasterLayers[zIndex to providerId]?.let { return it } + if (mapView == null || map == null || map?.style == null) return null + + synchronized(mapLock) { + val layerKey = ComparableTriple(-zIndex, LayerKind.RASTER, providerId) + val (belowId, aboveId) = getLayerBuilderContext(layerKey) + + val layerId = "raster-${zIndex}-${providerId}" + val tileJsonUrl = tileOverlayProvider.interceptor.getProviderUrl(providerId) + + val layer = RasterLayer(layerId, layerId) + map!!.getStyle { + it.addSource(RasterSource(layerId, tileJsonUrl, 256)) + + if (belowId != null) + it.addLayerBelow(layer, belowId) + else if (aboveId != null) + it.addLayerAbove(layer, aboveId) + else + it.addLayer(layer) + } + + allocatedZLayers[layerKey] = layerId + rasterLayers[zIndex to providerId] = layer + return layer + } + } + private inline fun getOrCreateLayerForZIndexImpl( zIndex: Float, layerTypeMap: MutableMap, @@ -678,14 +757,8 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG if (mapView == null || map == null || map?.style == null) return null synchronized(mapLock) { - val layerKey = ComparablePair(-zIndex, layerKind) - val belowId = allocatedZLayers.lowerEntry(layerKey)?.value - var aboveId = allocatedZLayers.higherEntry(layerKey)?.value - if (aboveId == belowId) aboveId = null - val (newLayer, newLayerId) = LayerBuilderContext( - belowId, - aboveId - ).layerBuilder() + val layerKey = ComparableTriple(-zIndex, layerKind, 0) + val (newLayer, newLayerId) = getLayerBuilderContext(layerKey).layerBuilder() allocatedZLayers[layerKey] = newLayerId layerTypeMap[zIndex] = newLayer @@ -891,6 +964,10 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG mapView?.let { view -> if (loaded) return@let + pendingTileOverlays.forEach { (zIndex, providerId, overlay) -> + overlay.update(getOrCreateRasterLayerForZIndex(zIndex, providerId)!!) + } + pendingTileOverlays.clear() pendingFills.forEach { (zIndex, fill) -> fill.update(getFillManagerForZIndex(zIndex)!!) } diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt index 4c08bf981f..f82fc2b9d3 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt @@ -9,34 +9,55 @@ import android.os.Parcel import android.util.Log import com.google.android.gms.maps.model.TileOverlayOptions import com.google.android.gms.maps.model.internal.ITileOverlayDelegate +import com.mapbox.mapboxsdk.style.layers.Property +import com.mapbox.mapboxsdk.style.layers.PropertyFactory.rasterFadeDuration +import com.mapbox.mapboxsdk.style.layers.PropertyFactory.rasterOpacity +import com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility +import com.mapbox.mapboxsdk.style.layers.RasterLayer import org.microg.gms.maps.mapbox.GoogleMapImpl import org.microg.gms.utils.warnOnTransactionIssues -class TileOverlayImpl(private val map: GoogleMapImpl, private val id: String, options: TileOverlayOptions) : ITileOverlayDelegate.Stub() { +class TileOverlayImpl( + private val map: GoogleMapImpl, + private val id: String, + options: TileOverlayOptions, + private val providerId: Int +) : ITileOverlayDelegate.Stub() { private var zIndex = options.zIndex private var visible = options.isVisible private var fadeIn = options.fadeIn private var transparency = options.transparency - private val provider = options.tileProvider override fun remove() { - Log.d(TAG, "Not yet implemented: remove") + Log.d(TAG, "remove") + visible = false + map.removeRasterLayer(zIndex, providerId) } override fun clearTileCache() { - Log.d(TAG, "Not yet implemented: clearTileCache") + Log.d(TAG, "clearTileCache") + map.removeRasterLayer(zIndex, providerId) + map.clearTileOverlayProviderCache(providerId) + update() } override fun getId(): String = id override fun setZIndex(zIndex: Float) { + Log.d(TAG, "setZIndex: $zIndex") + if (zIndex == this.zIndex) return + + map.removeRasterLayer(this.zIndex, providerId) this.zIndex = zIndex + update() } override fun getZIndex(): Float = zIndex override fun setVisible(visible: Boolean) { + Log.d(TAG, "setVisible: $visible") this.visible = visible + update() } override fun isVisible(): Boolean = visible @@ -47,18 +68,33 @@ class TileOverlayImpl(private val map: GoogleMapImpl, private val id: String, op override fun setFadeIn(fadeIn: Boolean) { this.fadeIn = fadeIn + update() } override fun getFadeIn(): Boolean = fadeIn override fun setTransparency(transparency: Float) { + Log.d(TAG, "setTransparency: $transparency") this.transparency = transparency + update() } override fun getTransparency(): Float = transparency override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } + fun update() { + map.getOrCreateRasterLayerForZIndex(zIndex, providerId)?.let { update(it) } + } + + fun update(layer: RasterLayer) { + layer.setProperties( + visibility(if (visible) Property.VISIBLE else Property.NONE), + rasterOpacity(1 - transparency), + rasterFadeDuration(if (fadeIn) 300f else 0f) + ) + } + companion object { private const val TAG = "TileOverlay" } diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparablePair.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparablePair.kt deleted file mode 100644 index ef423f275b..0000000000 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparablePair.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.microg.gms.maps.mapbox.utils - -data class ComparablePair, S : Comparable>(val first: T, val second: S) : - Comparable> { - override fun compareTo(other: ComparablePair): Int { - // Lexicographical order - val firstComparison = first.compareTo(other.first) - return if (firstComparison != 0) { - firstComparison - } else { - second.compareTo(other.second) - } - } - - override fun toString(): String { - return "($first, $second)" - } -} \ No newline at end of file diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparableTriple.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparableTriple.kt new file mode 100644 index 0000000000..db89401fd1 --- /dev/null +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/ComparableTriple.kt @@ -0,0 +1,23 @@ +package org.microg.gms.maps.mapbox.utils + +data class ComparableTriple, S : Comparable, R : Comparable>( + val first: T, + val second: S, + val third: R +) : + Comparable> { + override fun compareTo(other: ComparableTriple): Int { + // Lexicographical order + return if (first.compareTo(other.first) != 0) { + first.compareTo(other.first) + } else if (second.compareTo(other.second) != 0) { + second.compareTo(other.second) + } else { + third.compareTo(other.third) + } + } + + override fun toString(): String { + return "($first, $second, $third)" + } +} \ No newline at end of file diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/TileOverlayRequestInterceptor.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/TileOverlayRequestInterceptor.kt new file mode 100644 index 0000000000..e8d1d46cba --- /dev/null +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/TileOverlayRequestInterceptor.kt @@ -0,0 +1,198 @@ +package org.microg.gms.maps.mapbox.utils + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Log +import android.util.LruCache +import com.google.android.gms.maps.model.TileProvider +import com.mapbox.mapboxsdk.ModuleProviderImpl +import com.mapbox.mapboxsdk.http.HttpRequest +import com.mapbox.mapboxsdk.http.HttpResponder +import java.io.ByteArrayOutputStream +import java.util.regex.Pattern + +class TileOverlayRequestInterceptor(val actualHttpClient: HttpRequest) : + HttpRequest by actualHttpClient { + + companion object { + private const val TAG = "TileOvHttpReq" + private const val CACHE_SIZE = 100 + val TILEJSON_URL_REGEX: Pattern = + Pattern.compile("^https://maplibre-tile-overlay-provider\\.invalid-domain\\.microg\\.org/provider/(\\d+)/tiles.json(?:[?].*)?$") + val TILES_URL_REGEX: Pattern = + Pattern.compile("^https://maplibre-tile-overlay-provider\\.invalid-domain\\.microg\\.org/provider/(\\d+)/tiles/(\\d+)/(\\d+)/(\\d+)\\.(\\w+)(?:[?].*)?$") + } + + private val tileCaches = mutableListOf>() + val tileProviders = mutableListOf() + + fun registerProvider(tileProvider: TileProvider): Int { + synchronized(tileProviders) { + val id = tileProviders.size + tileProviders.add(tileProvider) + tileCaches.add(LruCache(CACHE_SIZE)) + return id + } + } + + fun clearTileCache(providerId: Int) { + synchronized(tileProviders) { + tileCaches.getOrNull(providerId)?.evictAll() + } + } + + fun getProviderUrl(providerId: Int): String { + return "https://maplibre-tile-overlay-provider.invalid-domain.microg.org/provider/$providerId/tiles.json?nocache=${System.currentTimeMillis()}" + } + + // minzoom and maxzoom set to extremely high values should ensure the zoom level is capped only + // by the map's global min/max zoom level + private fun getTileJson(providerId: Int) = """ + { + "tilejson": "3.0.0", + "tiles": [ + "https://maplibre-tile-overlay-provider.invalid-domain.microg.org/provider/${providerId}/tiles/{z}/{x}/{y}.png?nocache=${System.currentTimeMillis()}" + ], + "minzoom": 0, + "maxzoom": 100, + "name": "gmaps-overlay-${providerId}", + "scheme": "xyz" + } + """.trimIndent().trim().toByteArray() + + private fun isPng(data: ByteArray): Boolean = + data.size >= 8 && + data[0] == 0x89.toByte() && data[1] == 0x50.toByte() && + data[2] == 0x4E.toByte() && data[3] == 0x47.toByte() + + override fun executeRequest( + httpRequest: HttpResponder, + nativePtr: Long, + resourceUrl: String, + dataRange: String, + etag: String, + offlineUsage: Boolean + ) { + TILEJSON_URL_REGEX.matcher(resourceUrl).apply { + if (matches()) { + Log.d(TAG, "Intercepting TileJSON request: $resourceUrl") + + val providerId = group(1)?.toIntOrNull() + if (providerId == null) { + httpRequest.handleFailure(400, "Invalid tile URL") + return + } + if (providerId >= tileProviders.size) { + httpRequest.handleFailure(404, "Tile provider not found") + return + } + httpRequest.onResponse( + 200, + null, + null, + "no-store", // prevent MapLibre from caching TileJSON in SQLite + null, + null, + null, + getTileJson(providerId) + ) + return + } + } + + TILES_URL_REGEX.matcher(resourceUrl).apply { + if (matches()) { + Log.d(TAG, "Intercepting tile request: $resourceUrl") + val providerId = group(1)?.toIntOrNull() + val z = group(2)?.toIntOrNull() + val x = group(3)?.toIntOrNull() + val y = group(4)?.toIntOrNull() + val ext = group(5) + + if (z == null || y == null || x == null || providerId == null || ext != "png") { + httpRequest.handleFailure(400, "Invalid tile URL") + return + } + + // Check in-wrapper cache first + val cacheKey = "$z/$x/$y" + val cached: ByteArray? + synchronized(tileProviders) { + cached = tileCaches.getOrNull(providerId)?.get(cacheKey) + } + if (cached != null) { + Log.d(TAG, "Serving tile from cache: $cacheKey (provider $providerId)") + httpRequest.onResponse(200, null, null, "no-store", null, null, null, cached) + return + } + + val tileProvider: TileProvider + synchronized(tileProviders) { + if (providerId >= tileProviders.size) { + httpRequest.handleFailure(404, "Tile provider not found") + return + } + tileProvider = tileProviders[providerId] + } + + val tile = tileProvider.getTile(x, y, z) + if (tile == TileProvider.NO_TILE) { + httpRequest.handleFailure(404, "Tile not found") + return + } + if (tile == null || tile.data == null || tile.data.size == 0) { + httpRequest.handleFailure(502, "App provided no tile data") + Log.w( + TAG, + "Tile provider returned a tile with null data for ($x, $y) (zoom: $z)" + ) + return + } + + val image = if (isPng(tile.data)) { + tile.data + } else { + ByteArrayOutputStream().apply { + BitmapFactory.decodeByteArray(tile.data, 0, tile.data.size) + .compress(Bitmap.CompressFormat.PNG, 100, this) + }.toByteArray() + } + + synchronized(tileProviders) { + tileCaches.getOrNull(providerId)?.put(cacheKey, image) + null + } + + httpRequest.onResponse( + 200, + null, + null, + "no-store", // prevent MapLibre from caching tile in SQLite + null, + null, + null, + image + ) + return + } else { + Log.d(TAG, "Forwarding regular HTTP request: $resourceUrl") + return actualHttpClient.executeRequest( + httpRequest, + nativePtr, + resourceUrl, + dataRange, + etag, + offlineUsage + ) + } + } + } +} + +object TileOverlayRequestInterceptorModuleProvider : ModuleProviderImpl() { + val interceptor by lazy { + TileOverlayRequestInterceptor(super.createHttpRequest()) + } + + override fun createHttpRequest(): HttpRequest = interceptor +}