From 882f75864cff4a6a9ee666e5aa7f83620350e8c3 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Tue, 27 Jun 2023 09:23:00 +0200 Subject: [PATCH] Set up SDK & reusable map view component (#476) Adds `libraries/map` which contains some initial building blocks that will be used by the location sharing feature. Ref: https://github.com/vector-im/element-meta/issues/1684 --- build.gradle.kts | 1 + features/location/api/build.gradle.kts | 53 ++++ .../android/features/location/api/Location.kt | 26 ++ .../android/features/location/api/MapView.kt | 297 ++++++++++++++++++ .../features/location/api/StaticMapView.kt | 135 ++++++++ .../location/api/internal/MapsUtils.kt | 80 +++++ .../api/internal/StaticMapPlaceholder.kt | 111 +++++++ .../main/res/drawable/blurred_map_dark.png | Bin 0 -> 34121 bytes .../main/res/drawable/blurred_map_light.png | Bin 0 -> 48957 bytes .../api/src/main/res/drawable/pin.xml | 23 ++ .../api/internal/BuildStaticMapsApiUrlTest.kt | 71 +++++ features/location/fake/build.gradle.kts | 52 +++ .../location/fake/LocationUpdatesFlowFake.kt | 35 +++ features/location/impl/build.gradle.kts | 54 ++++ .../impl/src/main/AndroidManifest.xml | 21 ++ .../location/impl/LocationUpdatesFlowImpl.kt | 96 ++++++ gradle/libs.versions.toml | 3 + ...erDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...erDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...rLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...rLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...ViewDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...iewLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ViewDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...iewLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + 25 files changed, 1082 insertions(+) create mode 100644 features/location/api/build.gradle.kts create mode 100644 features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt create mode 100644 features/location/api/src/main/kotlin/io/element/android/features/location/api/MapView.kt create mode 100644 features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt create mode 100644 features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapsUtils.kt create mode 100644 features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt create mode 100644 features/location/api/src/main/res/drawable/blurred_map_dark.png create mode 100644 features/location/api/src/main/res/drawable/blurred_map_light.png create mode 100644 features/location/api/src/main/res/drawable/pin.xml create mode 100644 features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/BuildStaticMapsApiUrlTest.kt create mode 100644 features/location/fake/build.gradle.kts create mode 100644 features/location/fake/src/main/kotlin/io/element/android/features/location/fake/LocationUpdatesFlowFake.kt create mode 100644 features/location/impl/build.gradle.kts create mode 100644 features/location/impl/src/main/AndroidManifest.xml create mode 100644 features/location/impl/src/main/kotlin/io/element/android/features/location/impl/LocationUpdatesFlowImpl.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewLightPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/build.gradle.kts b/build.gradle.kts index 240af6c9ae..b0bcde29a8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -247,6 +247,7 @@ koverMerged { excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*" excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState" excludes += "io.element.android.features.messages.impl.media.local.LocalMediaViewState" + excludes += "io.element.android.features.location.api.MapState" } bound { minValue = 90 diff --git a/features/location/api/build.gradle.kts b/features/location/api/build.gradle.kts new file mode 100644 index 0000000000..a2f73e7def --- /dev/null +++ b/features/location/api/build.gradle.kts @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 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. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.ksp) +} + +android { + namespace = "io.element.android.features.location.api" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.network) + implementation(projects.libraries.core) + implementation(projects.libraries.uiStrings) + implementation(libs.maplibre) + implementation(libs.network.retrofit) + implementation(libs.maplibre.annotation) + implementation(libs.coil.compose) + implementation(libs.serialization.json) + implementation(libs.accompanist.permission) + ksp(libs.showkase.processor) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) +} diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt new file mode 100644 index 0000000000..596bc4d1a0 --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt @@ -0,0 +1,26 @@ +/* + * 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 + +/** + * Represents a location sample emitted by the device's location subsystem. + */ +data class Location( + val lat: Double, + val lon: Double, + val accuracy: Float, +) diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/MapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/MapView.kt new file mode 100644 index 0000000000..464422d713 --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/MapView.kt @@ -0,0 +1,297 @@ +/* + * 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 android.annotation.SuppressLint +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.LocationOn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.mapbox.mapboxsdk.Mapbox +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.maps.MapView +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager +import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions +import io.element.android.features.location.api.internal.buildTileServerUrl +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.FloatingActionButton +import io.element.android.libraries.designsystem.theme.components.Icon +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +/** + * Composable wrapper around MapLibre's [MapView]. + */ +@SuppressLint("MissingPermission") +@Composable +fun MapView( + modifier: Modifier = Modifier, + mapState: MapState = rememberMapState(), + darkMode: Boolean = !ElementTheme.colors.isLight, + onLocationClick: () -> Unit, +) { + // When in preview, early return a Box with the received modifier preserving layout + if (LocalInspectionMode.current) { + @Suppress("ModifierReused") // False positive, the modifier is not reused due to the early return. + Box(modifier = modifier) + return + } + + val context = LocalContext.current + val mapView = remember { + Mapbox.getInstance(context) + MapView(context) + } + var mapRefs by remember { mutableStateOf(null) } + + // Build map + LaunchedEffect(darkMode) { + mapView.awaitMap().let { map -> + map.uiSettings.apply { + isCompassEnabled = false + } + map.setStyle(buildTileServerUrl(darkMode = darkMode)) { style -> + mapRefs = MapRefs( + map = map, + symbolManager = SymbolManager(mapView, map, style).apply { + iconAllowOverlap = true + }, + style = style + ) + } + } + } + + // Update state position when moving map + DisposableEffect(mapRefs) { + var listener: MapboxMap.OnCameraIdleListener? = null + + mapRefs?.let { mapRefs -> + listener = MapboxMap.OnCameraIdleListener { + mapRefs.map.cameraPosition.target?.let { target -> + val position = MapState.CameraPosition( + lat = target.latitude, + lon = target.longitude, + zoom = mapRefs.map.cameraPosition.zoom + ) + mapState.position = position + Timber.d("Camera moved to: $position") + } + }.apply { + mapRefs.map.addOnCameraIdleListener(this) + Timber.d("Added OnCameraIdleListener $this") + } + } + + onDispose { + mapRefs?.let { mapRefs -> + listener?.let { + mapRefs.map.removeOnCameraIdleListener(it).apply { + Timber.d("Removed OnCameraIdleListener $it") + } + } + } + } + } + + // Move map to given position when state has changed + LaunchedEffect(mapRefs, mapState.position) { + mapRefs?.map?.moveCamera( + CameraUpdateFactory.newCameraPosition( + CameraPosition.Builder() + .target(LatLng(mapState.position.lat, mapState.position.lon)) + .zoom(mapState.position.zoom).build() + ) + ) + Timber.d("Camera position updated to: ${mapState.position}") + } + + // Draw pin + LaunchedEffect(mapRefs, mapState.location) { + mapRefs?.let { mapRefs -> + mapState.location?.let { location -> + context.getDrawable(R.drawable.pin)?.let { mapRefs.style.addImage("pin", it) } + mapRefs.symbolManager.create( + SymbolOptions() + .withLatLng(LatLng(location.lat, location.lon)) + .withIconImage("pin") + .withIconSize(1.3f) + ) + Timber.d("Shown pin at location: $location") + } + } + + } + + // Draw markers + LaunchedEffect(mapRefs, mapState.markers) { + mapRefs?.let { mapRefs -> + mapState.markers.forEachIndexed { index, marker -> + context.getDrawable(marker.drawable)?.let { mapRefs.style.addImage("marker_$index", it) } + mapRefs.symbolManager.create( + SymbolOptions() + .withLatLng(LatLng(marker.lat, marker.lon)) + .withIconImage("marker_$index") + .withIconSize(1.0f) + ) + Timber.d("Shown marker at location: $marker") + } + } + } + + @Suppress("ModifierReused") + Box(modifier = modifier) { + AndroidView(factory = { mapView }) + FloatingActionButton( + onClick = onLocationClick, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), + ) { + Icon( + imageVector = Icons.Filled.LocationOn, + contentDescription = null, // TODO + ) + } + } +} + +@Composable +fun rememberMapState( + position: MapState.CameraPosition = MapState.CameraPosition(lat = 0.0, lon = 0.0, zoom = 0.0), + location: Location? = null, + markers: ImmutableList = emptyList().toImmutableList(), +): MapState = remember { + MapState( + position = position, + location = location, + markers = markers, + ) +} // TODO(Use remember saveable with Parcelable custom saver) + +@Stable +class MapState( + position: CameraPosition, // The position of the camera, it's what will be shared + location: Location? = null, // The location retrieved by the location subsystem, if any. + markers: ImmutableList = emptyList().toImmutableList(), // The pin's location, if any. +) { + var position: CameraPosition by mutableStateOf(position) + var location: Location? by mutableStateOf(location) + var markers: ImmutableList by mutableStateOf(markers) + + override fun toString(): String { + return "MapState(position=$position, location=$location, markers=$markers)" + } + + @Stable + data class CameraPosition( + val lat: Double, + val lon: Double, + val zoom: Double, + ) + + @Stable + data class Marker( + @DrawableRes val drawable: Int, + val lat: Double, + val lon: Double, + ) +} + +private class MapRefs( + val map: MapboxMap, + val symbolManager: SymbolManager, + val style: Style +) + +/** + * A suspending function that provides an instance of [MapboxMap] from this [MapView]. This is + * an alternative to [MapView.getMapAsync] by using coroutines to obtain the [MapboxMap]. + * + * Inspired from [com.google.maps.android.ktx.awaitMap] + * + * @return the [MapboxMap] instance + */ +private suspend inline fun MapView.awaitMap(): MapboxMap = + suspendCoroutine { continuation -> + getMapAsync { + continuation.resume(it) + } + } + +@Preview +@Composable +fun MapViewLightPreview() = + ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun MapViewDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + MapView( + modifier = Modifier.size(400.dp), + mapState = rememberMapState( + position = MapState.CameraPosition( + lat = 0.0, + lon = 0.0, + zoom = 0.0, + ), + location = Location( + lat = 0.0, + lon = 0.0, + accuracy = 0.0f, + ), + markers = listOf( + MapState.Marker( + drawable = R.drawable.pin, + lat = 0.0, + lon = 0.0, + ) + ).toImmutableList() + ), + onLocationClick = {}, + ) +} diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt new file mode 100644 index 0000000000..e68c42368f --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt @@ -0,0 +1,135 @@ +/* + * 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 androidx.compose.foundation.Image +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImagePainter +import coil.compose.rememberAsyncImagePainter +import coil.request.ImageRequest +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.features.location.api.internal.StaticMapPlaceholder +import io.element.android.features.location.api.internal.buildStaticMapsApiUrl +import timber.log.Timber + +/** + * Shows a static map image downloaded via a third party service's static maps API. + */ +@Composable +fun StaticMapView( + lat: Double, + lon: Double, + zoom: Double, + contentDescription: String?, + modifier: Modifier = Modifier, + darkMode: Boolean = !ElementTheme.colors.isLight, +) { + // Using BoxWithConstraints to: + // 1) Size the inner Image to the same Dp size of the outer BoxWithConstraints. + // 2) Request the static map image of the exact required size in Px to fill the AsyncImage. + BoxWithConstraints( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + var retryHash by remember { mutableStateOf(0) } + val painter = rememberAsyncImagePainter( + model = if (constraints.isZero) { + // Avoid building a URL if any of the size constraints is zero (else it will thrown an exception). + null + } else { + ImageRequest.Builder(LocalContext.current) + .data( + buildStaticMapsApiUrl( + lat = lat, + lon = lon, + desiredZoom = zoom, + desiredWidth = constraints.maxWidth, + desiredHeight = constraints.maxHeight, + darkMode = darkMode, + ) + ) + .size(width = constraints.maxWidth, height = constraints.maxHeight) + .setParameter("retry_hash", retryHash, memoryCacheKey = null) + .build() + }.apply { + Timber.d("Static map image request: ${this?.data}") + } + ) + + if (painter.state is AsyncImagePainter.State.Success) { + Image( + painter = painter, + contentDescription = contentDescription, + modifier = Modifier.size(width = maxWidth, height = maxHeight), + // The returned image can be smaller than the requested size due to the static maps API having + // a max width and height of 2048 px. See buildStaticMapsApiUrl() for more details. + // We apply ContentScale.Fit to scale the image to fill the AsyncImage should this be the case. + contentScale = ContentScale.Fit, + ) + Icon( + resourceId = R.drawable.pin, + contentDescription = null, + tint = Color.Unspecified + ) + } else { + StaticMapPlaceholder( + showProgress = painter.state is AsyncImagePainter.State.Loading, + contentDescription = contentDescription, + modifier = Modifier.size(width = maxWidth, height = maxHeight), + darkMode = darkMode, + onLoadMapClick = { retryHash++ } + ) + } + } +} + +@Preview +@Composable +fun StaticMapViewLightPreview() = + ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun StaticMapViewDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + StaticMapView( + lat = 0.0, + lon = 0.0, + zoom = 0.0, + contentDescription = null, + modifier = Modifier.size(400.dp), + ) +} diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapsUtils.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapsUtils.kt new file mode 100644 index 0000000000..f5a15e46c6 --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapsUtils.kt @@ -0,0 +1,80 @@ +/* + * 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.internal + +import kotlin.math.roundToInt + +private const val API_KEY = "fU3vlMsMn4Jb6dnEIFsx" +private const val BASE_URL = "https://api.maptiler.com" +private const val LIGHT_MAP_ID = "9bc819c8-e627-474a-a348-ec144fe3d810" +private const val DARK_MAP_ID = "dea61faf-292b-4774-9660-58fcef89a7f3" +private const val STATIC_MAP_FORMAT = "webp" +private const val STATIC_MAP_SCALE = "" // Either "" (empty string) for normal image or "@2x" for retina images. +private const val STATIC_MAP_MAX_WIDTH_HEIGHT = 2048 +private const val STATIC_MAP_MAX_ZOOM = 22.0 + +internal fun buildTileServerUrl( + darkMode: Boolean +): String = if (!darkMode) { + "$BASE_URL/maps/$LIGHT_MAP_ID/style.json?key=$API_KEY" +} else { + "$BASE_URL/maps/$DARK_MAP_ID/style.json?key=$API_KEY" +} + +/** + * Builds a valid URL for maptiler.com static map api based on the given params. + * + * Coerces width and height to the API maximum of 2048 keeping the requested aspect ratio. + * Coerces zoom to the API maximum of 22. + * + * NB: This will throw if either width or height are <= 0. You need to handle this case upstream + * (hint: views can't have negative width or height but can have 0 width or height sometimes). + */ +internal fun buildStaticMapsApiUrl( + lat: Double, + lon: Double, + desiredZoom: Double, + desiredWidth: Int, + desiredHeight: Int, + darkMode: Boolean +): String { + require(desiredWidth > 0 && desiredHeight > 0) { + "Width ($desiredHeight) and height ($desiredHeight) must be > 0" + } + require(desiredZoom >= 0) { "Zoom ($desiredZoom) must be >= 0" } + val zoom = desiredZoom.coerceAtMost(STATIC_MAP_MAX_ZOOM) // API will error if outside 0-22 range. + val width: Int + val height: Int + if (desiredWidth <= STATIC_MAP_MAX_WIDTH_HEIGHT && desiredHeight <= STATIC_MAP_MAX_WIDTH_HEIGHT) { + width = desiredWidth + height = desiredHeight + } else { + val aspectRatio = desiredWidth.toDouble() / desiredHeight.toDouble() + if (desiredWidth >= desiredHeight) { + width = desiredWidth.coerceAtMost(STATIC_MAP_MAX_WIDTH_HEIGHT) + height = (width / aspectRatio).roundToInt() + } else { + height = desiredHeight.coerceAtMost(STATIC_MAP_MAX_WIDTH_HEIGHT) + width = (height * aspectRatio).roundToInt() + } + } + return if (!darkMode) { + "$BASE_URL/maps/$LIGHT_MAP_ID/static/${lon},${lat},${zoom}/${width}x${height}$STATIC_MAP_SCALE.$STATIC_MAP_FORMAT?key=$API_KEY" + } else { + "$BASE_URL/maps/$DARK_MAP_ID/static/${lon},${lat},${zoom}/${width}x${height}$STATIC_MAP_SCALE.$STATIC_MAP_FORMAT?key=$API_KEY" + } +} diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt new file mode 100644 index 0000000000..d39bfd7d15 --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt @@ -0,0 +1,111 @@ +/* + * 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.internal + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.features.location.api.R +import io.element.android.libraries.ui.strings.R as StringsR + +@Composable +internal fun StaticMapPlaceholder( + showProgress: Boolean, + contentDescription: String?, + modifier: Modifier = Modifier, + darkMode: Boolean = !ElementTheme.colors.isLight, + onLoadMapClick: () -> Unit, +) { + Box( + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource( + id = if (darkMode) R.drawable.blurred_map_dark + else R.drawable.blurred_map_light + ), + contentDescription = contentDescription, + modifier = modifier, + contentScale = ContentScale.FillBounds, + ) + if (showProgress) { + CircularProgressIndicator() + } else { + Box( + modifier = modifier.clickable(onClick = onLoadMapClick), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = null + ) + Text(text = stringResource(id = StringsR.string.action_static_map_load)) + } + } + } + } +} + +@Preview +@Composable +fun StaticMapPlaceholderLightPreview( + @PreviewParameter(BooleanParameterProvider::class) values: Boolean +) = ElementPreviewLight { ContentToPreview(values) } + +@Preview +@Composable +fun StaticMapPlaceholderDarkPreview( + @PreviewParameter(BooleanParameterProvider::class) values: Boolean +) = ElementPreviewDark { ContentToPreview(values) } + +@Composable +private fun ContentToPreview(showProgress: Boolean) { + StaticMapPlaceholder( + showProgress = showProgress, + contentDescription = null, + modifier = Modifier.size(400.dp), + onLoadMapClick = {}, + ) +} + +internal class BooleanParameterProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(true, false) +} diff --git a/features/location/api/src/main/res/drawable/blurred_map_dark.png b/features/location/api/src/main/res/drawable/blurred_map_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7e90d568f15f8b687e25265f4a3a27ceaaded09c GIT binary patch literal 34121 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

cEIOjJ1jTpQ0$F>yZKoJ-(9kbycKypd$!wK z=dWxja%~Z1KHs%+)XCvm0uwVNbbfJ!?34!u^ZXcL)FABEm~qnxW7S*K>Z~jv?O1^@@h|t_c5l-GEz|bf!(8XXa!`*YQX~GajF_ z4H}LPM~wPN)qUY$kA3rClx!#YC>g{$&^tl^r%l@>aY)*nyP!j+Q+N$TqXUtwiM-S` z^vCsF^i5mkNKkSKYkpy|Nl7j^26h(bD+~XB+KSMKfc-L+ppJK_tpoBSH@%?uMu1> z39#2wD+%z^JHJhgmrnTf9G6BwyN#}7Bi1to?(e!=uXbl53L?sD@|69`$9F2OwzV^w z@fr)`S|ljg;9RdK;u~*M=CA|U1hQy z!3A=ug9%}^L(6r7$t4hhYEu3bC8>~>3B104etiS|+xszZ^ex`Mts&a$HJ?Xz6tCgh znHjB@qaS|@q}~r8TC3dGQ1|t>kmGz>?b!eJ{>?tlHC*5AnDyQnlEv$l5DQ~I-j4aD zqy3LWs8@+2Ka2BoGa39|0M&FjZh0Mj)pT-|YmpU>%k0IhW*4HyZlpBAC;n{az4W4Gv2_hjxlRPCQ-*yPqa!H+vj%6)+vzCJ+*A z$BbIenexNHLOOJQ29`W{XNO;fca*qp5AOFLEpXC>j&$zp&vSUjUnrq0U>-*>DWVWwK?Dw_)=*JJwRqXHI-p==1 z`RbhWQ+s)f_;U@cY`rf1z^gSZTTd?^p!DOMP2Z9^k722@R&tqzkyQnGuT4Av*_+q233NO@bRcmp<~KF1IOi) d`vCj){{gDE`>8sOoXP+I002ovPDHLkV1hAOWcdI9 literal 0 HcmV?d00001 diff --git a/features/location/api/src/main/res/drawable/blurred_map_light.png b/features/location/api/src/main/res/drawable/blurred_map_light.png new file mode 100644 index 0000000000000000000000000000000000000000..365cf96786fc9c5c5bd2ea584748bedcf87c46c7 GIT binary patch literal 48957 zcmV(+K;6HIP)&mVq3edjuALILbA2oy8PLzpgfRxwuIeS@_C^8dGAb{-h|NQ^|--(Q*i!&^~TFMPiX*)RQ)mX&O??3HDH9?>)Yr6aVu>e1HA^{;K%?{rl@zeP4O<>sMqC`p=$sJ1qF){=98_ zb(|E^!}s26p#$9Dhdns%FBDk#Zr|?tZQc9S2Yr6N3O)Yq^Yhf!f2;E6x6g(9ou8lh z`Tg(h*F)feMPkVlL> z;A{Et3-~H+ukRJU@5$$V_^-0?-gza_e}Bu+Wkazni~{v1N%NiSx!Yvsb6l6TWXs1^+F{#(=98eYcYm)A7&O>4yf@BMxikZ?$1~CZ z=sy7(OA)CCR|!M2!2w-_REp0NXyCu60pq{l*DrYd{C1~f6@%5&>U@4-|JPXS{rol3 z*ROv+`^q{FRszPPu_x>`Bu>}~u6$%C3A=d%3BcoRVtYj`AeJsh+bJi7gCmgXArfR+ zl)wM}z)0zxzim8lYznvh0`hwO_t_fxegAokzU`=Z(8<0S(ynl@lW){SK8M3c-FQ7j zHV`!?AwAakUilVn&IKiPD0qW0Fa5T4$!IcHoD4qvLdxAoetr3s5y8GJ3Zjd}ShloQ z#MfJ;-#KkjywF#9-+T@4eR*Eh7X0Jw>!AcrP)cdfbDt|#`*~MGY31jK>oUFgUztLf zy`djsm4n#)B=JX(u9HY#|aPL^YCnU53xQ4_DH6aG!EitTKRwv6M8Y z$QJF@u=lq6W)v?c{Qc(zjo;q}kDr$rN*(pa^!LivV?4iK{CdIS{hLiWlo~?D^U6Hn z{LdQ~5`K}y?##iNwDI8QVPBtueLV3rcAmDE95=Llo&+ixEGWP}BI4r0Mcbprrvkuflz7y#j75~# zsrlmq5a|?Q_~9eoTYT%O7gppTjSwjJ=ZjtJpBg6sa3|lY+Q#}%N34IprNGvq}FjG2^JUb31<}~k3QN%u4 zkLGhnEU`18W*7}DU(KRu?fCex{cYc!yX+ukd$rjOk(Dbxa+zE#W4nDj(D*+I839Jj zseuQ9x&QYaLB@-ygq)U!xs+<7G(9VW3HU4q&+oh6biPBc`l{voHNsl8n?Ix}@*-2gn8JI;Qd@)>!E)${w?+L4{%dCa#-;Tf0j8q@VR#T9OAMcj3%GDA|XLjxKxL}O$HQu4t@!&NX$gkJdIb&IeDB}MT;PIv_KM|}lG{>MI9|C(BOb|=DV8B|8 z^jQYuqgQ@acT$%VdUSrxzY^V^m$@;n3RYYSjTzffnE@oKK#S`X6YpHric?%{7! z513WLLSFEFt1eyZ5R)tD*s5o`WekzV8t0cyEij;#S0F6)?W$RlUD~vGA7un7N&W)I zII$vaOzgaAtsZ=L-VJ3YG#-;NiU<|coRv(@u_C9V@P|_^P@^{?};{ife zMlo${a3oc=Yq5b-DoudV!LU%xEkzk~j$U`V~YnhiV6 znEMh)X!BuhN@$I`l+i=Z^CLMRMKRC|EC@t71zV;jNZpFAohOuC5tch zI==!r%}5~YH#HdDN0!O{6M|L&U40Bju#!0eI{&a+_8|=aYMIKQloW$5Mx@9%nmv8X zrh>L-Q2%&FI3KF;&1p5KCL*?o&rl$HTdB?{)*Fm!1j9w)BjTb^cw4ed`FeoTw-Mzj zxuYYJD1Q!IswPp(c<_KzQwNnIHhpi0R*k`66bl^d6KM2zR88iUjC+OH5G;7)V2P@a zeg@lfJqS!G;{%*wHI~drj7e8c?JJ zb2Ne;+WSFYWm0+nt&7fa=Dz?WV*7vU@AnzUHL~x!(5XCJg-w5T{njv8Ic)Xs180=4 zKnRec%aFvseZTgb6`UIfTyTny_D|3#;UoW4j_ZzUq{a@Y5f+Z?Pcv#YS-BzjY$C?E@lFkMJ>&jST9u#R;qp+4S1PvDW+Or4 zh+&Z9g?^nA)HqVnGJ3%zP#8TWh{ip7cFpglU#}S=y=n-1(fZj06}hJ^`q#=f}!M`%6l7Wi*o2+n%R2`Sj?GM!0P0zyExZZJ_$%))JleY=O7_ zbh;2gIzJ)S~rkzLA4sv=C9{@2Og`K)bVJ;CI z+jb%B17u1;cPid;6VwUIov4bHP^`$ijOmyu#06@Ev66A+Ef1t2mSXw$vNT47DM|_u zR+6*bJEnkDs%tF8(6dvxwr<1TlDegm=U_SpQNE>i4cF?tur%lR|o8IhQ%? zAhGjr=CZW1n3TfT*2sXYWbU!VLxW5`Pm0{S+f1ZB(vVTM97ukZG1u8Z;s$E9jaB1R zWI??_qZ5Iip84)6qjA?6epF7PLB1eypxbxUpa(iJk?Zs*OQ_!j^9S{fm#2V1pjU{k z8Wmy(#pt$~zw(;NQej4sTtU%+-xAFY&T;s zi?0JwCJlSP@Lj9G6DzlwT>kaIBW-kxRMQ}CWh}6-n9ua0F4%MNMN9;*2Z+XzYqDz# zbqGzGF8lS0bCNh*xeGeT$=|+wMc6at9AFx!mWfC%(^xVeSgM#7=87yPuz)6~wvmeS z`ks-K^^(5g-0W#pKSov_&)}tW*(bLtkkQlWJ^_PYoNLhl&uy>D=}U%-6rHIrYh{rT zH2_Vef1k%vB5EOpYQTG^cZ4Jj8e|=Z4`8wZttR_~jLy9+8#*L#;cAYa_*Z?TqSPOp z2-iQ#!HtguMJwYw5gxgn=CdV<0ng`nDn{@dRGOdWs!jR;>= zlkz5=Vf$!9KVS@%10b!*O=Xau64IXs-LWWw>Dd|Z& z_95Vbt#Dxg(;NB6ivEnY1Di=Z3OXVqt$nf(_ACA*#RhVU3PYNTsdW10O2>MevZ`*_ zduyqS z3RENpVTo!VD={JNZ2Q1x8dZs%iqQD=7e0JsY%)Opy+LK=0zjr(2=8%hMaN$he)HNa z8C*~H*Oc7$1*I;^E3Q$F075=XC>=8aU{R9=w}B{BR(^m>X%r|rLLN_FU#T5<(??-* z`bR3vT89;-j!&I}BL#J1X-2FtBD;v~FdvYZ4ic|vC#Dmtc((f{7RTF{z|1#YQTn!p zh-KXw3qS~PwV7wCCxiwTFbiNG9EFjaLJy1+%V|Q9FG6jkWfbvHrk^n^5r~^B$Bn#E zLSQp(l5tazrVU6DPNu8Y3rg8pl72_)*M^y}079M~DAU}GD2+Wg(Dn?_@g;VrL0=R* z)y^Qr{Fac2PPl|(B+$DwqY8#TI;B+ROZyD zMY>kQzw!J*G`$MOTE--R&6^XW;@rmGQgi{#4m455VV7gdRH2%{;tr%f09GRwcq1m` zbf@3dwqK4p#i8E5<}mf(bZhj55XitOqqX?X&*lg0aB&6`q@B)>cmC|4abXQVQ6vKf zmejVC8LoLceP4P@DWCQ*z!zgm;d>0z3WHAe@QAfYWGgE4ik<}clx@)YA3cV;Mfrg1 zi^5ainMT@(l=`tXT?h;+xWSTBLn_AT3+c4GF1slN8Q0K6{!C58l;8F|!V+g5tA;iK zpUMD;q(@npcb=D`20xRhl8I07sq65E-*B*0DlF zm1`IgOX7bwN<(5jj-_Y2iHc^uK z#DtwOZufYY2~kZ-QwWo?i6gZdy#7u9PCrw#f`bM5k3as30ATr+cCtf z{+8EczCAdc)m6r;8Df^BL#4IqZ8eLy)qMC^Zu|KP6+Agom9=d;Ff!xX<$G0P`ge}~ zsl&$mk!iV|kzzKK@hU0~)|Ux^6aoAhgiq@T!41WT9#cMA@eB2GVGL}%9{F28nidTZ zr9M^cn~48-A|C7?tD1B5D~6fJ1Z_*NubE-$mB!r0fw~m>l>)M}!14X=cTK7>2%llc zet%(_e2(I3idTMY#voko5d=lX!a^PaM}&vi8a*-nmFpd97xiM+9@r95n-Gg(WxrPb zJzmpK3XHvLZwX4d%t;(YWHUkZ;>R$eQ*lN0m_N%^xLOfoSu%uVromgIsjlFEq~Qz^ zMZ=bn3s{XVD{d6i%2#%y>@eG+A;h%d1OB7@lv)?{%*R8zjT9ACcFICE9uS2A4Uf-4 zE^IDzIE9#CojZBY!?m0X)QdC@Z^d~MUZ)wY$C21%r&5HkF0I;IYK6$VB!+q=R(1F9b6#D_~)$r=%;==G@cin@RV=PC}9Y zsui(v!H2;&Cc+HhLtft@77$BC$41_Yc_(N+nC{onqC+^>wNl<_{R|1n zbpt8HxvC6jtZ+vzF;Gc7oR&p}uL;w2j><&Zv15d~YDs2ec6{B-leP|Jx4wH;~&ketFe`u{MiKnCP~(vD>1skqCDQwCP0qZ*az zi%mF2VJSyoluggI8HI#8c@%FBU&75)cs z5OI+AUqzCIOqMuXkK(6?7B!wcU4M=H^e!!_xwUP8SQRM8P=i-kyuwGO^8EJ?EshR3 zvr;Br4O1xQ()gkV3h^*G^3|S(c_GDB17?-k;`^_g5#VOlnk~Z>Ctzg+;@jZhF@25w)AyeD8ARxPjP7QBk|ve{7o*tfxRvaSAm)~Zx(R;mC0EuK*GpXlt|lY9@p)@%6Vp63v0%GQOnMm?5J zYUEoo9ZxNguP41F$Zp3D+}!GE&3p!%t?w1h!JGR?ajHA8t>`3~io>1dckE!XNz^p^lZu+SEG zc-A?td;l_j2+Zu!?}%9svV%cz{Yg;~^`(?5K8!0FftkCQ`X>#ZBNA_}&%!p?<8~h@ zB9T!^S9=s=2QtZYQolzN3MZMO)lnBNz46(glLn8jBbaGlIC&dF-08xysrcYU$+apy z`}$cSTS!Lz^qw8I>XP@uLKaA*o^LyeRao`6l|HB2E0uf?3hVZ2bY1-!V@|nlPAKu$ z89$3d;w*43#a!t_a&cJ+Et5lvC{R69Rz!a|+93xJOX-=d7RKD$!sn!~q3EXF)C)RM zSCFe1o7=xPIz+ixc1bH@F#>KC{LOuFoaz zp^*yhY5TBq71rl~ue#14ao@I>`VJyMKs0bRS++{<(O_|=rqU0_GJJ=2$Ts}L`=6On zXn^NkSERQdC6HxC#1~MabZU!Lv0UfXX=>lM0X@l;ViiqeoOK+97S1H(f5 zRx58bEZCw9G@B_!%i7M9uIJeqwv1B+zXSpO5RO)y8MFDG0x@XF{T z_JfY5N8m_tOM7ZWyJu&qzq?q+U^7mq1_SG)#J5XpUv5zsXv(}2U3;VkyZ||8k~Zphne0|UQT~=Be=jq zMie8oL<=%%m_*k*u*(q})GGoXN87aS#|R=Up+f{0s`KBKTPn&xl7$Fb&*;&Lq_Bpj z>6yv4baIue5GD|<;RzB{?5M)e9o<P$JXcLSg83|C}~nmsi7&#wB*0kL7s>&suXXsj=aE{wxIWJnL4 z(Vn>2qKqv{ z+jRyr1Xs#oUA3pJGcnZ2%<8|BXt5tci)k+-{E~HVHLF+}HT>qFf`IM4g4+ciIb24p5u*^W>K-hC~-C(+3pl?EDTB^uqzX(Ebwq>@pAs2URoica2qsS~IYTLlSb*dcp-~ zXyefta>x)M37pFJDi%j=RQw*LD`^`3@|kjrePB>%$v~78g{EINp>(wG|ANlXZ`FTC zWa(CcTa#UwQMeorBNj)j?O&3c;^>ttxjQR8;k8jpZ8K1<9ybN{7{t;t7lvH==NGFM za>Vh+(yN9xowG_Yv*Q<@Yn3xzz`HdcL)KIgYp7(gEVjmZpN;d%uO-(YBf%LO08=<5 zFmyJz$t?zy3UgLaCs$KhqtDa_7+*};+bT93VH!>`9d$M z787g8k{HKO%OcAh#ETGU42q3k!~iyMPHdM9q_$hM1*g$Ja|M9~xGONw7pA2XWYiPc z@D#b$ePX^c-62vrD!`TrG*^mp`=YXw8_eYg{kD!ru)ZXmz9oQTikr2Cyb#H6pzpa! zn;ISm%Z>Zcnw_7=LJp=h?|gh3;zaXlKXY%YIyQNj(nKx;S{R?Kzi0%4TjbYFC@UfE zz;)b)WJYy&JTJto8P9KUaxu|6NWH(;BC)da0U}58w@ESeLY+ualQyx9G6~+a8m+jM zja00ps{h4w!qV{oki~lq7I*!S&0(j;=;Z>-5D~1(C`*P(8+%w9P1{q9UUO15P5@Ld zWLpI6mZwHL-V3!BX^f-G>4Ag0-uE6{HDb}V6Jf`pf{l#nS&TdKdt@qXx)ROSYVvp= z)%PUlw9)FO8(sLw5AfIyMn#g+GXahzpJ{&%E%Vdnz$ZV%>u^(IFvi)Me6 zCA^dT(rqx4GHDo)f)tE!)G`%6_ev&S_up z`1z6X^q905btwAxMAFCg1BZ;%{^<2VJ<&Gh$s8Y0IEI0&$!9do)P9fjm9XiutHQA| zfKN)H6^@5{mgumZ+c*sxdDgDIa5fHSrdQ?$>m4*6FTUS-(_kDY;6k~BU2dNvc*xtPV8>=TYG|$1F4Qka42Ms93m*RJ z?feL{_rYM}D>0@4RwEOv@h?kCS%6UQEr1IC`q;>4L~b8gkb-$A>~2=A`iXFTRpw%a zo2#wg)?m`vV>_e;I*V!miLJ?d1vf=LPN*2BCanNTF(-!wixXS=@IO$Ntf7RsV~M4d z3|Jb|8jOvTm_65S>N~WQ&v-DRF{h%}u8}MvRq~sOB+XF{nMF<2h+Yf>t2HxEdy=Fa zU9&PF3eM7`WAYJoa-6SvDD~-`uqEx^l;KAwk3oK;v7yOEOwnv_nMcpiP+~cru%Hrh z6Oo#5jO8`Y7s_p4kDA}bjtZv%Nd44@$ccy!40>iwJ!{N!+ZWc1=o5Ng#te*5W;iF+ zEX#%a;MNfKr$snuAz7Eb=`$7J*>K@Btu1ED38$-}9qLE-#a5TfgejY*g6!z-A{pCG zJNZpsl*v`XjD~Ey1}<_jsmxs-(s?st8t8Ej%i>QuP^Y47H|XRybB7AUeZ*oG142`k zmnBZx@Gm{@M_cIONaQh!z$t9~{r7<0S9x4j_qAx7IxobhT`z%`oj*V=2`h+J@xch1 zx}74B)P$VrlB_Rljh#o0aqC%%@>@1C5d4SKN7J~eCS5O#&+Gf4mjcMy6}ydtIvX8y zB+2c09Ec-`0MxMN2z^#e&&$E(NMtI;*_=3nG7|tt8~wG0@j9Qmk?mv0wWJ+Tec{e3 zE-iTv^u_u~3>%sdjLZmtJtV?@Xb0r{2$1u43)FFTHQMP)D1)Wv%BUw}p_ZqZOMR9+ zN|urWHmQHWL;Y3s4Iv$4uF}6}147HY7yk9*F*#1>(JsC*a*mihA~rb19qO0E$ltY( z?zgF%obws8V~0m#W*xZEwG=pZfB9TbVv~X>*gS)f(iKMwu$K}ozHF<4_ho%*R>f+> zG9(ElV!E*FT&QapWruPO(@1rMO7(qpq~Gqhe@hAtO64M{rew@Qx!X?eA3i2Wv78J? zi#Qr}sG2JHADsg%GUcp@%Cmg|^VI4&7rdcFyYxuEd6%#Ytf9Y$0V(C24h{ksu)!7m zbNZz?xNZ6);+2rUJYC>t5;?SDso$u1^n-KRBG`rg@To*$9ZqY}$# zMveoR#Fm*-cAo-4gTapYqewroF>JV$p$Z9h>KrZs3|Dyci9Uyg9(gbpY( z)uF}@#~i1sbwwu>Jj=)extxj0VkJe5b|DRO`e`vSJb)-=+^Xf+sFzIe8%kUirmN9y zz{5%kN$%7=kwiDJ*cTX98nE7n&ZIBQC=NSPU&956A#hL%L&s3sQ#@byy%(8>(vt0q zQl&Hp=Ll}1gZ!G;KRgv$Di&Z&6#L?;YUeVY<4j>&R5{lZM<6!bl)cfN#t}S-!7n*F zLzn$(hP-WyVy{E92#+m~Cnl#6laRbvn5Ao`>vKh|y_-}0$cY+EIQ-5@hr~_Fhsa=8 zDeGq1B!#H9#OdXTyI7l&&)1g$LR%?1^7951Q96RrHg~`u(3n)0MuZ?p!uoBgg~JUM zMWH!z4{06xG%;9`5NOmIMOeA4oU)fL>O@`F>kx4mh{U{ScII$McH}y{6?Tuur>K9@ zkjCiP6o-S3meFYoJt#Nh7t#(W0MKBKSm?zzre8CxfR3&x77MaBFw3yRB}*4iLqc4m zE;8!!TnplK#aLQ!MCwVd|2!^c1XOJAb_LmmWmV^68@ zAchh)XnjXObA8c(yim38zqX;lY~jlRmctncW7P_5D5RY{b63w4e^qrf=R|wa1mF?R zm8auFKgCn#z&fC&_w3?MzqvT@1rTJ$&@t9yVFvB;Gcgec13=58Y1E0jg#d&=fz&(m z${gGehc~Ckmw(F7%R$ny+MlcEvcHEd+yW1MDR%%^Mym+Ipqg&nP!WA`^VBK2$K;!j zDP!thF(JOL7QyST>})xg9*YN@iO{KVKKB->VL*CcN*Qf085$&5eQ()vfmhGgA~3W^ z7eWewSyXj5HS6+?2vN;8oM*@Qjk14!tav^Ye?~tW|M6oQ!uvOmt1uL*5X*ytB0kt6 zgah;L&HRCDZQq@=geP+$^hr*DUF$jWrVN?VJx?qX$Wt6y)Bgn;mPppXiJ#BVZ#`wO zSPr}Zyf7FmZBr!>N=XGBfw%fCz@xpg?Ml}a-_Wl)weD@_Te$umP3c8|?|=Pw!D{$- z*Yw`^-`7VJHDKno`!0J47DHd@JzqV7(yL*g_MsfxedY^yjFO&c34Thg*Kq@ZOz|4> z>a$~aLG60Nz8NXa4e}A5=AAan%i42;$o$ z*Qsh05vzbG>Ocp=NZUzn)X-;=&Ck>2NYB4DK}gi>YVrJX^jORCbQU#y(Q?y;e=a)ZUKO z4~fs8{P?pjkYLoOIn|Yl%(oTkfOn|3L5wqziKGcaP^EZ%L}al6>-i7IzQrwy|08r1 zDRN@O#CE96sb@vMDdnUn1Q66YIE;_9CbV(jtBe;*Uf=k7L@r<}ViVk_Km}0wQ>QT6 zrfHR|&Jz792?wzKb4ML!mw*_y2psY)cgxUbT3@IS$k z)Eoke!6xRAJq-II<c4;02`LNaDSM_K4puo6Fa(rFzrO6O^OTnGq|5mX+$ywLv1Fkpuy0kfO&-vkQncaG zv^B)of9;dLVoL6weDuH)5Xk%G`}2)dg_f5nmwQ)hAR838o%;qWv>Y@aG>#iAFG;}h z9fykDU`dCeyyf1@4}4_9V?u*V?7J%c#jqIzOw{s*{^Xe<>@*k61s@s#5KG%lb9iS} zH*19j!H=6EGJ#`}vHyj2NU#V-b=>8VOmssJA<_nf9IJ>rVS=tZpTKm9s??1mLe^@! zK%K`|xCGe zkbGFv0C;TnQA}4P&$1Aa0`wLSZWd+*vvxsWdpgEGs>MArI*TwyE-|b^b*~Bo$EsJz zhG{;-^tE>D-YHF~PR)T#emZ(2j9!APK_2DRv~YQfOgv+=0sH(!Y^EshRbPFcD2%F# zbqXq22>;ylxU1Yz|vzLk#yTC7po2ey~Y%Q_jJ~#{>0o zH7l=FWoJ!BWWMvPgo``_Ek|7=N`l#d_^I%s)?NKW>Ypb|fh*-*dlkZ>wkx$xUny4`$hxaiu zNh_9q5#DvXa=1jJw6gR0;)I#R0e#vnN&AyBUnS72BAkUvz_95u`qINw(X_kD^EcuL zZlro0YgD{XsBXwoSUDwdhU57FtppuYn7M*v*xBQ*eMns0qTHdZ{f>J-5OqRpQ^pv) zTGk4(d@5q+0rH*q`K&Df6k8r#g<889lVheKRYFjRUcBp=0gaw8Q7SI1Xv4sKeJQ8$ zxx>KlhBO1%*EpcU!pas9wu>4WB=gEvX4le!k-J3S-#eTgV1tE(59SHEm5b%&$Ss>w zEvokBMisLkNT4q90+-q369P?)SuuS={~x5)oM)4q(4}^&#!>8aX@O$7C;j^zt&ein z&}*jDNU)JzaF;5jlZ6KsaPbNC?#D485ckjL-}^^5{U~Zft)x*G<(UT^~fFGc-?3rtI+F%~~Dk(40DcS7jN z5<)2Q8f_y6{C7V!JQ1-QbfvFq3NaveT}ZmziX^(;+@)_`%hqz?Ze1X^<6M+hR2Mf` zr@^CA8W`H04bAGGT)!8KKupl-1-0^ayMlR))?3S3s=h6SDTAwBGm{ybEt#|g z%cn25nxGlE!K%zwhr+|)9-eTe_EO3eNL`09RqVSu$ka$uttHX0P_;$i@lD1VO#KoI z(ow>XOOS9v4Yy*r4hrWMc(Dk1zv4NrMM~^dt;r;5Q$iIo(Hu{GQDBW2P>CLDZS#v- z!M(3}HZw!(F7@D0s!CHX?4lDWfL89;4ndQa$`+W$4lR$R{5DttX_#n(2R1 zQ^x|&-hW^$gU40^-WnLdHn@_%|7Ou47ihFm%#uJlTxS_NQQ;qKTtQ;tW>e2hpF2@e4mW&%eJ2-+uj zH7mnL@|aR9^p@tmY^J5mwyFM5WPL`VNiA~bQD}6;C%F!29JXEn!h8Fw z^zn$}`9-Xi#P%QPGgK>J!SS4pFu}7;-bq>m9exB#=(d#`YU6 z!}vX;A_5J@KRwfSv;e;bD2Is{*jA?_G!=nk(YrXx&UkpMxCw+9XRMB=OKQIk;HS%mq==7+Cb03=5~VU`~NeMvDcGEM{PG z$J2pJrN8->x=nc_)e^yF^nF{=`!bwz)lguCqGpunH;>VakYOy~_yF|WY$Rt+ikmjF zde{!g;t#k8q8(O8G*xR{f0TeC&@#4A#L7D`5*V())04GAh`YLy16T{6WcKMlbBP5} zoPNOlE@F-icw`fO1;<-4?pj?iEq)&lMNL_Zqs({QLlv@vwWU)Ysqhh*Z7;nA0**{Q zsE64>RcL!y*8=m1@C2`AxuIfgf;p_Bakw-gC=ixV7a|g6$gHagA&e-V=S-g*qIA#`rV7dPyY4JD^(IO+w>o`?dm2v+HcFx(tP2q|538{l+a zUvi^dAhH?0tRgl190oH7@6_l?M?taxIi;eJq;Jn3>)N2fwxBff4;EWJWRvJMG7QBA8-BbV%y zU<^PkpetX}3eByoGTx;|xKv1p;BG#5yiuV`eT|Qa;>^d)FqPmzo5KiCT|pu;w;e$sC(^MXvMO zjM=LlWI2{a`EAxTBrONp$WP`T)*GeXplUKeE0PLVj!~#_Oorj8=L+}W+%}EecCr1S z{zRyC^JQ5w0Rpg%TG4hL6dYnJHGoAwDuousBGcj3T*MtR29m^#IT3(`e&vkQcel1+ zz_@UqLCQJG13Kvh-AO#!c-TxS<;Xop-Ul1rrsS|jJ0-KBXE1pAiJi^9z#Yx{`n@0M zIB~@ty}~7u?UW%j`uvds$iRk)x~;k@R3c!iRaiaB3B#eP6*i zdc$M|Lt)C`e>_CVXyMBDC~Be?>Lmc3ADhEcMM`9nPnL|GV8m0<jPK#`e>wn6Ec5-|6PjGc5w;v_ zsTn{JDXUrFWaftJsmhRCCq0Q3020H-pK|bb!@$lzW<%}JI6_s{ZMR!EY{B(D9#5Nu z{~xzGmaT?U6tPq^WF~;cj&5Oq9Ucj^k6oDDb&I&^t(ZpC4jSAda5?DlfCBN&H>cY% z%C3GJH1MEWfU1Tl@Cc2V7}z?i<(ALY+twQFmoJ9Q>Hm${-)s zL10ju0@kv~1r~I;(w-p#%uosf&{#{|c}X%RC~CaNEEeCqW56ZbY0h6Km2~u(YDG85 z2p?9;Dwu>&-1v+?096YdRuOg8wIml_$JF6{xlVhxzcB;po!gZv;rR?ETpK+mrb%)~ zvZ#;yQpiVb7j#AL)ZVBsby>;|%3-|Ob8yOm%)T; z^{Q%q#*~Wh9oG%t|6jwg761)4!YfW?E@_YW(>fLWTC{R!38vpeA#RE}%(L|UjPdhT zR^wSKbf0+wjCVMzdPQMuS))u4F$LsHb zg|$I1Lu^<`!GJRWz)v{#5ZF|Gd|q;>y2HlBOuI*0bhBX+aUedNDh0a1A=dm#9OtJP zAQiUG`E8vDr^rLorM-r7`UP~B>B`dw5Ga`(3@rH(c4Whq$eZWOLOtd3+*G>_4iC%K z{`Jg=k)3pIm^kvB5NN!=F+=2N1Tn^V-%%$Ggo;tWZF%B*DJC`|bdLBuA5=9%)bvcA zwZ3BpHW4LaMNn3Sv^nvjwd~2j4gYbGlI{GYtFnx^N)y)&_ORhRccHZ)MtfD%>P(tr z7b7}Nq+}L%Un7=-MT|ir!BztY*!+EgbuRW$oVNh4ICsnn*Sk1h-7Y-YbqHv7n~>c@ z=+91n20c^4s(l?NWJgLmkZL{xMlPcX(vB*15VsWfZ{n*81xfG zDN8obxP6=k8QbuT<8)ZN;Hz#}Ru}I#{LoFwp|(5tu{)X~Fdb1{#wnhj#m{1=d#zN~ zfbW3e3Mn(Zk3|3ZO#RA@r|H(8;aFdiSLzWfI?GzA zH|2dya*1M>HkW|Cf5-WgLo2~07T7TZ1)sRvPk?a^gu;=HE-Gy@)Ed2nEC|J3if0wk z@czLZvbyP7kv;Xf<)vWqTn$teq(oC_ba!SY$4TlsazZ>?VmiIClHwr{$C=z);Vm5b zA4%?#phkV`w!Q%YWdh~cyy5*0{=sDgci@@M)b0;3xi04b5RN?2O2!2#v>S|OAI3#|C#Qx(`_esff=b0TkK+EP$ zEMui~50NyY1-Cgs97CR<_syUgM3CmhWFjS6qGLgmzTNF*%0yXPw<}l@9CAP` zNS;*2iKS;aam6~Sf^{N`w_FNJXIGN!*w2Nvvkx~_ z=Ch~rn;h4uDa@@1L%<1vN&h#^evOC2Wm5GH<|9Ob<5x(i%V@S*1&bDkWk3Tri{Zw# zOf!n5EJ%<2Wm#d{Vn5TH-UezRk-34pR?Q(NpGjG5cwNBM`kX%1m9ECdCVb!dY1x5F zT>lmZt+`^AGb*R%)uSz1m`J5x53LofYJXJ42yR+X(+4$j?32+Vm_{(@p!4NTlmN)@ z-#XG%c-;+F~#awMM z6{3Nz$~Eq$FvO{WtL{w&7-1zPKtf@Ldn@IUm8vMO{z@teBVm6EwJV27#LD~W8KK#w zg9EBp&>Z}glzB$R;Aflq(%@&u?OgeLTEr!%m0vN9>7c8Hf&z!x1}`}} z;eXGXbEC^It%cPZ*4{vga&$GmVtgW{^GK_$|L*A%g#{WR@}pAIoi39-1bbTd=P}6a zprb#)g8UrMG8Dx`tj2@uQHgZ>;tQ2|} z=N#hfV?04~4+-gUDw+#8z~+#iV5`X!N?Y#0I&7oX5I*b-b(Zf~MWzvH4e#<>e^QkN zMN`ZfyDceyEId@glsu<>=Yvf+sxoBMI{!6P3L%*Y###Y_z>KURS0@r%_QYqbBBm4t zI|bk;SN}q001O}>0i#G|M6aAZ`mrAqtnsLUh-L#8ie=Y)Bl~Cf_L1}9t^|-8`O-l$ zLUG<~#tQvp5E++_sjZ!&(e2ZE-!Wn4Z#=m+Z_b%btoDk9Al=-bhnNjn$4OC_C3Z3j zq>1N~1+CtwFdWP7Ksz|IgG1zmmA=1Wj&aw+syaKxron`IF`RxBH(MDx;tw5N$pEOpRkA zctGi}9Y?LO;Ynt9q}eiyf#F=TZlnJELK+$d=fP$``a15J>F|y_u-#+_HPzT9v=}D)5qxTU zMXESA2Ar{56qI%DXTo-E7fO@G_p~2v@R}lTeLrP75UHfVaIp;ghrCy*i2yx7!oS$> zI*fR7zZ#QL(O!WWfbwlS+laFYoL3%~n`Fc`TIt+-cqpP8c2Cc=O_JYNt?dx0Z`OeBW%Ua>U{ty0aU^t{rm4cms#sAZ=FJZ&5A7*~ zAwIC9yZ#`Sag5OW$h%tNuY8^sgeJ3V^IF(0PB3Cg>~(Gf?O$RFfLg zKJ}paNg$vZ$nT==2*#XVa;>`U#1;5e*N}h_@OV1;f}#O@JwMBAfWqd>G$6(0tD%mc=SZ7>ta8LvVIq{ z{Qy2vi|tghFL?lVsin5Q7yuaM(hM>NT1x;*r!sRt6Ebs+_<+m6kzaeo5+#m!0910k zxk9OH!u8x6;*Tw{qSjYEgan4wgNv_n{7=`{fJ>LaLO zvQjLE&|u!Hu{YM&itanIdJJ*hg~~*rj!Gm5YwJ z=EPQ_nLtfzMzl(oAfQ?`7#Y#n>LkUIgZ)QfubY__MGBi*E2ZXk%lasAvA^s_{V&C~mgK{TF(+^h%A?_F!-C4wuS*8EWUL5sKPhW75i4Rs zN~l(lVn_rZ{f(N=Vni%hUOFDO}Qt31zca#6r8pe@XTn-0+tDF{_F+@i| zwJ>zmFH)aRilyQm?B#hzwdr(ExS?(sB`}2kL%^R#qyWn!KW@zG9?mJ*+bsrf(Ir>5 zsm{$BaMI80$sdm1uyrGF`a3znP-hHc5uN}7MO;rptQu;(`g66OY|rNww%<;?(GJ;9 zm4$cP2)F_^@*zEsDVCQxJ5kG#D+Z}zjDTj^hwN4iRnxPMhBU*|h9CP?$Sn0a&PFz!lc(e=J=zI=ga%4S^HiO?M#G1EIrdF2U6k#fOsnQO|_kY ziQHI+C$J#;e$<;Km!^E>r02qJkH6R+P@Wyz{r&jcN;w`5r8lLL69dVmo+=|4>l{2Q zQ~$*nbbw7LuYDi$-w`2rciCb(t1aQf!O>uXM=IihYD~n7ae{QUAjTaA%W#j`z^80M zUmm59vA?&%u?pqE-xZ^TBA)bRYoRr*I^&MSJXFzHR+5dRN5J&PpLOA}BmVv{UGT zSaxki9Fy%Wzmc7pxMdH|Yels5CZ8|)P#TP3^IF6=z~Ah`3lA9@Hj#*rJwB?BfEC%H$Xx4>JJQXDTQ z64GJ?Re_cipM%lt5<}AH2S7q)fzrO`3i_UmL(57#dkJb49nwX$n6;=bH-vJIjETB5 z`I7O-5Y(WEnKUG@Sn70tWe_P9noz3^`-erpXIM{CCcz5DR{Tm!Xn^~6rT0P0)7H0% z>?>)i zc6xdnhGRhm*j6`{0`e~NeRs^XR5}W@HE6t<3F}|ocJ6S_F_0A4;y}7iRh}gUIRE2l zFF+VxRafAav*db@EiVfz%!`$V7dL4}DqURgl(CAnQ3I^j(bBcJ=qsGPkx$T|XqhnQ z3^9W@f}Si>+(Lv3Z2i7yS{|IrjWaZRP&|45Cltde`YbwD@R;s)ie3ZtP@}abwh(G$ zI5w$A`Jh+cO7rfLj8Tc}8<~C%>0p*6&L$8qh3~s~CA26b6`R-Bv9fNSFMTlbN&75K z0M++Bgz^|Mdayh8)B!?fArGy8)&Zn+h{Y|jJ*pl;?j0XB0(QH0=&$wHdOJucOa1mO zoQ}BKG>mY`VJG^lANQeB3cs<+6_}W{WSqf5EcFH(t|>as1GI9qVm7M|g>@*Ao%DnQh@{acxuqNn(&4Vz-R_j4LaXQ)7K@v#a?AhjAWq`0W*^9V zaqZ8K3Q26o10H^!wKv2Mqt4eBlU3S#RKg3jl^+CqxcI2kmxt${XoJ{Z>=mw0$eYj( zZKcntxtZ%!D;O42rR8V{D%>?Yl}|f6(Y%IH^L7j?M--ZGQX!)xyjY_pGo0yQa69(A zQcmyNrO+AdW!iYPYv5)&qm*-nwYqVm|2h8#pMQ5;EW$_)6~G1$w~2_F`2y(zIo^2I z-|UaBTb)7#r$3{RAQ$(9ud|%-{p=uN&?C2PkaY+tO&Du@fhL0jYz6mJ0h4=W>hps4 z=viAQZ1xEq@|8>~R9kvbn9P`%o*2qb1{omUQzoAV3Ow)pyl)&<#8qd~IQIv;12a;0 zPz1Pe1^502@`h4vTCKKkA3DFvABwC(=JK@F0A+}fzz&hGoT&h8v}Ju5*orn-&lIk( zZUbGp;=+S-VUKe)wzXdrQ`m>y8UZSvXK_a6yA{>*)yAZ=YXu$xjiADK;+F6@uIdlH zAJ6%_`O!)hi2C}DnjVvKjU;1glMGJ=m3ektdNR^D9a|4l2IOjOY!(yir|M%ZNR{dq zF>L`RuNsfqul|dE6CM%7*;yTQ7HrI#+= zo}4p7*ceOoSlvOSc&FO`6v}D6f)erTA`rO%Y}QvM2&-pL7plzdrtod<`opky#d^$$8>`nkg*%KLnX83?ad zCnC&zD!mrOG^jnN9^Nrl_Kz@#bC62Z08&*Tyu&9k72*eC07I++4n#m+|V3lgG|Ia z3+DZK9;dA#9Wb`oaBK$f!3LslG=|{7X09@9jIoMU;2NG?ibdz>?+AzA z*>NIxiaKxiJdy|~SR3ENrZT1XAFPbZ z_B>LWV$iS=_qgnDsint=JwvlYAewX7v<)MbBE`k@BL0CX>Q}Io5K~N*X92(?rN45- zfa|zLHV6DUAXMV|#lwBzAZV=XN~JtF?=(X+VHkP}#dz8IU>YVtNe5w^& z27V=gVhrNpX6st<3sg$a3N72=yr2u`#rFJD{>tdG`45L;tpFjbwD^k>tAQJ@-=8gh&>yC4nhoD!!-Vhz#L#Ug?RCr>5o(*~PB{@HfL3VCKY_H@^t z7)mVbrpctbXBz)`ot==KNft?EaqK&h2konH4&Ad0IpG9&;kw6Vu;c8x!YemnQGJBb zZFD!^p^AoJeD3!OYw>f~xv<%f)?(@ud{FsUDIdm7%En><$ZdXaKM%dA&RPt{JtWEz z@LyG(sm&-xZ~J=0s>6XUYCxy3kuU7}p5yRa274sSD*^BMK-^($=!Ryr07=zb{=@w? zQ7N#D@_JrwSuU9c1}@*k;ej+>)AFRma6skG>R3mE6X~CE2vdQfHuHUvN8ZcSkh&sz z3ZAXCy5_A&PE06N6@g;J*wTGWeV{gk>ge@b(q-ta??jKP;%r`Ais*u0qHWe{v@G}Q ztB*-+#Kc%SOr{Hog}e>U>#1>`dOnBi;uLb=fyZX4W&n0+10o!7gVCJn?xJdtmz177 zpeWGS*t?85fi9f;$}?8z><F5i52+8~~xdKaz&|@psBu zbWLB&eW`0LMHDxST4<#pz{yB^Lt0`JxXF8`BKy2>>3p_PwFwGGbs>xhAi|E^CVeZb zj8#c5_T@?L1aN;R)d^7{ENB~ITpre(I$O|a4GbYs+qo5Ee@6CkIy%#U#AU0)*TN|+wgY5&{zTqRCG%%Km!L&^WOY7Ml zuEHz2i%GH1JLUX{?+i*uAogy%F#zT61XmmqUtMo~o#X%70JH9?b zuubOsHCxIt#tOVNvV%0@49y>gvZ+5=n!01^%OUGpwFU~aydr6*Ik7=fbu=`JZ8*U~ zHN^;0iS|4kZPN%^s5AU$+81MdVc}9v9DA3iEJqhoai0}@{-|pl;gZ}JcuY59P$X;B zjbb9E!!y0XSPzTVBu~}dpr*8~s+sC!FqLR^ykIa#SoUJlqk#p6X5T#p0!eqvAwYZs z*aurt9YsUCK(7@5!uSEuUDO->|4HJ}>X`z;St$;|V}@r&!v1fT=mqx#`9^S6TC$Km zLUjzG_Mn-~jONo6ETYU(-Yq9W6bg2#vs(G}N4q=I&;;GNB$6BjV2*h`o&8f0X!-9L z!X4FAXJMXB2VEnb4tZ*1kr}1aw)7Wi(+1iGj9mK{L>at92!iRsGb>udX;g5qC1>O< z>5T#rE+E|g;pkYke1vUd0}H;%7BMPjio=h-BQ^m$R)MuWz%22{tu6xyrI)-xg+@i4 zz6i=68H}1BY0=gF=N>j(n6QMLmwil_t7ROb^^~J2{TM#k_2(*bCx+^FoK zqjcMjo`^d>j~p98gL5{18I+QY#PSI#`jf1n*B_k)Nl|TzA(^*JGBS+(w!_}1& zs*iN$TvTt#P>e+Sw-cs&kYjY^kuOf|x5Y(x-Hof`L7ytMyVH&+PbflO|3qTeB@F-EtIBe@npuHlRJmq3_}x58N9= zw&hxgn3ou?9g;p71P{!cB8v`vA6VF_?k17P=et5;=!a!u?|d7vrl|0Dku`R-Bl`AX9KjGcPNn~prCbc!_Ohq zEh~wbnA5q-cu`1DnNKW!jQe5cPH0YrtlP9h=oLsWl;qJ{w*Q41L5n1b6uQM#FL)LO z@B^05&x_jE#I;9;nw1_28d%vhH74rpDDL0Mov564Z_wZ&I*$BRwJPb@`i{H8mvW$C zbT0*djZpF;Ns?1H7~^>-!-SHC{D3LBqfeS0#F!pkI1CcT`2qfjOD469-37|1>v~bm zg#DE!SzUP#oAWq&jhwVkE7Ih)w_nzKaPCW|A!Jur+?WxZnH4@_Q)!2$dJ8T4dTnz2 zHiAcH)Hu1=Y>*|qeHhSa&=TvSz&PKfXV#Wj$d)f9=lWI%%u7n|pcD}spp{X?iU3T( zU=sr0Bu=Mk6=eAC$3evbO)#p#AjXreKH`wi4Jw60+l1{u=? z2uUCg-#dqG-YvG7g>qL983t@pnQX7PSPS6f93?uVMZ4NU_GRvIMFw zUJ^Xok!XTBK?xmG>l5GOA62`M0I+G4qoLANRsmu5N1faNaK*H82|d3Cpd~yVNCl;s zy8Bp&(k!F~3I+=}dG)k<*0wxrn4sB{JgcX7fUQ5^ljEdlh|w{y!y55u%>8Mw)5dgd98uLRWwg5(%2`%I?7>th};;1{Yq4y#!ai9k1qy4ytq zjQJx(EmDi|h$Q{->=e$Z=?pwq1ZKO8(Na&%a&*N_JWtLK4G&xsM}_(mT~`q@6lbeb zqi*H&towb)3d(Zn0reP=>?ZN!vl3Ahv8}7l`Fr5wYtL*{peY?>J3y?V03mCR;W8vs zTwdco6n7Ic$uVV|q3DClWF518rR1B%`a)cUB3A&U4c8P#lZ3Vg_i!viZx4Aw>>5rE zU};mP8RsF#4+c^87yqXnHtsz!!1mta75ERTmjakF=N*E^PF4(f#6B&U;YdQDfhff- z*VL;P_R=+>cuG_$m=Yc_LJeFzZi+yII=p1}tx?Q&5zTkNQ~RcO(NS6OfJl-*05D<} z%lVQ=q0sN7b3t&i!jlw%=Dxfv}Lnagr@~ z`|?9Qq+=*Ak+O5=*z1(HP!Za_=k3$^r=BmyQcR&(Bukya`d+SS)OdzFZvv*hMg~Yh zGhg+c-w|`H%TeCE$_k`>@Vfk(IR$d|x?Nw!?D|(+ze5=VuYWj%k`@9Y`J}l)DZ!RJ z_eW_R_X`PvyMTfct}!QE@`$#(6(yc5HX2I&3QU7`;pG^CbvHPNlUYj*$tg3}WljJo z`jVDGl+CuLj;Gp2Sx*7BpNqW8M6?9 zAk31cR>SnvPEDXny*C%Cz$i+bFq=LMI?O)A2IcDu{BsI)F`$uiN}XVN**AmJR*QXgPplhEY5dkDkAtwe1r!Lbyd$MG!)#>3WX2xA5Fn~<5jp;o;k?BuBd#5Q#m!}*s*5Ntee(+hd473r z9O3gbeXqm^1c9EBMXn~g@Db7~y@J5PW``g?v5(U1R z$Q+~4$mJ(=*x2B)1YBE%@ciH=wxJDEz6$eHuuY+BUL&tnOeS)M2wu@ci}#AAyFsu3 z3&GchVEfc6*xMRBjp--4K(LRb#mv;GCU;VcC1U6ECFFU1u zX^&zZbaA){&XdIqfs2i$BYm2Pc(*YZs)N(vMOU|-aTk8{tQjo=#3XVwH+xO8oXE5g ztRfdk1AfKU!*z|NKi}1N*V9;c5F13B4p#kqTd95VY;-!l2q&2lj-dW@|CGuZENaz~ zeRU}jO@F#;jVphfdQB)ZHPY1|kRWwW8@xV2(^dp1F$_ns4@N_yvgo9z+8l%`-bRRz zde<}18p6OyXPfW7r9@}4Ql>qwVdgNPvT>sO-19QUK8oZGeeFz~%z=a()cW`SDu(vH z8Z;6`q!K$*K0dXrNV9Jb!4ZqyZ)+BM{+cK+dlo<=5HW)gC_jY0WWah9YiKu2vqI9> zmX4B`)He7no7u1F+>&He$Z50WbEj)x4|egQQIHSD%Q#+_|N2GOuLq5LrYmMT)NGFL zgoj#X@6>Z+A%#W*LIJi(#aFIe=4h)-fk&Z0@9Z76sMWPyY2iDQ1-9&>j2fBR#`kr5MNYq&)%;gelB0xN3lh|KjK@tJZ^jb0*P1fs9^IGDKO-oS4q{(QrDY3zDU%M#@P`kJ34~ zBt@_Dar*B?q8M6jL<+ZZ7n*mfpQy3FMs^C5tzNMBp(Q*(3+=imFtR};M*wUj@~!|N z^z&LN0JtI1Fd(Vx~z8Yl=xpVFc8g{7;Ayh_+K<}oT$blQu4o_zb zsqJ2A3h3XB|FV_j>0*Trc_xki_ zq@stI?|w~W9ZO2d)^dppq^Z6V;6x0$p!I@*q*IWLD+gDCGT?<Pv;fC3i~BiU&-}7vCdbH8VCXQ+ML+;U<&)oDl|kKwYMHkr&laf z7w0&&k6`vOH)BS(OhZ&YNsP6LX((OERZ5dg-n7!6)iv3zTtkYueoH$9kZ*xTyU9u^ zgc@~ySLtXjLLu~B^E~j{m^6baMXN{O2j$b*fwj8|% zR5}lqeB8T3!|CXho6zF%N9hsE@-L9!KY6jblL8GGe?R2mkUPUF?>hi6RKs&4RE?c0 zYti?ZK&5*mDTZ_)7A%b$RSZP2v1SVNMW1Jp5p{zc3&b%7aIpTUnMMIX{%cd>F&PPe z=$VZV=f3n^en+C(@sMFyf%!217n$c{%OI__f@tTdylsuj!bRQ3y-nhbrVud5W(mT9 zMWkXIsr04Ezf@#D>u*6W?K7ovL@u++MOX<=@~mGOeDiiP`dqfKvBtLDxfmONJ%kvW z$l5}hvOhj5vQ7tvPV15Q@GPfUvEJ4l0ug&_h{_YRg18i$Ki^L3Y8Ge1Lv;0rwQ%Xa zl=<^BpuzV0JQk0ByzS7bZ$J3m%c|denuO+L&SE#qad<^d{{tgjMI*2nYMN;v?0&U= z9xRf2bDF1-#l=putG7)&hN9ml*buwFYy?#U6gliv{AJn7)KKK(6{fvQ6~qRj18pTI zRK#Ulb#Fj}^f1OPWw%w{o6|Ygz7>n1tY;+m1$&uBHtRTr)Q@TnZsnK%libOiDfcUl zFQryXNRf97Ssqf$5nMHD@T?xqPhu#4Z*ufRYV{0xyn+x|#S<*A^n}d@L#2MDL4jll z7mznfRdQDwr9kfA2IttxLpTfgmM+tvgd7neCja!!>B;@x-1y_~bQPb;_j>pM$knhQ zK*a)&Sl~AQF;%E_J#&#aYn z8*C=cC}YbgCGJoV%Lye3CIV&1k;ycY;1NhmIYLAq#>iRh0Ge2O@b=H=x^M@+bSL5( zx)@2OFsP4!wEdLxh5Z9n(AdpNCeld3IcBB|95NC=b6TRGbEZ*c1TkXdYHP`x=uX+0 zs?GpmkqO__0v}waTs}%*VFDZ(Q~XLt2Nqe$_F+h36fD;Q2((xZ4#gFb1`WAhf?+s4 zjXswL{zlMB+{+6E6tKs~P##ni7%udmg1u!V zhg8vD9~gL3CJ{_Q#N;rtpbez@c!Ux=(n*w47C4w|lsF=^>ufRpU^{Z#Hwly=7vpe* zi1pLLuE?!3Y9v>23|3$wVnkZa2>7;~Z&<^A8l1z-VfazGFkWT?Vr(%^4NzA@1j(Re zM39(D@=B;n4z6KcJAAF8sTs%HkM~@R-tp|9?s0y^hC;w5DYe3S2F4ePwO_1p^dhQ* zOk7LdeLBO@`izP6mTS%?&L!~fpalDRo@q;!>VjU0DMt>mpNDG}3JZD4VWfWL@7On7vnPC@45zNcOc-*_>C~AW7}9C@ofEf!pa7;o-C-M%6@PH!jtsLjhRV(e>_#6T?I6zhd7I2)QE zT0cjlA?jDlu-lIezL<_7D4Uhbr!w8cNQ~AT0rs2H#^^OA!&{Sl=RF=Fx2$ayq3|K9 zr^tuF3gIPD$x1yjYn^gSY|++jWKKJB9#)%q(6KcqfZ)JqVc6s!-=T{nYZP)bMl>~# zg|t$DDz=A}r|}4;T2Rc{|KV1}v_mX6V*>X>c*Tx>@IMT98ocAIvP!un&;q`vhyrO_ zLwVtAOy~96OU||ZKFbXNB;Kq4e%5<2nL9;emY*9m%-;A7>o_-ykZt*TrZjjThljHw zz$q4_1|b9waf#S{)Wo`W4QD@2A+VKBTqoc-Qfo3MY*y)y8sb8T?Ov+g=&gyK#rKzn@3+<7FzIFyZpo zWAvPUsUvq~oMz0!WBAq9(hNC%Ub z*W~kaFQEJfTPj$7$XNBTq7*#C>ETzQDg&nLQMp28?AIG4wgW8L`5t6$RTt4KvzY0ZQ~dQ)mW5~+ z!=ZpQoWd5B&1W+8;9@ou4q4Ag;4o?g293PpZ3iqQeAs=PM>b>RDzvo$hWEcFJ)2;w zD3CO)kl8*0xLWc^Ixz0~U$x4Ad?agNXjp7(xyVXM+IMx5q5x&3Ox>V&RPenQMh82( z)dMc#gtQFYJJ63(zFoIa<6}jy3Jg09U2+JTXj-8|nHjS~%gsfTmb29WNF$dFC^kr_ zgWL}S?Q2WE*S_&1h03f8ZBoh$#s>x_%(T>t9y6>8UFUztZxpwniCwKjVpS9{8L$~eKd}oRzA>j8-We^`Fxe~>_Y6iR)dPVP)QT$1f zU=F~jE=nB8e}$Q(K}XCXFToryQ2cxzant_bM~ixNr=m^$e))@sNQrfnH7&O7L7Py3 zX*r#eZxl&Q;G7@Ju>BLGb5D*hGR-ZdC#bz)i^&EMpc7rV=k_3+|6qIFA}!*2rH|r7 zIq;bx7T){T0lAK%W%qT27`8+BAl+yZ_#)SukI)ljIBLO%5FEm%gB@ET#D$p;403z80kEuG)K{>Y2oHXNg+ono<*^y=eKjxie`mE&9@rVhi*Jq z2DE^0qBshnmUN5_M}T5_fDD~b(sKpAyu}>o+Y8veH65uV4xL6JQR*iOrlkf-=vLS- zJa2=uW@y7^2T}#^m1}`aj^b>g;bfBEO<*clAup6g zxeW~%3~)RTLMaOrl8_ams9BkVbBvQaI1Ak(FAozPq-Z4sA*~}glo(>9_a?gQ#q}GV zqXogC;55j>OMdnNMv`;c$Js!shWeJbu`V?g2eFJK8(5RQ%HlD|r$Hmg#d5F@5wGQt z^pOnKS&IG24H(5K-vU$n{1s#wZrHAq-U?D0*thW$g3=fhC5k&aR=NZ7D4q{D6bzc?9a+zz| zNq7qhNDimO#b=P1rL6}>*dU4%#U3#xY0C&78LHLLV7bB+;VZc}sWi%9&hJ!{(nl0sDHXLF0G$mdP-*hDJ{ixrMdAy^AT#KP1 z`X`kj6;O&L;A}UpnUmq5#xxMABBtddI2SV&xPR9cQNZNp(qSg^PWSx+c(mZOq`_>o z3w@PRotTeChP0zB&Oj#TjND`5DftR^mXgIREaC6s%t+Fdk_5tl&sil4;$H4s9&Y_GJ57U%DcOFg(BudH+8CF z+HVRy9n1(j6`K_+eys7o7#0y3W){4+95@6jF@?~6caOB}_)>01&^mAv9vGDC*8ivq z8dFjc0m9hn3jsweKDwxvkC=~A=cHmnIBrB535Z{pXtsi#>*vDq(y@)t8mZzGd^m3M z-tY4t>5_($0uNV6indyWxnkWfiC4o62_BSz${av~GIK7xxT{>jg`W8C?DXyYD*ZW_Chn{_SzGrFz3Xl6E(qmzm-0o4xwWD-gd%EQ zV5n5Vx9A7kUUTrtIxS|Off;#^YI?r1pxE^m481r8^m&Mrl-D_L&C=&s{ve0y05~Q0 zSUN_N_omMi z3+w%ZwtP|205QVF9eexrp(38^9vMIH5q1_fHd#@j6oA@-N~G=~w9HgpF)^vh0Fpyc zoSTkA2|Oms(Z$?D(a;sWbUQgD4Qh%6DG!W~`i5VXfL5YnWuF@c#Y*>*sgUTu0fe=R z(CHQ)vtt{mY>Rd@O_CK%1<D7r;aoZ z%QPiu8@{FPQM7?OV${Vr(B8t_c1&wMoGkNO-^dp<{ySkpmJc^qeXFuRm6Nf*lVBDS zi)~boFVhZ-(T6bT4y!c4@?t6Z8hJ}TObkN=V{QL9lz@g|kZH4$l>t!V!UC2T&>%Hn zJ+#{*&Xn4WxFZnH1La*>4m^e?dkOXnBtf8%Lq?4UBewl*OA#>b_*1jUS?L-I+P7f@ ztNXElnN#m01)-{Ipf4U3F)vU~p+?g=albOdke(;stDm172QT4f8a&-sC=en)CiSVr zA~eqU*A}4tv|(zck}jJ12I`;h5ck*6_E3#o*-|U`YgRgYfR{<+98-Y}ex6XFl>Ce?k<61U041Xcrm#;q!=ekQjPZQdy9{VzG37c)TyE-GNB{NBRe{~f z!%cPhISnJ6rW`)$-QS^jK1WYqO;w)ZGbmY11nUovj2{huauSdt47!ez+5*ie>QF)` ztZfz%W>!0#I<3K_Pw9H2tVkNZAQmLUq*0{vQ1yv2$qBK=Mow(hahTY^mk^@lTq&`0 zSiQOoFC><&Xs0ZEI^{SBsHo~9V@$eZQ=qST)Po+-;;jp+YYMNF`L21?FsS!mFv~4IgKX?s?2cLC=i z-#Fp9Mx1;IW&~kA9?8;C7>9iAF(3<{X1DdC{wBt~Z()S-;o71{W~Sg2Vw=zN$(rrt z#Ap$R(Vop9%{t6BV{B4jcr(x)SE87zpj;ERjWwCy`d4?q@Q;f6BfA~U;q3_)UhG4c_!2!UOC{zMpb z?a_th5J-O^3v5D#dt*fJ$iT>Fpws@}!EqEAB*J+%IK2v>fu_TBX+mUJ4<9Fb#*5FP z;F$~8y;2!i+wd@8Y5v45`Pu>B?SEf3_aWh z!KBk$60-S>!Yq!$>T!OBcOFBd#_So9CZ9t)Vs%BY00_u{wY~#EXhUw!Z?$tvUGX$3 zV?}52w)fQUQ--g1;U)2^a)U;wDUl=gUEIZU(+C<1Zr5#Mf;>Jm6AS?^hindUM;(~j zhZsXm&Ou@89ROq*cs1uBhW()xbXrWY&x_FTHW7INVKUuLM{f6&?Wnu6$e|$O$|XOu z3Eb*}pLz*;RWa zxJAv!Dhxg4{SR;%cQ4;!*Y!sxvtWo3m7M-GZhEmSm;au)4m1JnE75OHeyoOqH z;o8WX(PatW7&DaI;$6X_nEs)iv2V^B=)M$4jCXYSV~s1wG-kiq0g<=ahtkFK2^Jb6 zn2aYBcxV?%1(A)>jL|$5wKe1#GG(IWl0W{@2oUBb9Z>G#Ik>etnAXC889?qs__)$R zu%_5Zn~a{v!4U3UOW53Zt$x15tWN=q(D@A%#nV&Gxf1vNKBBZ^N2!+&7PRps@8AaF zX{N#9AcY>ME9~4vQsvD0FctYCwy{+#3gPRLO;F?s(%_st{pnq$WXex^^fhuu&qPtw)&X+rz11P@3m7YAKFfR|47id*%<%Aq zG%??cFq!pnpW$FLE=l_U7{AxEm|Fymbc!l~2ir;>H;FZzs0h}rj54{nz;+0^Lq{qZ zlHPOa&29AexO2bR+WT6?*N4ixk!-eIaMNy;Xe7+dS-}8|W6G`&5~k$h#EI{(3@vu! znc^+sN9xWUEHo(J#>I^J3GLCU>_&y1ylR!pAC&@(T@3SZ1gRAy;JBH~c<4-vE>k?U zXk%Ba6oVp$-Ox~$z!+|1){1ftjet`aMqLe~!?K#Jd9sT$jwKnFSzHx!vx}?<6r^76 zvz5Aj9D#*mB5FJJkUSiY|A88QJe{vI`ywQFs{J@^gi$k5D6z?DznO;Ej2a&R>sTOW z#*r&{#KzA0+sGw|oRlu?$o27krLr+LSO)c?>8h@wRQc#THH_`Jwh_$QIfKGaf9EvK zT#Z;!P*zM)pEb5u$ams-?x3j}QVhEwLoS>+3K`BbDHpMA zGc61QWN!}T`SXM;z?{5|;g%@uZVaLS1jyxJ5VMv1;!_ylLR`MtHbY%o?EgXBjL#G%r99QVnII$ZBbE)@re9nRbFy+?MGBl$k7SSoDtA29VVmN_t-&v_fM zWMP)EY|B6)K>O&6L?}L#u?nPP1_GNoy7EqGaMiblQBhsSB;4*w@f>^Hh$e-B*0`TT za$%jN6pU@x8-7zD6GMqE=iQk~SmWgn&dza_lP`Ek%WwOY+KJ+4j4)Z=aHU@8rt}aW zY!jPkw9PmimFtI;oJG>94N_njk~Q^)mI1GGOuWI$GLn(}(H*;*O3Uj4n)|&w?OGvp z|1h;H>BXGe-B;uJ=KF9i39us)^tjgM2}`pDe)}lCz_V;fa`Ji_nXT!#WXY~8@Qkc< z6TP&Bd;tCgkb!v1RZdUl|Gpxh>}|m-y80(xmAULk)$%S@YL@i0dk`!~0{j#F=^4fu zvuvIbEiYh=$-?TgQM1LklxtDw;pI`73Yk0Z_kQ=ph(_cPd14`CL6Uxhoa}NLp(-&r0sT7Z)qYZKR^h zPAMHo(-!Uk%GPBlh;1FxzD5V}htPR zV@i}U$BSlYR3Hc%0*`h2AO1|>KFbt*(wTnE1J)tzYa}g~jIJ;**|>~0&{Gox#ICAQ&e_}aK zGAi{6Y;=tRg#$tZvrK!{A}%=)O$2MX-!(?nz$Tr5LXdCNKY&Hos~!T!$J`mt;qYR7 zF3;z5bf3zX*hS%85w0VY%YJF&rMw;ju0;P>m|B=gTci`u%+!owb1`+leAY3X`CO^s z1(d`91w{7ytN@U)Y|uF}3ntN<6@RJi++-X+6EzF7#5P)&dDiP!(O8xYQ~5~_y%s0k zFH1@)WpddH={P?PvJ}ZP%wz8>!X()?H=?O?Sx)8(Kt%8{8QyYRV8iKA#H|Zp_>wuI zMr<|s745#xUl6xYzSe}r;s+oXRh{e>_?wS`MVvIx_t#Ta)h zC7)VBm9c8wZB%Y1tWPK9QJ=V9WG@!U)@M#Nu-p6f^@I;`dgQdk9Yb{1ae{(+2_)t8 zB)lIj_0Zc&*kCcM6pDR@F$TFugbI;u3twMfwvAXEXSz?Nf$RkIVn z{!yA3x9%liHY-hcS6XFV)~BJz!ahkLC)Lbh|L#Lr(V-xVw{Q8zd~61AnNo~2t5U0(rtWjfHdDk!!Tpmpx?52W}XW_Oj>4?AWX0sZA?86Fe}apQdpT0Igzy zhQn1+(m5#M!O9E`zC7dL%Q$Pthlq=Q$<9!yov1g47`Y9OmYA_qj2|D;^N{Nr81KQt!0iS=ja&DXH$wo*ve6*g<&-g9Q zfvTybZSYaQ41#_e3(5$qC14bD#AEZcv2N znld||&i zfqA|Y(_Y{>8aoNryIK~QMb_kw3t%{lT`@u`t);-l1DyEHuaa^_Q7cE|8xK?c+eOX=*!0ldcWh04tWg=y}5q`qylZGli5pO}yfX zd`N{o-`dU3)nje52u*VMBcOm-45nl_RST2#Yy^;Uu#Jl`Whmu)U%z#$9|Gnu7=-V> z4PYfK90o@oKY(f?1XI`!h}BXoRy(|PnrzS-ir^IvAI1r{882(n^5Kn>euVBDD$vUU zSqGiF<^-l(gG{`~cW&DwsEeMx7vR(@nsi~C1u>0xmoV%}3IV-ec^O9U#+=CaUgB|j zidd1Y9&P3mKx{d*r;2=}D?xj(16@qIM%kuhL%J@Ti!oNwIp|0<(>07t%*qRij@iIq z2VJ~wj|)8_gIn-NaW?6xx~>_dblSr7tLW&@w0Eur7Mx?6A@AA-v$#mF{#+63k{9E; zEh%#PDI4%+#+n#N+8OlJNOtCTYew%OAzp1cSZ#n_lOdqLUxZHKQ|kWi63Zm@%O288>~^Hw$h zhLKKDTdafn#S`aOBma=h%7*1s&yY?% z)w~z~+t2%{W&zMi(Il?C6}{IZK+Du{-@|}$J!#}nD-w^apBi~wkIU5 zVc}8}S1ZwvW~2hlxyS0`mTcCtSs8xHIr!2}>pV6tcJ-&hz9}tW*K(4Sq8aE@nJaqR zCLfQ9#bU}sfydc0#Ys?`W1$l7zgtMe{B8e>l_^B*dsXc8$%j2DzN#q3(akE_Zba~tK1>iNcm0*2KuX7a?9AJ2joaLka6iX;r`d+c?QW>$v6eD7WrbB7e zq&_pddMqJSL;Q|)A5nJ4KURcg?-YzEP`~^FZOXHj`Mos^EX?7MRWq&kKA!o;0dy4s zCuG6h=+a*$xDNY6I+Dv%vyT`q);B8RU(o3ZUAKGGrmadMEu||vdgOS_2|-Qejglhc0_@2WAB$O$b~1+aTxwawBX+Zbrrv38|>* ztNS0hij5-EEl(D_`{z6T=8_GfW9b~z6HcSi?UzHaUk{5k&)297pJSj2XH}vSU8FF5 zEOlx-{HZ`c9`!MS$=qlh0FS9_yeEC16k@B8DYI!UJOGUNdWuDw{_fY?V9xT>_fydT zz!G&=p_c?=bR&UBTy~gixT~kGa~Mze=Wy@b$q3m9OqNh}JH^dFXdD+O_+M$gB6i<+ zhI7Y?^#I!&UQQ(Z69w`3^ss1ET|s?>5to1%{cAOBl)fIY-C^QN{G)&C-edH7;|~KE z((x}@MalUtun4)%8?;ctgHxX>x3|cFV)3jy`0b`{(oiu@8kY8Eo4o2`Vv$#nsE_PY zRC9ekn6%tIZcZuY>MZ1v6Eyc-%PZL7bTBthPo*z2 zxaq9L>Tt+h%9grr^w-eq`lXjMY+aK|@Y(Bbxfs;ce3o}z-;D*z$<%Q=0sHz>H22MU z&roV&fMiqdQK0w-7}eNo2sFBh+_0Dv_u?HcK-;u}YkO0~H3VPs0>$^d7A%CS z5=DB!lA_c>I&Ee^TH4^T0&;koaemgW?EU3-|B0^Yc`LQ(@D9?$7Cyad_0i032fh1Q zCJ;17WiRu*RsFreDy@ykydUEGGZH-)fF`ji6t?xx8j-o56zk3rQ)?(5g`hw{%_ojO zcsDve9cAoep(5$9=%YC~49a?>A~{kN#{X%{^}DL(iVs6j-*~FcUEHk?aAog4!GAw1 z?>RpknK*$Ce|+DR%maI|Ne>25X^Wv2_K}hK^`$uz^lc8#Mu1Rstfej=-iN3_nGwlT zP`nJh0mJiEnTkXYuf71GAz1*&fC?WDAo%s$ciYp$qGK0IA6);Q_;^-vCPq-(u!m~q zr_}8@G!_ce#GBp{Jm;1R*`8cT9t&)88Ht^*I}G5l28ypTma~LhWtqzYjhZeHOdz07 zIvn$D_jBH;O&!7^b^`04v|!!R{bGr0>lL?A%&qxH;Eh=~D9noGh6&9)?yin!xOX~5hdhq` z7y#BR<-|lgw90HO2AHH!OQ-Gxn7udSsc(u*=9v1z`!^c@$8Vl4Q=#}am<=02=zxVhloWt38flWb@*EWTGe;*yX z>yeUDMpb_dKX%b!kA3amGJw99=)>f%T=2%#!xH_&!!CI088t5kSRWC&LZeu@oF&}a zyQIK0Y*A!?uqd&=Y5;tm`@msG_B(3t9Gg&yXI+31x3iA7;BgyxLN$p+rD^~$=Gp0f ztQZ$G^1L2DvT$oQpbiTvvXV-~a0RszfMAm0I-rTpmW~-%^Qp!|gBqETebJ602WbB8 z4^sk?)gL@hn_3;Vzcx@EN!!aMLPf01!NqCI0JH}Nw!n7ZVg@3<`9v6c?ZAS$Fz-(0 zz1gWwuqIga`XUtouXwSHV2Ot!bl`c=qJ^gXLU4Y7MFke3+zKBbjmH%aX)XncDM;?& zFL}icIEwC_T`Ogz6Uh7phqZyY{b>@z zFC2zRuVLA{v7cSq?|nWV4;Rawh;4^S9fc5NdaRR3_W~CAS3A-NaRT^j$XHHFyN)gK zNhQ&&K0X(>&`e8qWHkLxE>+-)GFd7q}lT=K*#DbVe&%i`} z18#_F6s)!2u@oL*v6Vw#sZ3gZs@l?(A|icL8ZW*?6al9FOecbjV|3l(dU5!w;G z1~h`U3j^l=D!F6w9fsnM@r`(66Z0L#XixD6FYWGxaH^PU zbW@Co@<@stS3RsL>@LwR2app7!K>GiDhq(S3#>4?OX)#QG|0uR7yA&!uWTn@iES=?>nz#&Cd@@3HAL4#lN^&Np4$G1sQ4w@jK=d`@3TZIKa2d z2zEjY1E;|w1R^=Qc<*@~6#P85gSb}VKEj~uuM_3!?EO<>-GK6#P!fc+T3=KYbYU}8 zbSpZK4!}u=60}_^3dOA4h_>%ZX=lWa;x}+H61!J9>|%hT*6dr~CZd+imPeR->t2`q zuJ3OhKWj3mOpTD*TcR?+8J;W^RK8f6A|3lVs% zGdq_F)h;6WG;g}YfEp~PM{Kw+;XBOA8I=S!oo?0O@t#J96XUQca6UQVFb6!IsT=G_ zYNVxc(={pT>xMy}D8u>%-lEXQ1Z)V)jnt&7UjEMuHGA!ibq9AE9=Y_1ubC$Vlw|dBO{p`I+Vdb{UB$J3`;G?&q1H8A_tl6ez)swFC z7dI%vv1m#vX$2&~^H}9mA(p<)yfo6M(Ueedh*iTQz`b8QY(p`q7S?U`e*}<-m1(TN zoQR~@`jvixBv@e1w4bi}2^xi;6chbsPi>I7lA-sDU#ST=1QeADS40-T*d*(nnTgLO zfG#3Y-eE=~8s<3(bvb1W=~Y;JuX%b@^(#+z{kv9_TWj{Q=Rp8JfUwD#W*G*>SRSV& zPcaS+ItG`{E$biAatH@y^y!r#PzAod%i=L3ZHoD~+&r zDp9c=Lf9d=n=nwS6)bLr@7#UZeacY!0Vgw48RMPn9ca*>B8iIlWS(&`jG!>nRb~Rd zq*d`@Jnl9%|Z(7!>&=hM~)Jy|K(c{IPt2!yXIXJQv+uZiD3!~E;ke--8stB+^11ZsB z8w3vCziQhT1Q3h9w;;~i-%uzHfD%Q9+tnVA>szfICUO}AhXn}iw}TB2*=<||@ayY~ z>sOxbyp&0x6q+iD@^Q~H#ucd@nG~AmcTHL=MScQ-a98z;*s8FwBPQh2^1`JC&n%td zWY{0yKbzj#*>o)o&9EA0>80)_)VxWEh$Iv2M->t`oIv(S1 z?M8hF6j?)Zt?@YqR|x%-<93$n5i4!(=W|_F)%p+MVfxszVqgM*ag$>(?1FDUwFCZ{ zRNoAP_eznm_a@(D8Fcl^m4Qwa@{Ut6`PO_YN)4fB4>;Cxz8Zx?=q(tCGIizW_ zfJg|$JNk7Jtc}_TZu>=$!Uvr~Hrr8cE zVvH|G&^#ww7biE)!4`6AYv!0{Q4z87@4P$g_K7PCJnnu{x$9^1>>WAHmV&mYOhR|! z0SWOF1`oF3`O~)dM%`m?$%eu~&5bt*F^5imQFEc&FlJao_r#Fo3aMBF!kOctw+eR}P3xGmLU z`iW<=n@VpS;rw5@p-U>p_u38m<3tuf__K0|&9%5~^``b$bN}6Q#PdnbND0&|2nuh_ zqw}_}w%*?Cgrg@f8F^-75-ryqTAsF_F+xCVJKAypam>LF2DL|(^@zGOw}3J&zgqbN zKR*&Zr^NN^WLdjb^eN7DdB3KU()VPg`m2OoeQiBG^{J!}x>RcSL|_{GT$B<2W+L>h z>w?-N4rtOEejt|fzDi|e7C9jC0TlbA@L-J{xbN0`)Ma=*2@g~XZ7=!PWT)i~=rZ(9 zg>gjZ^J{Bg-~_L|Jr7yz>{?zz62*Yk%p2e;G-rgA(>BJOa{8JQuXNK_F%Hr%9zGT| z41~m_AK-UsBcSy6LV@+K|698i!#(Wv#a+&64@1otP5jK+2!=6%IpUweWFxBv@JlD7 zIV5DF{(0Rwvl)-G{UnvQjHtiFgR4qK? zpJ|pJFynCS?Vo4hp4N(Gh(Vhii+_J(3;tb#FwVyu?ZmiTc5u_QRYv%ivo6W9E-}gB zGPL(MD(UXarJ))$zwY|^vt^#EW^pmHq#QrK+V8TymNU))0Cv4;1;{#Ume)+r~-Zd;BM_!h;O0M(_$zH%6>343egiXn-Vj2oV8l zdX?II*fk$X7RMks%#;HW3~%?B-A$bvdvXlfhay&#=e%H~f?rcEsr2J^gYC;h*iwbQ z`v>x>TbQLmkodz&c%O^CEQnIAWp0-j?amm-iaj)9cC+l%rqTsEfVumQW)j>|7{N3y zSu8Ev4R0iS$Or?3;#oS;;Yn>#?)BsIlB8l0R_NsG*Li(BWQFIDZf_AFO^eT zW2?`vE` z-Y_6gpN!zitLTz8q-|VFREo1g;2JlK9M=_|(j*qQ*lCUE_TZ7=RHVGf0N9o!4r-E= ztdJ2X*3YVEEI=60e9}rTJ4Q-g_kEStKt66QE!2Icfs~7jA$r;O-o-r8f^Lk{mD4^ovQ00&l z5rmlb^vB1Dr0;C{Z?u`tI!m`Ye7kgkb7F?&C)t$FQel6zc^jD!sL%& zG2IStE6g#e6z_*%OOvv;6%dspuENgL75@!%5sIvJUy!`dLkifpqLWQpRhOOJ&Z=wU zqcR{CV87&xj&S{zXPVCXiTt5hfthGKT~9Hds~z+Y>EeEm8@}e_Bij^Apd&4=QbjZu z2u_dmez-Mhk%XDqN4Qx9*|`rzRcpp^Z>5N2BVdOl@68GYrUNoAvz6My1E4)e&g^k# zslz;sef<81T`pvEZ49FUdMTW^hUxGv9`zwi5 zi>Y7gduPs(O-+n^EM&V}had0zgNPiMA&ygiSW^iM^?rv1Kh<+aEWSVXNS6^Mmt)qp zBe9a2kTL$k@6|+JtZKCa@Q+g(im45PQj_r2mo?adDZLM0e+qI;)0(5;AQ0_-2bp z39P7&GcRk@zVVhVO zW(7cI=5Q6`_}!_B$Iv+@co9~A)c&6e;DX+cR1n+5nJ-Kn@C}RJ( zX(C_>ItH&Y^1lJ$**qa?cmq*!8^#V6vPOFL;kdHz=KM{Z37IKioQ3Qsxj8qC9y65ezK0BnEWyL->D_S|-kv zi~osFy|+6!lbC6Ww0^|G4eZ7x<6`K2);BliFXiR+?xa<)BZB5c@r+a2!vlf~X8{7& zT%mVMt&Iad7AwqhQVLqCcCGJXYNu1nM=~TdJnJcho!m9XT-pbUjqx&{N26KG@z5_s z*$nBsR(z04rgJqrdd^%0UR&el5o;`$cS1lc3VzF;OMbT-43JP)&X>8W)K?3T)5< z6_q+(0lsOrv7xduG!A6BrfL?kI`I4*UD4N4=_+%837$H_c&TZ0RXKWCM=!Xq72^Yh zCw>#pAwAl;6p>ryG@GA?@_97-KxQ$ht7JbO6X2>s*KP!J1VCAxB zjBGs8q`0?qV)KxQWEr2r$xE@Oh62v;*(hG7&qaYM=n_Zz!n zDv>GLEakRRbLy`be^B)>Rm0v%dcu58o3k$s*m|ch1(PX#)O{WkcXX$|DS0 zm!b{p2xu}9rIvC`JoY!9pM%PIaWD3JJBWm_j_L=0&>j-aDNrd203Z}FO-KL`1OV^#ay#gCilnk!1Ej{UwORUDXhCTL>Y!_9!I9&w2?1F-0 zTuKoU#wJ4i2p^;cSXpc-M<~}#V^VMEbY?w1+*o`3o|L6GKtfHT8r!k`2Ih7YSjS?% ze`?n&xOV=cq{a^;uDVyXw{RCUKq)X z4Yk8YKR0y7X8^(4Q-`ST0Qi-zjP+0YA%W_lH%UT*j!8kVOhEN>C3wy4FZz_WmBM1# z$4#E5KWn}`E4!U!XSYk8=aT~bt9r6i)ijv}d@`#)bfHLFRIcbsjFfF>BuSb zb4e@V#u}ACR8$RfoKKV#2*~KYZC}eU>T~)!!O(gC=^)79sT3DcFG!V{mm5Ntmht5s z;O;5}|DQq7W^bAqh+O$B6eRQzmq|nBvSM$T(@^Yj2p0#o=u11^`lhTR%dZeHQZR>8>=IzDY~kRF0@%(9P<_Fomv&gJOn(L#A)zm#yD^ z*o|DPhha8^O9m4hlg#+V2G-Blf3$%!ZBH+dM-v1hE~gefaa^6ER0@VIdP<-K33+G` z+?3afR`zfEe%SFV0yLtxOJod*g(mQiSH#dMp<8KJG|&*}k9~o*kT7v{toa>7j%pF; zfJNr6?G{&ls&Ai?3=SK`s1CqrtntM%_&MPNY=evJK)W9scdESo$kAT+b%wT#+6!T`5~?PfA0?SDVTMM_8CH{h=Ic1_?kt z?rw>WR!Jm@LqJ>wWULbjP^?Lzs@I|AM(0e6|4M0b(?TsLeygy4C`J~^9KpP)*S8v& zUh&*X-6l1DPT`!MPzAN^_yf|Dq{2+00R0|)8v%=k!f4SqCXhS=!lJ3&P@LmUAJCAZ ztk`tTD#gcqYzI!6U_{9gzzbzW!7ph8bp*T_z@m(ybN%sBaj`7Wun(n3VKEfW;8AW; zs_ypvd3hAM^lJIHNYP;%xL>Cq*i(k>Jf~?!Z&SADO#y#mMqd<7Cgn)U4Ih^V&@Cu> zkub``cb_27vn(o1>%>*nK5z3ndn6{(pt(Y%JkIG#Xqz+)u^5-hjyTS{3JI4^YwB%pDouE!2(rtXu^haZThn3_Ca)S8F7 zU?{bKEp`1tG_4hLhVPER7)Z07-|#*u-HygE1QeAwh6(Nb=RC}@Z{$uYll>XNdNd8U zf>(iP45yot_jh5~<|xCO63l=X7WlE|Acyo6Am&o9NDgKa`W=0DUZ1_&{yZrjbJ-)SsWU97xY_4 zllCX zU2wvsSz*QW>m2$t0)nlCqj7Kq1?G#R_GeOKrOauy5k~PocTpt3~rj|88qFTou;R1-}-vP|j|uWNay+m!^5tsK(Y@m}JB7IGa_eEdFhwZQ}uy!i) zf#Mjx?tw5t>6AuWN;rR_K`7u2eNTRxqZFW9{@5OAuM&vJdefK00qYgZggK50pEz3QOVPl)gK8Ow-?nn=;lxv(3Pv*E?XSh0&y3%;Xa zj-8BLk|NZzHRW>Z{*H7CZ}&6#t-&K96QoLbuCjw^k0PD;u)9sj4K!2OVo8(anf?8T zdd!asDKF7NPA}v?sR|J-G4zT1I6cDOvQ1hj;F2xW2utvlIY5z8g4jr7BpMv(*x_ms znqLrh&AvtF>WnCL=?N;I;Z8YsTn5ee`iNmT1ZdY7c|Si9Gs*xkVS$>4mAZO<>87v4 zG|eDiPul9sz$3`tfQa4(7>WVr3_puZ@wvhVuADVH3`l8M*5B51-Z-G5?K>|%(Wdo2 z4`8WJR~;q)kbKL%xH;0XgkB&(lVRb&n`N6!sB;x3BP>0=1%{ewu$u;O&h~~GQ{QTx zdWti6-ADty93u$Ue_E(ay#&ORm)H3$+MktCpOkEvMsKcEQ%l31CTBw1!>|r3+pBkO z@6Z~kyI9{rafKiaDh6O4+sx0qt^Ndj*!isMe7Z%|Kwdhl4DE1D4!dbwm}Zrt<0Nen zP7lirCv$|~&{2XjQqQJu5n@INLx>l_3G>;HB#*|c1Z33K+ke%z`@kqwfQC^X5%}P~ z64Jd?Krvm;!afQp1RgZ<>F125;0#-g==}7k3ciz9SEN(kLXofNAT8ZBw?F4E%X|+4 z`L)oGX}kK-EZV;L>T_r!Lrx3Fcxl^-BBFv!^aKMg^|jB^czu~472h+Z;nfRN2QA~^ zSGOMyPw$zw(>4iPnyqa5i4)SNy|md(ZVq&=P?EC+Fl?Aa`G+_b7{`pnh0fxg5#$O! zL1qgeIVY|T?%kgsluDYN7t>~6OV1?aFN|67=E`SG$TV+~z$ZpW3f??Y)`Gq-Y+{dNA)4#I_LfI|g#I9g1(37Q|H6fcv z02`Bv=E57IT8g_fV{v-TD39xhAc*PX6vWwV=HCPq=!E5bINL7eIXeG&v8}d%BF@ws=<4q4xT+qlLxsK>$H~*ty$(#Ls;!d8 ziA-;V;ML1-;O)Fmn>X-DT7n9Q$V3@E>r&pOP)k$NqFh|i6617c+>Atq##KycuXz+u zOD~AKXE{VO@@6pi6P-JT!g`vStdv{5Hi0dPSPy~1jm2y;qF*X}!k>){G zUs%K$L?Rx9F9f!|h~OwkMbaoyVNOB6d5O+)h<8!xtQwt2cgA`Z zl-pq5fuiM(pe-hTioMa(?Xw>g#UN!cKxXP!%SDS0Tf5LOce%GQ@?0_<`48pJYyfh? z7H^7burxs^H~$ddz>x;0fT2T{n0fh0RRlT~C2vC>%K zzE}uFq#+sHS&)eLEV8AB$#lvys6T)u$*6v3`V&j*W^9a?WN6 z($fsFle`!rPtk)_v#0vB@92ZO0z`UGAaOp#f~UWT#o7A?trCy~NyiKt@NeDqiWqg; zL^DEJDjuY_(6f-w+jlI2)XQRW9y_O?GL@i;Np!^q_2zzkP}J}+83huNGpNPWyH%Eh zA<@mXM!-)e*Yv5CxU>GKX(U1M>jUcQy?%rQRd6rOWf^E~%bMi4nC-;`zo46AmaDht zir(ps%1h{B&Pku$gL-|#;$vO3*a0j$=ZRssevKHzsK7fSF_P4hogB^u0? zv*C<2msHa#=9JLQ)NP9!AT3ztoK$>~xugo|-ZS;9&C^)PZ4Bv0P^=HoyNn5rgVii9 zNatD@xQGgR%j+iAUTlFr-^8@2*^mpPf@tdcX`NtpFP$s6d>>!8pFe|702FYgOvmNQ z%kD<(_xluNWFvbyuyN@5ocrkxO0kiy@W8}~x)e()U!it>|L_7iMa^;29AFToM{7-+ zLg>nGRfnp4$nnIob{fZrO#ZSG%T*I>@*-I+;sX)$k5FT}x1R zH_B)jOW3DVa|Wn(S3#UwH8<2W&qVEyI z40T=`0E)4+1}dr$WhO~u08L{Y86%2vFi5i@1}qp`eHp#`AmvUC_v6hr=Od?`>X1?; zCV)hakW)+{4#9c*C_?*~)io%av{scO<9Vltvv6q&ln-XxAGtwByxC1^4EsJV8O12* zB^xx-JRt*v&*iZTq^F=Nz}j2h%l9hl_=E*`xWf-jrC5L~l2X$m1MDG zS6+LGuKNYXiX$Tc{-#gEIl^r7)9*K;W|lLBN$E$KTf0P2R2a(37uD4*!`hV*7dlQp znBu0sJbP;~m+-wm(3%nWJS6$+2XimnCJ;oEUk-+TflJ|?JNCG60^cjRK&?5vbJMs% zAwZA(M~GAdOhO7?vYmNP3ft9sH-ut(*pv3O8byRka>YJ?yqxo}1j0}CgJn~lzoHVh z>>S85+<(`Nx`COfV>230r%2^@^v6}&)0Ii`74JKDwLPgul}T$*eXe_nw2(87(;{Pv zv6LM(qZ|hr+S1-wOijYF5vf!Fg5aF|gPqj^HQbL;s~tPg(t}WQgE}YV?a0z-S~@S8xCrv{MO1l6!wd`1w^=l-KPo zJ!nDq7cJ*9a{xWc1l($+{*D5Bl0gE00&BwOe3YtP7{;|ug~vpwYq3B^1fF}=)wh_U zVahd=W)vgGsR^*~Jo3A8&8)xRh8QJ@BXwe1jTe?b1*EI$Pg&BD2qt)?q zz@wXQdwqn`G*bKv(C~mABeSww1Gpdq&8hF0oC zqfqDNCCG^r>iauY*`VWe{_^VfNV%AvP3P@(|D)`!XsxhYVMDzetg6fp0D&C|VdO2? z$BiG*_&wd~C%-dEVQUWn4EX74rzz}S*xZbE0Cu3k!NVN5Lvywqrbn9 ztcsw;N4E>#`&WD`Ej6f#WZj&laQ3VpJkODoz;I3u8WQAP(hWf)0PJ|Eu_H0`9SRp^ zfrW+0WZv~ykxARv^ec+WoRc=6ox zV@EUISB+ULWxX6iPnOn|Z~F!0Z23q1W7Ln+MLLqMDz^cAO`WtK)`{=}ZA2I?#XNf< zh)(LIqPV3^UXv^D4sc#V@<1|z#pL|;Me&fR#~(s*hH(Mo`&0)({Fx|kAWP6V9x=xP zPtpQ102q80qWA3T^n2lbmyJAKeO~{PXl@&dJ2%;kCC9a6icA2Mu8llLmIRBvBuSJz z8487u^J|UlPjHHuG|Z%G7UzoT&tk16d+60!m4cz_7?#YGzsPB?f~984RE!Q@ z_LnoqB^nq@&cMZrsd2K#ocz^NkX*qbG?p$vRsvxr|EBHgEQJqMrc)v4okgf(C;?zx z>4_YwKX>rbLqSMcEP{2S8eVttv}TNvz0%Ceg4qb^C~Rm_3jK?1=x1Ocn$naXyd0N5 z{iwMb&W5%kO9*1QVm~&CZ%Pas>#1l3aqLseIZR!u%pA6dj3s*Wq5Xix;=L#W#`SFp zK&Uk^f?)Z;8Hr89>8CL#ij24{m!Q$Em9KPpQ1mohK_z75YFh)5QbJLK5a565Zgfkt zc?G-8TnYf9n2CsAAr{wQJ)345x|xiq&SgzVZ%@elR9=|bxwJ!#w1wE$_~m7E1v^Vr zW4t)6Kn`F}SbXKD1$!vriLjHaPLyMVoAB~t$hAiH7tTvK=^cuYliTsPy5vugc-@c) z?ivhNfSI@B))WPBrx(I_VirhcNa%Z0<#rW6y3F$HDIQC3WU$3qL$-f_Ydsp6182nK9dwsR0hJoBQ5nii#T^n5Y20_Kk}95j{xaDsks#7GrF4e zDE#Ym0x1lQq>~ANWJ6@0v^dv3goaMC)~sTdlJopU7NuF_F`A^P1N{(a1nhr^YRG2G tqg6X^4U-?=6{Zppcz9w}r(FQi{{W&4_1?z0 + + + + + + + + + + diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/BuildStaticMapsApiUrlTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/BuildStaticMapsApiUrlTest.kt new file mode 100644 index 0000000000..023c7be365 --- /dev/null +++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/BuildStaticMapsApiUrlTest.kt @@ -0,0 +1,71 @@ +/* + * 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.internal + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.api.internal.buildStaticMapsApiUrl +import org.junit.Test + +class BuildStaticMapsApiUrlTest { + @Test + fun `buildStaticMapsApiUrl builds light mode url`() { + assertThat( + buildStaticMapsApiUrl( + lat = 1.234, + lon = 5.678, + desiredZoom = 1.2, + desiredWidth = 100, + desiredHeight = 200, + darkMode = false + ) + ).isEqualTo( + "https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200.webp?key=fU3vlMsMn4Jb6dnEIFsx" + ) + } + + @Test + fun `buildStaticMapsApiUrl builds dark mode url`() { + assertThat( + buildStaticMapsApiUrl( + lat = 1.234, + lon = 5.678, + desiredZoom = 1.2, + desiredWidth = 100, + desiredHeight = 200, + darkMode = true + ) + ).isEqualTo( + "https://api.maptiler.com/maps/dea61faf-292b-4774-9660-58fcef89a7f3/static/5.678,1.234,1.2/100x200.webp?key=fU3vlMsMn4Jb6dnEIFsx" + ) + } + + @Test + fun `buildStaticMapsApiUrl coerces zoom at 22 and width and height at max 2048 keeping aspect ratio`() { + assertThat( + buildStaticMapsApiUrl( + lat = 1.234, + lon = 5.678, + desiredZoom = 100.0, + desiredWidth = 8192, + desiredHeight = 4096, + darkMode = false + ) + ).isEqualTo( + "https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,22.0/2048x1024.webp?key=fU3vlMsMn4Jb6dnEIFsx" + ) + } +} diff --git a/features/location/fake/build.gradle.kts b/features/location/fake/build.gradle.kts new file mode 100644 index 0000000000..cceab3f2b7 --- /dev/null +++ b/features/location/fake/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 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. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "io.element.android.features.location.fake" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + api(projects.features.location.api) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.di) + implementation(projects.libraries.network) + implementation(projects.libraries.core) + implementation(libs.maplibre) + implementation(libs.network.retrofit) + implementation(libs.maplibre.annotation) + implementation(libs.coil.compose) + implementation(libs.serialization.json) + implementation(libs.accompanist.permission) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) +} diff --git a/features/location/fake/src/main/kotlin/io/element/android/features/location/fake/LocationUpdatesFlowFake.kt b/features/location/fake/src/main/kotlin/io/element/android/features/location/fake/LocationUpdatesFlowFake.kt new file mode 100644 index 0000000000..c3f070acbb --- /dev/null +++ b/features/location/fake/src/main/kotlin/io/element/android/features/location/fake/LocationUpdatesFlowFake.kt @@ -0,0 +1,35 @@ +/* + * 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.fake + +import io.element.android.features.location.api.Location +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +fun fakeLocationUpdatesFlow(): Flow = flow { + while (true) { + delay(1_000) + emit(aLocation()) + } +} + +private fun aLocation() = Location( + lat = 51.49404, + lon = -0.25484, + accuracy = 5f +) diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts new file mode 100644 index 0000000000..66d29fd6bb --- /dev/null +++ b/features/location/impl/build.gradle.kts @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 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. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.ksp) +} + +android { + namespace = "io.element.android.features.location.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + api(projects.features.location.api) + implementation(projects.libraries.di) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.network) + implementation(projects.libraries.core) + implementation(libs.maplibre) + implementation(libs.network.retrofit) + implementation(libs.maplibre.annotation) + implementation(libs.coil.compose) + implementation(libs.serialization.json) + implementation(libs.accompanist.permission) + ksp(libs.showkase.processor) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) +} diff --git a/features/location/impl/src/main/AndroidManifest.xml b/features/location/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..b4f5d8f271 --- /dev/null +++ b/features/location/impl/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/LocationUpdatesFlowImpl.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/LocationUpdatesFlowImpl.kt new file mode 100644 index 0000000000..11b1e2a02d --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/LocationUpdatesFlowImpl.kt @@ -0,0 +1,96 @@ +/* + * 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.impl + +import android.Manifest +import android.content.Context +import android.location.LocationManager +import androidx.annotation.RequiresPermission +import androidx.core.content.getSystemService +import androidx.core.location.LocationListenerCompat +import androidx.core.location.LocationManagerCompat +import androidx.core.location.LocationRequestCompat +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.features.location.api.Location +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow + +/** + * Returns a cold [Flow] that, once collected, emits [Location] updates every second. + */ +@RequiresPermission( + anyOf = [ + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION + ] +) +fun locationUpdatesFlow( + context: Context, + coroutineDispatchers: CoroutineDispatchers, +): Flow = callbackFlow { + val locationManager: LocationManager = checkNotNull(context.getSystemService()) + val provider = locationManager.bestAvailableProvider() + // Try to eagerly emit the last known location as fast as possible + locationManager.getLastKnownLocation(provider)?.let { location -> + trySendBlocking( + Location( + lat = location.latitude, + lon = location.longitude, + accuracy = location.accuracy + ) + ) + } + val locationListener = LocationListenerCompat { location -> + trySendBlocking( + Location( + lat = location.latitude, + lon = location.longitude, + accuracy = location.accuracy + ) + ) + } + LocationManagerCompat.requestLocationUpdates( + locationManager, + provider, + buildLocationRequest(), + coroutineDispatchers.io.asExecutor(), + locationListener, + ) + awaitClose { + LocationManagerCompat.removeUpdates(locationManager, locationListener) + } +} + +private fun LocationManager.bestAvailableProvider(): String = + checkNotNull(getProviders(true).maxByOrNull { providerPriority(it) }) { + "No location provider available" + } + +private fun providerPriority(provider: String): Int = when (provider) { + LocationManager.FUSED_PROVIDER -> 4 + LocationManager.GPS_PROVIDER -> 3 + LocationManager.NETWORK_PROVIDER -> 2 + LocationManager.PASSIVE_PROVIDER -> 1 + else -> 0 +} + +private fun buildLocationRequest() = LocationRequestCompat.Builder(1_000).apply { + setMinUpdateIntervalMillis(1_000) +}.build() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a4d549f77..2bf327ed58 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -155,6 +155,8 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0" telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } statemachine = "com.freeletics.flowredux:compose:1.1.0" +maplibre = "org.maplibre.gl:android-sdk:10.2.0" +maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:1.0.0" # Analytics posthog = "com.posthog.android:posthog:2.0.3" @@ -186,6 +188,7 @@ android_application = { id = "com.android.application", version.ref = "android_g android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" } kotlin_android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin_jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } anvil = { id = "com.squareup.anvil", version.ref = "anvil" } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..04f26d7d81 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:407a17fca1405575861b7c8861222a5d529778eeaeb904c3058fe19ff9f809fc +size 143328 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6aebd55728 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95068257a39fc8a693adce87c54b605be395de5fd639ee00b7d34dde5bce568a +size 147055 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..97787cadcd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:819c585286d2ef5c7064766d537b9da62be54b67340a5a7a44e94dcf53b1caf4 +size 277318 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7efba0b598 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0e68e68e1cf4f735671b5abcfb4e0c936ca1a00ead817e8f010d39f5b753252 +size 280801 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..78ba79757e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d34ac54f8c46bc3752366adeefb0952b23f052f9359b48864a6a43455334a6d +size 4965 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..04f26d7d81 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:407a17fca1405575861b7c8861222a5d529778eeaeb904c3058fe19ff9f809fc +size 143328 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..97787cadcd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:819c585286d2ef5c7064766d537b9da62be54b67340a5a7a44e94dcf53b1caf4 +size 277318