diff options
Diffstat (limited to 'android/lib')
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( |
