Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 68d2d2c8 authored by Fynn Godau's avatar Fynn Godau
Browse files

Approximate circles using a polygon and a line

parent 4bdbc20f
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -26,6 +26,8 @@ dependencies {
    implementation("org.maplibre.gl:android-plugin-annotation-v9:1.0.0") {
        exclude group: 'com.google.android.gms'
    }
    implementation 'org.maplibre.gl:android-sdk-turf:5.9.0'

    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
}

+15 −21
Original line number Diff line number Diff line
@@ -98,17 +98,13 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
    private var markerDragListener: IOnMarkerDragListener? = null

    var lineManager: LineManager? = null
    val pendingLines = mutableSetOf<PolylineImpl>()
    val pendingLines = mutableSetOf<Markup<Line, LineOptions>>()
    var lineId = 0L

    var fillManager: FillManager? = null
    val pendingFills = mutableSetOf<PolygonImpl>()
    val pendingFills = mutableSetOf<Markup<Fill, FillOptions>>()
    var fillId = 0L

    var circleManager: CircleManager? = null
    val pendingCircles = mutableSetOf<CircleImpl>()
    var circleId = 0L

    var symbolManager: SymbolManager? = null
    val pendingMarkers = mutableSetOf<MarkerImpl>()
    val markers = mutableMapOf<Long, MarkerImpl>()
@@ -302,20 +298,25 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
    }

    override fun addCircle(options: CircleOptions): ICircleDelegate? {
        val circle = CircleImpl(this, "c${circleId++}", options)
        val circle = CircleImpl(this, "c${fillId++}", options)
        synchronized(this) {
            val circleManager = circleManager
            if (circleManager == null) {
                pendingCircles.add(circle)
            val fillManager = fillManager
            if (fillManager == null) {
                pendingFills.add(circle)
            } else {
                circle.update(fillManager)
            }
            val lineManager = lineManager
            if (lineManager == null) {
                pendingLines.add(circle.line)
            } else {
                circle.update(circleManager)
                circle.line.update(lineManager)
            }
        }
        return circle
    }

    override fun clear() {
        circleManager?.let { clear(it) }
        lineManager?.let { clear(it) }
        fillManager?.let { clear(it) }
        symbolManager?.let { clear(it) }
@@ -342,12 +343,10 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
    }

    fun applyMapType() {
        val circles = circleManager?.annotations?.values()
        val lines = lineManager?.annotations?.values()
        val fills = fillManager?.annotations?.values()
        val symbols = symbolManager?.annotations?.values()
        val update: (Style) -> Unit = {
            circles?.let { runCatching { circleManager?.update(it) } }
            lines?.let { runCatching { lineManager?.update(it) } }
            fills?.let { runCatching { fillManager?.update(it) } }
            symbols?.let { runCatching { symbolManager?.update(it) } }
@@ -649,11 +648,9 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
                if (loaded) return@let
                val symbolManager: SymbolManager
                val lineManager: LineManager
                val circleManager: CircleManager
                val fillManager: FillManager

                synchronized(mapLock) {
                    circleManager = CircleManager(view, map, it)
                    fillManager = FillManager(view, map, it)
                    symbolManager = SymbolManager(view, map, it)
                    lineManager = LineManager(view, map, it)
@@ -661,7 +658,6 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)

                    this.symbolManager = symbolManager
                    this.lineManager = lineManager
                    this.circleManager = circleManager
                    this.fillManager = fillManager
                }
                symbolManager.iconAllowOverlap = true
@@ -691,15 +687,15 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
                    }

                    override fun onAnnotationDragFinished(annotation: Symbol?) {
                        mapView?.post {
                        try {
                            markers[annotation?.id]?.let { markerDragListener?.onMarkerDragEnd(it) }
                        } catch (e: Exception) {
                            Log.w(TAG, e)
                        }
                        }
                    }
                })
                pendingCircles.forEach { it.update(circleManager) }
                pendingCircles.clear()
                pendingFills.forEach { it.update(fillManager) }
                pendingFills.clear()
                pendingLines.forEach { it.update(lineManager) }
@@ -743,8 +739,6 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
    override fun onPause() = mapView?.onPause() ?: Unit
    override fun onDestroy() {
        Log.d(TAG, "destroy")
        circleManager?.onDestroy()
        circleManager = null

        lineManager?.onDestroy()
        lineManager = null
+129 −29
Original line number Diff line number Diff line
@@ -20,76 +20,177 @@ import android.os.Parcel
import android.util.Log
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.internal.ICircleDelegate
import com.mapbox.mapboxsdk.plugins.annotation.Circle
import com.mapbox.mapboxsdk.plugins.annotation.CircleOptions
import com.mapbox.geojson.LineString
import com.mapbox.geojson.Point
import com.mapbox.mapboxsdk.plugins.annotation.*
import com.mapbox.mapboxsdk.utils.ColorUtils
import com.mapbox.turf.TurfConstants
import com.mapbox.turf.TurfMeasurement
import com.mapbox.turf.TurfMeta
import com.mapbox.turf.TurfTransformation
import org.microg.gms.maps.mapbox.GoogleMapImpl
import org.microg.gms.maps.mapbox.utils.toMapbox
import com.google.android.gms.maps.model.CircleOptions as GmsCircleOptions

class CircleImpl(private val map: GoogleMapImpl, private val id: String, options: GmsCircleOptions) : ICircleDelegate.Stub(), Markup<Circle, CircleOptions> {
val NORTH_POLE = Point.fromLngLat(0.0, 90.0)
val SOUTH_POLE = Point.fromLngLat(0.0, -90.0)

/**
 * Amount of points to be used in the polygon that approximates the circle.
 */
const val CIRCLE_POLYGON_STEPS = 256


class CircleImpl(private val map: GoogleMapImpl, private val id: String, options: GmsCircleOptions) : ICircleDelegate.Stub(), Markup<Fill, FillOptions> {
    private var center: LatLng = options.center
    private var radius: Double = options.radius
    private var radius: Double = options.radius // in meters
    private var strokeWidth: Float = options.strokeWidth
    private var strokeColor: Int = options.strokeColor
    private var fillColor: Int = options.fillColor
    private var visible: Boolean = options.isVisible

    override var annotation: Circle? = null
    internal val line: Markup<Line, LineOptions> = object : Markup<Line, LineOptions> {
        override var annotation: Line? = null
        override val annotationOptions: LineOptions
            get() = LineOptions()
                .withGeometry(
                    LineString.fromLngLats(
                        makeOutlineLatLngs()
                    )
                ).withLineWidth(strokeWidth / map.dpiFactor)
                .withLineColor(ColorUtils.colorToRgbaString(strokeColor))
                .withLineOpacity(if (visible) 1f else 0f)

        override val removed: Boolean = false
    }

    override var annotation: Fill? = null
    override var removed: Boolean = false
    override val annotationOptions: CircleOptions
        get() = CircleOptions()
                .withLatLng(center.toMapbox())
                .withCircleColor(ColorUtils.colorToRgbaString(fillColor))
                .withCircleRadius(radius.toFloat())
                .withCircleStrokeColor(ColorUtils.colorToRgbaString(strokeColor))
                .withCircleStrokeWidth(strokeWidth / map.dpiFactor)
                .withCircleOpacity(if (visible) 1f else 0f)
                .withCircleStrokeOpacity(if (visible) 1f else 0f)
    override val annotationOptions: FillOptions
        get() =
            FillOptions()
                .withGeometry(makePolygon())
                .withFillColor(ColorUtils.colorToRgbaString(fillColor))
                .withFillOutlineColor(ColorUtils.colorToRgbaString(strokeColor))
                .withFillOpacity(if (visible && !wrapsAroundPoles()) 1f else 0f)

    private fun makePolygon() = TurfTransformation.circle(
        Point.fromLngLat(center.longitude, center.latitude), radius, CIRCLE_POLYGON_STEPS, TurfConstants.UNIT_METERS
    )

    /**
     * Google's "map renderer is unable to draw the circle fill if
     * the circle encompasses either the North or South pole".
     */
    private fun wrapsAroundPoles() = Point.fromLngLat(center.longitude, center.latitude).let {
        TurfMeasurement.distance(
            it, NORTH_POLE
        ) * 1000 < radius || TurfMeasurement.distance(
            it, SOUTH_POLE
        ) * 1000 < radius
    }

    private fun makeOutlineLatLngs() =
        TurfMeta.coordAll(
            makePolygon(), wrapsAroundPoles()
        ).let {
            // Circles around the poles are tricky to draw (https://github.com/mapbox/mapbox-gl-js/issues/11235).
            // We modify our lines such to match the way Mapbox / MapLibre draws them.
            // This results in a small gap somewhere in the line, but avoids an incorrect horizontal line.

            val centerPoint = Point.fromLngLat(center.longitude, center.latitude)

            if (!centerPoint.equals(NORTH_POLE) && TurfMeasurement.distance(centerPoint, NORTH_POLE) * 1000 < radius) {
                // Wraps around North Pole
                for (i in 0 until it.size) {
                    // We want to have the north-most points at the start and end
                    if (it[0].latitude() > it[1].latitude() && it[it.size-1].latitude() > it[it.size-2].latitude()) {
                        return@let it
                    } else {
                        // Cycle point list
                        val zero = it.removeFirst()
                        it.add(zero)
                    }
                }
            }

            if (!centerPoint.equals(SOUTH_POLE) && TurfMeasurement.distance(centerPoint, SOUTH_POLE) * 1000 < radius) {
                // Wraps around South Pole
                for (i in 0 until it.size) {
                    // We want to have the south-most points at the start and end
                    if (it[0].latitude() < it[1].latitude() && it[it.size-1].latitude() < it[it.size-2].latitude()) {
                        return@let it
                    } else {
                        // Cycle point list
                        val last = it.removeAt(it.size - 1)
                        it.add(0, last)
                    }
                }
            }

            it
        }

    private fun updateLatLngs() {
        val polygon = makePolygon()

        // Extracts points from generated polygon in expected format
        annotation?.latLngs = FillOptions().withGeometry(polygon).latLngs

        line.annotation?.latLngs = makeOutlineLatLngs().map { point ->
            com.mapbox.mapboxsdk.geometry.LatLng(
                point.latitude(),
                point.longitude()
            )
        }

        if (wrapsAroundPoles()) {
            annotation?.fillOpacity = 0f
        }
    }

    override fun remove() {
        removed = true
        map.circleManager?.let { update(it) }
        map.fillManager?.let { update(it) }
    }

    override fun getId(): String = id

    override fun setCenter(center: LatLng) {
        this.center = center
        annotation?.latLng = center.toMapbox()
        map.circleManager?.let { update(it) }
        updateLatLngs()
        map.fillManager?.let { update(it) }
    }

    override fun getCenter(): LatLng = center

    override fun setRadius(radius: Double) {
        this.radius = radius
        annotation?.circleRadius = radius.toFloat()
        map.circleManager?.let { update(it) }
        updateLatLngs()
        map.fillManager?.let { update(it) }
    }

    override fun getRadius(): Double = radius

    override fun setStrokeWidth(width: Float) {
        this.strokeWidth = width
        annotation?.circleStrokeWidth = width / map.dpiFactor
        map.circleManager?.let { update(it) }
        line.annotation?.lineWidth = width / map.dpiFactor
        map.lineManager?.let { line.update(it) }
    }

    override fun getStrokeWidth(): Float = strokeWidth

    override fun setStrokeColor(color: Int) {
        this.strokeColor = color
        annotation?.setCircleStrokeColor(color)
        map.circleManager?.let { update(it) }
        line.annotation?.setLineColor(color)
        map.lineManager?.let { line.update(it) }
    }

    override fun getStrokeColor(): Int = strokeColor

    override fun setFillColor(color: Int) {
        this.fillColor = color
        annotation?.setCircleColor(color)
        map.circleManager?.let { update(it) }
        annotation?.setFillColor(color)
        map.fillManager?.let { update(it) }
    }

    override fun getFillColor(): Int = fillColor
@@ -105,9 +206,8 @@ class CircleImpl(private val map: GoogleMapImpl, private val id: String, options

    override fun setVisible(visible: Boolean) {
        this.visible = visible
        annotation?.circleOpacity = if (visible) 1f else 0f
        annotation?.circleStrokeOpacity = if (visible) 1f else 0f
        map.circleManager?.let { update(it) }
        annotation?.fillOpacity = if (visible && !wrapsAroundPoles()) 1f else 0f
        map.fillManager?.let { update(it) }
    }

    override fun isVisible(): Boolean = visible