summaryrefslogtreecommitdiffhomepage
path: root/android/lib
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2025-08-29 14:51:10 +0200
committerDavid Göransson <david.goransson@mullvad.net>2025-09-01 08:34:16 +0200
commite02d460164fe09d5cf085cdcb3f40a9251bae633 (patch)
treecbad368a9d30876f24631a6548e7d32e2e2c9524 /android/lib
parent4f03db1de3224d666cf6fc5ab255f3629435a59a (diff)
downloadmullvadvpn-interactive-maps.tar.xz
mullvadvpn-interactive-maps.zip
Add interactive mapsinteractive-maps
Diffstat (limited to 'android/lib')
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt8
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt5
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt42
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt11
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/LocationMarkerColors.kt2
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt3
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/Constants.kt6
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt101
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLSurfaceView.kt10
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt47
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LatLong.kt2
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt1
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Ray.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Sphere.kt7
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Vector3.kt89
-rw-r--r--android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/map/Vector3Test.kt69
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt4
17 files changed, 382 insertions, 28 deletions
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt
index 140cf5aafb..4c5613c2c4 100644
--- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt
@@ -46,6 +46,9 @@ import net.mullvad.mullvadvpn.lib.model.GenericOptions
import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.IpVersion
+import net.mullvad.mullvadvpn.lib.model.LatLong
+import net.mullvad.mullvadvpn.lib.model.Latitude
+import net.mullvad.mullvadvpn.lib.model.Longitude
import net.mullvad.mullvadvpn.lib.model.Mtu
import net.mullvad.mullvadvpn.lib.model.ObfuscationEndpoint
import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
@@ -580,6 +583,11 @@ internal fun ManagementInterface.RelayListCity.toDomain(
.filter { it.endpointData.hasWireguard() }
.map { it.toDomain(cityCode) }
.sortedWith(RelayNameComparator),
+ latLong =
+ LatLong(
+ Latitude.fromFloat(latitude.toFloat()),
+ Longitude.fromFloat(longitude.toFloat()),
+ ),
)
}
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt
index 2e0b2fcf05..0963ec1aa1 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt
@@ -88,16 +88,17 @@ fun animatedCameraPosition(
return CameraPosition(
zoom = baseZoom * zoomOutMultiplier.value,
+ verticalBias = cameraVerticalBias,
latLong =
LatLong(
Latitude(latitudeAnimation.value),
Longitude.fromFloat(longitudeAnimation.value),
),
- verticalBias = cameraVerticalBias,
+ // verticalBias = cameraVerticalBias,
)
}
-private fun Float.toAnimationDurationMillis(): Int =
+fun Float.toAnimationDurationMillis(): Int =
(this * DISTANCE_DURATION_SCALE_FACTOR)
.toInt()
.coerceIn(MIN_ANIMATION_MILLIS, MAX_ANIMATION_MILLIS)
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt
index 05b8d59701..76e84dca98 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt
@@ -1,19 +1,25 @@
package net.mullvad.mullvadvpn.lib.map
+import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
+import co.touchlab.kermit.Logger
import net.mullvad.mullvadvpn.lib.map.data.CameraPosition
import net.mullvad.mullvadvpn.lib.map.data.GlobeColors
import net.mullvad.mullvadvpn.lib.map.data.MapViewState
import net.mullvad.mullvadvpn.lib.map.data.Marker
import net.mullvad.mullvadvpn.lib.map.internal.MapGLSurfaceView
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.LatLong
+import net.mullvad.mullvadvpn.lib.model.RelayItemId
@Composable
fun Map(
@@ -21,9 +27,11 @@ fun Map(
cameraLocation: CameraPosition,
markers: List<Marker>,
globeColors: GlobeColors,
+ onClickRelayItemId: (GeoLocationId) -> Unit,
+ onLongClickRelayItemId: (Offset, GeoLocationId) -> Unit,
) {
val mapViewState = MapViewState(cameraLocation, markers, globeColors)
- Map(modifier = modifier, mapViewState = mapViewState)
+ Map(modifier = modifier, mapViewState = mapViewState, onClickRelayItemId, onLongClickRelayItemId)
}
@Composable
@@ -34,6 +42,8 @@ fun AnimatedMap(
cameraVerticalBias: Float,
markers: List<Marker>,
globeColors: GlobeColors,
+ onClickRelayItemId: (RelayItemId) -> Unit,
+ onLongClickRelayItemId: (Offset, RelayItemId) -> Unit
) {
Map(
modifier = modifier,
@@ -45,12 +55,18 @@ fun AnimatedMap(
),
markers = markers,
globeColors,
+ onClickRelayItemId = onClickRelayItemId,
+ onLongClickRelayItemId = onLongClickRelayItemId,
)
}
@Composable
-internal fun Map(modifier: Modifier = Modifier, mapViewState: MapViewState) {
-
+internal fun Map(
+ modifier: Modifier = Modifier,
+ mapViewState: MapViewState,
+ onClickRelayItemId: (GeoLocationId) -> Unit,
+ onLongClickRelayItemId: (Offset, GeoLocationId) -> Unit,
+) {
var view: MapGLSurfaceView? = remember { null }
val lifeCycleState = LocalLifecycleOwner.current.lifecycle
@@ -76,7 +92,25 @@ internal fun Map(modifier: Modifier = Modifier, mapViewState: MapViewState) {
}
}
- AndroidView(modifier = modifier, factory = { MapGLSurfaceView(it) }) { glSurfaceView ->
+ AndroidView(
+ modifier =
+ Modifier.pointerInput(lifeCycleState) {
+ detectTapGestures(
+ onTap = {
+ Logger.i("Registered marker click: $it")
+ val result = view?.onMapClick(it) ?: return@detectTapGestures
+ onClickRelayItemId(result.first)
+ },
+ onLongPress = {
+ Logger.i("Registered marker long click")
+ val result = view?.onMapClick(it) ?: return@detectTapGestures
+
+ onLongClickRelayItemId(result.second, result.first)
+ },
+ )
+ }.then(modifier),
+ factory = { MapGLSurfaceView(it) },
+ ) { glSurfaceView ->
view = glSurfaceView
glSurfaceView.setData(mapViewState)
}
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt
index b66b0ea657..e8ac13f793 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt
@@ -4,4 +4,13 @@ import androidx.compose.runtime.Immutable
import net.mullvad.mullvadvpn.lib.model.LatLong
@Immutable
-data class CameraPosition(val latLong: LatLong, val zoom: Float, val verticalBias: Float)
+data class CameraPosition(
+ val latLong: LatLong,
+ val zoom: Float,
+ val verticalBias: Float,
+ val fov: Float = DEFAULT_FIELD_OF_VIEW,
+) {
+ companion object {
+ const val DEFAULT_FIELD_OF_VIEW = 70f
+ }
+}
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/LocationMarkerColors.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/LocationMarkerColors.kt
index 4e0959912b..b5fc439e6e 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/LocationMarkerColors.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/LocationMarkerColors.kt
@@ -8,7 +8,7 @@ data class LocationMarkerColors(
val centerColor: Color,
val ringBorderColor: Color = Color.White,
val shadowColor: Color = Color.Black.copy(alpha = DEFAULT_SHADOW_ALPHA),
- val perimeterColors: Color = centerColor.copy(alpha = DEFAULT_PERIMETER_ALPHA),
+ val perimeterColors: Color? = centerColor.copy(alpha = DEFAULT_PERIMETER_ALPHA),
) {
companion object {
private const val DEFAULT_SHADOW_ALPHA = 0.55f
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt
index a7f25ec545..aae3bb991e 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt
@@ -1,6 +1,8 @@
package net.mullvad.mullvadvpn.lib.map.data
+import android.os.Parcelable
import androidx.compose.runtime.Immutable
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.LatLong
@Immutable
@@ -8,6 +10,7 @@ data class Marker(
val latLong: LatLong,
val size: Float = DEFAULT_MARKER_SIZE,
val colors: LocationMarkerColors,
+ val id: GeoLocationId? = null,
) {
companion object {
private const val DEFAULT_MARKER_SIZE = 0.02f
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/Constants.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/Constants.kt
index a0aacc34c6..292abcf06a 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/Constants.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/Constants.kt
@@ -12,9 +12,9 @@ internal const val MIN_ANIMATION_MILLIS = 1300
internal const val MAX_ANIMATION_MILLIS = 2500
// The cut off where we go from a short animation (camera pans) to a far animation (camera pans +
// zoom out)
-internal const val SHORT_ANIMATION_CUTOFF_MILLIS = 1700
+const val SHORT_ANIMATION_CUTOFF_MILLIS = 1700
// Multiplier for the zoom out animation
-internal const val FAR_ANIMATION_MAX_ZOOM_MULTIPLIER = 1.30f
+const val FAR_ANIMATION_MAX_ZOOM_MULTIPLIER = 1.80f
// When in the far animation we reach the MAX_ZOOM_MULTIPLIER, value is between 0 and 1
-internal const val MAX_MULTIPLIER_PEAK_TIMING = .35f
+const val MAX_MULTIPLIER_PEAK_TIMING = .35f
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt
index b767d894b7..40ffc7c61e 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt
@@ -5,19 +5,34 @@ import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import androidx.collection.LruCache
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import co.touchlab.kermit.Logger
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
+import kotlin.math.pow
+import kotlin.math.sqrt
import kotlin.math.tan
import net.mullvad.mullvadvpn.lib.map.data.CameraPosition
import net.mullvad.mullvadvpn.lib.map.data.LocationMarkerColors
import net.mullvad.mullvadvpn.lib.map.data.MapViewState
+import net.mullvad.mullvadvpn.lib.map.data.Marker
import net.mullvad.mullvadvpn.lib.map.internal.shapes.Globe
import net.mullvad.mullvadvpn.lib.map.internal.shapes.LocationMarker
+import net.mullvad.mullvadvpn.lib.model.map.Ray
+import net.mullvad.mullvadvpn.lib.model.map.Sphere
+import net.mullvad.mullvadvpn.lib.model.map.Vector3
+import net.mullvad.mullvadvpn.lib.model.map.rotateAroundX
+import net.mullvad.mullvadvpn.lib.model.map.rotateAroundY
+import net.mullvad.mullvadvpn.lib.model.map.toLatLng
+import net.mullvad.mullvadvpn.lib.model.map.toVector3
import net.mullvad.mullvadvpn.lib.model.toRadians
internal class MapGLRenderer(private val resources: Resources) : GLSurfaceView.Renderer {
private lateinit var globe: Globe
+ private var viewPortSize: Size = Size(0f, 0f)
+ private val radius: Float = 1f
// Due to location markers themselves containing colors we cache them to avoid recreating them
// for every draw call.
@@ -34,6 +49,8 @@ internal class MapGLRenderer(private val resources: Resources) : GLSurfaceView.R
}
private lateinit var viewState: MapViewState
+ private val projectionMatrix = newIdentityMatrix()
+ private var globalViewMatrix = newIdentityMatrix()
override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
globe = Globe(resources)
@@ -51,8 +68,6 @@ internal class MapGLRenderer(private val resources: Resources) : GLSurfaceView.R
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
}
- private val projectionMatrix = newIdentityMatrix()
-
override fun onDrawFrame(gl10: GL10) {
// Clear canvas
clear()
@@ -67,6 +82,7 @@ internal class MapGLRenderer(private val resources: Resources) : GLSurfaceView.R
Matrix.rotateM(viewMatrix, 0, viewState.cameraPosition.latLong.latitude.value, 1f, 0f, 0f)
Matrix.rotateM(viewMatrix, 0, viewState.cameraPosition.latLong.longitude.value, 0f, -1f, 0f)
+ globalViewMatrix = viewMatrix.copyOf()
globe.draw(projectionMatrix, viewMatrix, viewState.globeColors)
// Draw location markers
@@ -82,8 +98,9 @@ internal class MapGLRenderer(private val resources: Resources) : GLSurfaceView.R
private fun toOffsetY(cameraPosition: CameraPosition): Float {
val percent = cameraPosition.verticalBias
val z = cameraPosition.zoom - 1f
+
// Calculate the size of the plane at the current z position
- val planeSizeY = tan(FIELD_OF_VIEW.toRadians() / 2f) * z * 2f
+ val planeSizeY = tan(cameraPosition.fov.toRadians() / 2f) * z * 2f
// Calculate the start of the plane
val planeStartY = planeSizeY / 2f
@@ -105,6 +122,8 @@ internal class MapGLRenderer(private val resources: Resources) : GLSurfaceView.R
override fun onSurfaceChanged(unused: GL10, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
+ viewPortSize = Size(width.toFloat(), height.toFloat())
+
val ratio: Float = width.toFloat() / height.toFloat()
if (ratio.isFinite()) {
@@ -121,6 +140,82 @@ internal class MapGLRenderer(private val resources: Resources) : GLSurfaceView.R
fun setViewState(viewState: MapViewState) {
this.viewState = viewState
+ markerVector = viewState.locationMarker.map { it.latLong.toVector3() to it }.toMap()
+ }
+
+ var markerVector = mapOf<Vector3, Marker>()
+
+ fun closestMarker(offset: Offset): Pair<Marker?, Float>? {
+ val cameraz = -viewState.cameraPosition.zoom
+ val camerax = 0f
+ val cameray = toOffsetY(viewState.cameraPosition)
+
+ val camera = Vector3(camerax, cameray, cameraz)
+
+ val sphere = Sphere(Vector3(0f, 0f, 0f), 1f)
+ val ratio: Float = viewPortSize.width.toFloat() / viewPortSize.height.toFloat()
+
+ val directionVector =
+ calculateDirectionVector(
+ viewState.cameraPosition.fov,
+ ratio,
+ viewPortSize.width,
+ viewPortSize.height,
+ offset.x,
+ offset.y,
+ nearPlaneDistance = PERSPECTIVE_Z_NEAR,
+ )
+
+ val ray = Ray(camera, directionVector)
+
+ val oc = ray.origin - sphere.center // Vector from ray origin to sphere center
+ val a = ray.direction.dot(ray.direction)
+ val b = 2f * oc.dot(ray.direction)
+ val c = oc.dot(oc) - sphere.radius.pow(2f)
+ val discriminant = b.pow(2f) - 4f * a * c
+
+ if (discriminant < 0f) {
+ return null // No intersection
+ } else {
+ val t = (-b - sqrt(discriminant)) / (2f * a) // Closest intersection point
+ val t2 = (-b + sqrt(discriminant)) / (2f * a) // Closest intersection point
+ Logger.d("Intersection t1: $t, t2: $t2")
+ val point2 = ray.origin + ray.direction * t2
+
+ val newPosition =
+ point2
+ .rotateAroundX(-viewState.cameraPosition.latLong.latitude.value)
+ .rotateAroundY(viewState.cameraPosition.latLong.longitude.value)
+
+ Logger.d("Intersection point2: $point2")
+ Logger.d("Intersection real vector: $newPosition")
+ Logger.d("Clicked lat lng: ${newPosition.toLatLng()}")
+
+ val closestMarker = markerVector.minByOrNull { it.key.distanceTo(newPosition) }
+
+ if (closestMarker != null) {
+ return closestMarker.value to closestMarker.key.distanceTo(newPosition)
+ }
+
+ return null
+ }
+ }
+
+ fun calculateDirectionVector(
+ fovy: Float,
+ aspectRatio: Float,
+ viewportWidth: Float,
+ viewportHeight: Float,
+ tapScreenX: Float,
+ tapScreenY: Float,
+ nearPlaneDistance: Float = 1.0f,
+ ): Vector3 {
+ val halfHeight = tan(fovy.toRadians() / 2.0f) * nearPlaneDistance
+ val halfWidth = halfHeight * aspectRatio
+ val x = (2.0f * tapScreenX / viewportWidth - 1.0f) * halfWidth
+ val y = (1.0f - 2.0f * tapScreenY / viewportHeight) * halfHeight
+ val z = -nearPlaneDistance
+ return Vector3(x, y, z).normalize()
}
companion object {
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLSurfaceView.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLSurfaceView.kt
index 19dd085524..3607cca820 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLSurfaceView.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLSurfaceView.kt
@@ -2,8 +2,11 @@ package net.mullvad.mullvadvpn.lib.map.internal
import android.content.Context
import android.opengl.GLSurfaceView
+import androidx.compose.ui.geometry.Offset
import net.mullvad.mullvadvpn.lib.map.BuildConfig
import net.mullvad.mullvadvpn.lib.map.data.MapViewState
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.RelayItemId
internal class MapGLSurfaceView(context: Context) : GLSurfaceView(context) {
@@ -28,4 +31,11 @@ internal class MapGLSurfaceView(context: Context) : GLSurfaceView(context) {
renderer.setViewState(viewState)
requestRender()
}
+
+ fun onMapClick(offset: Offset): Pair<GeoLocationId, Offset>? {
+ val (marker, distance) = renderer.closestMarker(offset) ?: return null
+ if (distance < 0.02f) {
+ return marker?.id?.let { it to offset }
+ } else return null
+ }
}
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt
index 26e69416b9..c12209273c 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt
@@ -54,14 +54,17 @@ internal class LocationMarker(val colors: LocationMarkerColors) {
val modelViewMatrix = viewMatrix.copyOf()
GLES20.glUseProgram(shaderProgram)
-
Matrix.rotateM(modelViewMatrix, 0, latLong.longitude.value, 0f, 1f, 0f)
Matrix.rotateM(modelViewMatrix, 0, latLong.latitude.value, -1f, 0f, 0f)
Matrix.scaleM(modelViewMatrix, 0, size, size, 1f)
// Translate marker to put it above the globe
- Matrix.translateM(modelViewMatrix, 0, 0f, 0f, MARKER_TRANSLATE_Z_FACTOR)
+ if (colors.perimeterColors != null) {
+ Matrix.translateM(modelViewMatrix, 0, 0f, 0f, MARKER_TRANSLATE_Z_FACTOR + 0.0003f)
+ } else {
+ Matrix.translateM(modelViewMatrix, 0, 0f, 0f, MARKER_TRANSLATE_Z_FACTOR)
+ }
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, positionBuffer)
GLES20.glVertexAttribPointer(
@@ -148,37 +151,53 @@ internal class LocationMarker(val colors: LocationMarkerColors) {
}
@Suppress("MagicNumber")
- private fun createRings(): List<Ring> =
- listOf(
- circleFanVertices(
- 32,
- 0.5f,
- floatArrayOf(0.0f, 0.0f, 0.0f),
- colors.perimeterColors,
- colors.perimeterColors,
- ), // Semi-transparent outer
+ private fun createRings(): List<Ring> = buildList {
+ colors.perimeterColors?.let {
+ // Semi-transparent outer
+ add(
+ circleFanVertices(
+ 32,
+ 0.5f,
+ floatArrayOf(0.0f, 0.0f, 0.0f),
+ colors.perimeterColors,
+ colors.perimeterColors,
+ )
+ )
+ }
+
+ // Shadow
+ add(
circleFanVertices(
16,
0.28f,
floatArrayOf(0.0f, -0.05f, 0.00001f),
colors.shadowColor,
colors.shadowColor.copy(alpha = 0.0f),
- ), // Shadow
+ )
+ )
+
+ // White ring
+ add(
circleFanVertices(
32,
0.185f,
floatArrayOf(0.0f, 0.0f, 0.00002f),
colors.ringBorderColor,
colors.ringBorderColor,
- ), // White ring
+ )
+ )
+
+ // Center colored circle
+ add(
circleFanVertices(
32,
0.15f,
floatArrayOf(0.0f, 0.0f, 0.00003f),
colors.centerColor,
colors.centerColor,
- ), // Center colored circle
+ )
)
+ }
fun onRemove() {
GLES20.glDeleteBuffers(2, intArrayOf(positionBuffer, colorBuffer), 0)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LatLong.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LatLong.kt
index 19f757ffc3..a3cbf13761 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LatLong.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LatLong.kt
@@ -46,3 +46,5 @@ data class LatLong(val latitude: Latitude, val longitude: Longitude) {
const val COMPLETE_ANGLE = 360f
fun Float.toRadians() = this * Math.PI.toFloat() / (COMPLETE_ANGLE / 2)
+
+fun Float.toDegrees() = this * ((COMPLETE_ANGLE / 2) / Math.PI.toFloat())
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt
index b1df67fea6..3ea0b48cbb 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt
@@ -71,6 +71,7 @@ sealed interface RelayItem {
override val id: GeoLocationId.City,
override val name: String,
val relays: List<Relay>,
+ val latLong: LatLong,
) : Location {
override val active = relays.any { it.active }
override val hasChildren: Boolean = relays.isNotEmpty()
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Ray.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Ray.kt
new file mode 100644
index 0000000000..2e8eb236c8
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Ray.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model.map
+
+data class Ray(val origin: Vector3, val direction: Vector3)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Sphere.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Sphere.kt
new file mode 100644
index 0000000000..73a759e938
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Sphere.kt
@@ -0,0 +1,7 @@
+package net.mullvad.mullvadvpn.lib.model.map
+
+data class Sphere(val center: Vector3, val radius: Float) {
+ companion object {
+ const val RADIUS = 1f
+ }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Vector3.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Vector3.kt
new file mode 100644
index 0000000000..e1c3901a11
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/map/Vector3.kt
@@ -0,0 +1,89 @@
+package net.mullvad.mullvadvpn.lib.model.map
+
+import kotlin.math.acos
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.math.sqrt
+import net.mullvad.mullvadvpn.lib.model.LatLong
+import net.mullvad.mullvadvpn.lib.model.Latitude
+import net.mullvad.mullvadvpn.lib.model.Longitude
+import net.mullvad.mullvadvpn.lib.model.toDegrees
+import net.mullvad.mullvadvpn.lib.model.toRadians
+
+data class Vector3(val x: Float, val y: Float, val z: Float) {
+ fun dot(other: Vector3): Float {
+ return x * other.x + y * other.y + z * other.z
+ }
+
+ operator fun minus(other: Vector3): Vector3 {
+ return Vector3(x - other.x, y - other.y, z - other.z)
+ }
+
+ operator fun times(scalar: Float): Vector3 {
+ return Vector3(x * scalar, y * scalar, z * scalar)
+ }
+
+ operator fun plus(other: Vector3): Vector3 {
+ return Vector3(x + other.x, y + other.y, z + other.z)
+ }
+
+ fun normalize(): Vector3 {
+ val length = sqrt(x * x + y * y + z * z)
+ return Vector3(x / length, y / length, z / length)
+ }
+
+ fun distanceTo(other: Vector3): Float {
+ val dx = x - other.x
+ val dy = y - other.y
+ val dz = z - other.z
+ return sqrt(dx * dx + dy * dy + dz * dz)
+ }
+}
+
+fun Vector3.rotateAroundX(degrees: Float): Vector3 {
+ val radians = degrees.toRadians()
+ val cosTheta = cos(radians)
+ val sinTheta = sin(radians)
+
+ val newY = cosTheta * y - sinTheta * z
+ val newZ = sinTheta * y + cosTheta * z
+
+ return Vector3(x, -newY, newZ)
+}
+
+fun Vector3.rotateAroundY(degrees: Float): Vector3 {
+ val radians = degrees.toRadians()
+ val cosTheta = cos(radians)
+ val sinTheta = sin(radians)
+
+ val newX = cosTheta * x + sinTheta * z
+ val newZ = -sinTheta * x + cosTheta * z
+
+ return Vector3(newX, y, -newZ)
+}
+
+fun Vector3.toLatLng(): LatLong {
+ // phi
+ val lat = acos(y / Sphere.RADIUS)
+
+ // theta
+ val lon = atan2(x, z)
+
+ return LatLong(
+ // This worked for some reason (camera starts at lat 90!)
+ Latitude.fromFloat(90f - lat.toDegrees()),
+ Longitude.fromFloat(-lon.toDegrees()),
+ )
+}
+
+fun LatLong.toVector3(): Vector3 {
+ val phi = this.latitude.value.toRadians()
+ val theta = this.longitude.value.toRadians()
+
+ val x = -Sphere.RADIUS * cos(phi) * sin(theta)
+ val y = Sphere.RADIUS * sin(phi)
+ val z = Sphere.RADIUS * cos(phi) * cos(theta)
+
+ return Vector3(x, y, z)
+}
diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/map/Vector3Test.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/map/Vector3Test.kt
new file mode 100644
index 0000000000..c9f35692bf
--- /dev/null
+++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/map/Vector3Test.kt
@@ -0,0 +1,69 @@
+package net.mullvad.mullvadvpn.lib.model.map
+
+import kotlin.test.assertEquals
+import net.mullvad.mullvadvpn.lib.model.LatLong
+import net.mullvad.mullvadvpn.lib.model.Latitude
+import net.mullvad.mullvadvpn.lib.model.Longitude
+import org.junit.jupiter.api.Test
+
+class Vector3Test {
+ @Test
+ fun `Y-axis center test`() {
+ assertVector3Equals(Y_POSITIVE_CENTER.toVector3(), Vector3(0f, 1f, 0f))
+ assertVector3Equals(Y_NEGATIVE_CENTER.toVector3(), Vector3(0f, -1f, 0f))
+ }
+
+ @Test
+ fun `Z-axis center test`() {
+ assertVector3Equals(Z_POSITIVE_CENTER.toVector3(), Vector3(0f, 0f, 1f))
+ assertVector3Equals(Z_NEGATIVE_CENTER.toVector3(), Vector3(0f, 0f, -1f))
+ }
+
+ @Test
+ fun `X-axis center test`() {
+ assertVector3Equals(X_POSITIVE_CENTER.toVector3(), Vector3(-1f, 0f, 0f))
+ assertVector3Equals(X_NEGATIVE_CENTER.toVector3(), Vector3(1f, 0f, 0f))
+ }
+
+ @Test
+ fun `Y-axis center to LatLng test`() {
+ assertLatLngEquals(Vector3(0f, 1f, 0f).toLatLng(), Y_POSITIVE_CENTER)
+ assertLatLngEquals(Vector3(0f, -1f, 0f).toLatLng(), Y_NEGATIVE_CENTER)
+ }
+
+ @Test
+ fun `Z-axis center to LatLng test`() {
+ assertLatLngEquals(Vector3(0f, 0f, 1f).toLatLng(), Z_POSITIVE_CENTER)
+ assertLatLngEquals(Vector3(0f, 0f, -1f).toLatLng(), Z_NEGATIVE_CENTER)
+ }
+
+ @Test
+ fun `X-axis center to LatLng test`() {
+ assertLatLngEquals(Vector3(-1f, 0f, 0f).toLatLng(), X_POSITIVE_CENTER)
+ assertLatLngEquals(Vector3(1f, 0f, 0f).toLatLng(), X_NEGATIVE_CENTER)
+ }
+
+ companion object {
+ // NORTH POLE
+ val Y_POSITIVE_CENTER = LatLong(Latitude(90f), Longitude(0f))
+ // SOUTH POLE
+ val Y_NEGATIVE_CENTER = LatLong(Latitude(-90f), Longitude(0f))
+
+ val Z_POSITIVE_CENTER = LatLong(Latitude(0f), Longitude(0f))
+ val Z_NEGATIVE_CENTER = LatLong(Latitude(0f), Longitude.fromFloat(180f))
+
+ val X_NEGATIVE_CENTER = LatLong(Latitude(0f), Longitude.fromFloat(-90f))
+ val X_POSITIVE_CENTER = LatLong(Latitude(0f), Longitude.fromFloat(90f))
+ }
+
+ fun assertVector3Equals(expected: Vector3, actual: Vector3) {
+ assertEquals(expected.x, actual.x, 0.0001f)
+ assertEquals(expected.y, actual.y, 0.0001f)
+ assertEquals(expected.z, actual.z, 0.0001f)
+ }
+
+ fun assertLatLngEquals(expected: LatLong, actual: LatLong) {
+ assertEquals(expected.latitude.distanceTo(actual.latitude), 0f, 0.0001f)
+ assertEquals(expected.longitude.distanceTo(actual.longitude), 0f, 0.0001f)
+ }
+}
diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt
index 724c567595..e28f45478d 100644
--- a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt
@@ -1,6 +1,9 @@
package net.mullvad.mullvadvpn.lib.ui.component.relaylist
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.LatLong
+import net.mullvad.mullvadvpn.lib.model.Latitude
+import net.mullvad.mullvadvpn.lib.model.Longitude
import net.mullvad.mullvadvpn.lib.model.Ownership
import net.mullvad.mullvadvpn.lib.model.ProviderId
import net.mullvad.mullvadvpn.lib.model.RelayItem
@@ -37,6 +40,7 @@ private fun generateRelayItemCity(
active,
)
},
+ latLong = LatLong(Latitude.fromFloat(0f), Longitude.fromFloat(0f)),
)
private fun generateRelayItemRelay(