Tap on locations in timeline to see a larger map

Show a fully-featured MapView, centered on the dropped pin,
which allows panning/zooming. Share button allows opening
in a map application.

Supports showing a description at the top of the screen,
if one is supplied with the event.

Out of scope: showing the local user's location (being
done as a separate story).

Includes some minor tidying: remove duplicate Location,
and make GeoURI parsing a method on that class; fix the
pointer location in MapView (I broke it earlier, whoops!)
This commit is contained in:
Chris Smith 2023-07-04 12:04:41 +01:00
parent ae054b7130
commit a2140ff282
21 changed files with 659 additions and 44 deletions

View file

@ -16,19 +16,25 @@
package io.element.android.features.location.api
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
private const val GEO_URI_REGEX = """geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?"""
@Parcelize
data class Location(
val lat: Double,
val lon: Double,
val accuracy: Float,
)
fun parseGeoUri(geoUri: String): Location? {
val result = Regex(GEO_URI_REGEX).matchEntire(geoUri) ?: return null
return Location (
lat = result.groups["latitude"]?.value?.toDoubleOrNull() ?: return null,
lon = result.groups["longitude"]?.value?.toDoubleOrNull() ?: return null,
accuracy = result.groups["uncertainty"]?.value?.toFloatOrNull() ?: 0f,
)
) : Parcelable {
companion object {
fun fromGeoUri(geoUri: String): Location? {
val result = Regex(GEO_URI_REGEX).matchEntire(geoUri) ?: return null
return Location(
lat = result.groups["latitude"]?.value?.toDoubleOrNull() ?: return null,
lon = result.groups["longitude"]?.value?.toDoubleOrNull() ?: return null,
accuracy = result.groups["uncertainty"]?.value?.toFloatOrNull() ?: 0f,
)
}
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.location.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
interface ViewLocationEntryPoint : FeatureEntryPoint {
data class Inputs(val location: Location, val description: String?) : NodeInputs
fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node
}

View file

@ -19,57 +19,57 @@ package io.element.android.features.location.api
import com.google.common.truth.Truth.assertThat
import org.junit.Test
internal class GeoUrisKtTest {
internal class LocationKtTest {
@Test
fun `parseGeoUri - returns null for invalid urls`() {
assertThat(parseGeoUri("")).isNull()
assertThat(parseGeoUri("http://example.com/")).isNull()
assertThat(parseGeoUri("geo:")).isNull()
assertThat(parseGeoUri("geo:1.234")).isNull()
assertThat(parseGeoUri("geo:1.234,")).isNull()
assertThat(parseGeoUri("geo:,1.234")).isNull()
assertThat(parseGeoUri("notgeo:1.234,5.678")).isNull()
assertThat(parseGeoUri("geo:+1.234,5.678")).isNull()
assertThat(parseGeoUri("geo:+1.234,*5.678")).isNull()
assertThat(parseGeoUri("geo:not,good")).isNull()
assertThat(parseGeoUri("geo:1.234,5.678;u=wrong")).isNull()
assertThat(parseGeoUri("geo:1.234,5.678trailing")).isNull()
assertThat(Location.fromGeoUri("")).isNull()
assertThat(Location.fromGeoUri("http://example.com/")).isNull()
assertThat(Location.fromGeoUri("geo:")).isNull()
assertThat(Location.fromGeoUri("geo:1.234")).isNull()
assertThat(Location.fromGeoUri("geo:1.234,")).isNull()
assertThat(Location.fromGeoUri("geo:,1.234")).isNull()
assertThat(Location.fromGeoUri("notgeo:1.234,5.678")).isNull()
assertThat(Location.fromGeoUri("geo:+1.234,5.678")).isNull()
assertThat(Location.fromGeoUri("geo:+1.234,*5.678")).isNull()
assertThat(Location.fromGeoUri("geo:not,good")).isNull()
assertThat(Location.fromGeoUri("geo:1.234,5.678;u=wrong")).isNull()
assertThat(Location.fromGeoUri("geo:1.234,5.678trailing")).isNull()
}
@Test
fun `parseGeoUri - returns location for valid urls`() {
assertThat(parseGeoUri("geo:1.234,5.678")).isEqualTo(Location(
assertThat(Location.fromGeoUri("geo:1.234,5.678")).isEqualTo(Location(
lat = 1.234,
lon = 5.678,
accuracy = 0f,
))
assertThat(parseGeoUri("geo:1,5")).isEqualTo(Location(
assertThat(Location.fromGeoUri("geo:1,5")).isEqualTo(Location(
lat = 1.0,
lon = 5.0,
accuracy = 0f,
))
assertThat(parseGeoUri("geo:1.234,5.678;u=3000")).isEqualTo(Location(
assertThat(Location.fromGeoUri("geo:1.234,5.678;u=3000")).isEqualTo(Location(
lat = 1.234,
lon = 5.678,
accuracy = 3000f,
))
assertThat(parseGeoUri("geo:1,5;u=3000")).isEqualTo(Location(
assertThat(Location.fromGeoUri("geo:1,5;u=3000")).isEqualTo(Location(
lat = 1.0,
lon = 5.0,
accuracy = 3000f,
))
assertThat(parseGeoUri("geo:-1.234,-5.678;u=9.10")).isEqualTo(Location(
assertThat(Location.fromGeoUri("geo:-1.234,-5.678;u=9.10")).isEqualTo(Location(
lat = -1.234,
lon = -5.678,
accuracy = 9.10f,
))
assertThat(parseGeoUri("geo:-1,-5;u=9.10")).isEqualTo(Location(
assertThat(Location.fromGeoUri("geo:-1,-5;u=9.10")).isEqualTo(Location(
lat = -1.0,
lon = -5.0,
accuracy = 9.10f,