Static images improvements (#933)
1. On devices less than xhdpi request a 1x image from MapTiler (such devices are generally old, slower and with little memory so avoiding to get the 2x image only to have to shrink it later could help). 2. Coerce too big width/height combos within the API limits keeping the aspect ratio (this will allow requests on big horizontal displays to succeed). 3. Don't crash when given weird width/height combos (i.e. zero or negative). 4. Introduce interfaces to hide this whole logic and make it easier for forks to implement their own. Related to: - https://github.com/vector-im/element-meta/issues/1678
This commit is contained in:
15
docs/maps.md
15
docs/maps.md
@@ -27,16 +27,21 @@ Place your API key in `local.properties` with the key
|
|||||||
services.maptiler.apikey=abCd3fGhijK1mN0pQr5t
|
services.maptiler.apikey=abCd3fGhijK1mN0pQr5t
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optionally you can also place your custom MapTyler style ids for light and dark maps
|
||||||
|
in the `local.properties` with the keys `services.maptiler.lightMapId` and
|
||||||
|
`services.maptiler.darkMapId`. If you don't specify these, the default MapTiler "basic-v2"
|
||||||
|
styles will be used.
|
||||||
|
|
||||||
## Making releasable builds with MapTiler
|
## Making releasable builds with MapTiler
|
||||||
|
|
||||||
To insert the MapTiler API key when building an APK, set the
|
To insert the MapTiler API key when building an APK, set the
|
||||||
`ELEMENT_ANDROID_MAPTILER_API_KEY` environment variable in your build
|
`ELEMENT_ANDROID_MAPTILER_API_KEY` environment variable in your build
|
||||||
environment.
|
environment.
|
||||||
|
If you've added custom styles also set the `ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID`
|
||||||
|
and `ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID` environment variables accordingly.
|
||||||
|
|
||||||
## Using other map sources or MapTiler styles
|
## Using other map sources or MapTiler styles
|
||||||
|
|
||||||
If you wish to use an alternative map provider, or custom MapTiler styles,
|
If you wish to use an alternative map provider, you can provide your own implementations of
|
||||||
you can customise the functions in
|
`TileServerStyleUriBuilder` and `StaticMapUrlBuilder` in
|
||||||
`features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt`.
|
`features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/`.
|
||||||
We've kept this file small and self contained to minimise the chances of merge
|
|
||||||
collisions in forks.
|
|
||||||
|
|||||||
@@ -29,16 +29,16 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImagePainter
|
import coil.compose.AsyncImagePainter
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
||||||
|
import io.element.android.features.location.api.internal.StaticMapUrlBuilder
|
||||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||||
import io.element.android.features.location.api.internal.staticMapUrl
|
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.text.toDp
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -65,23 +65,22 @@ fun StaticMapView(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var retryHash by remember { mutableStateOf(0) }
|
var retryHash by remember { mutableStateOf(0) }
|
||||||
|
val builder = remember { StaticMapUrlBuilder(context) }
|
||||||
val painter = rememberAsyncImagePainter(
|
val painter = rememberAsyncImagePainter(
|
||||||
model = if (constraints.isZero) {
|
model = if (constraints.isZero) {
|
||||||
// Avoid building a URL if any of the size constraints is zero (else it will thrown an exception).
|
// Avoid building a URL if any of the size constraints is zero (else it will thrown an exception).
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
ImageRequest.Builder(LocalContext.current)
|
ImageRequest.Builder(context)
|
||||||
.data(
|
.data(
|
||||||
staticMapUrl(
|
builder.build(
|
||||||
context = context,
|
|
||||||
lat = lat,
|
lat = lat,
|
||||||
lon = lon,
|
lon = lon,
|
||||||
zoom = zoom,
|
zoom = zoom,
|
||||||
darkMode = darkMode,
|
darkMode = darkMode,
|
||||||
// Size the map based on DP rather than pixels, as otherwise the features and attribution
|
width = constraints.maxWidth,
|
||||||
// end up being illegibly tiny on high density displays.
|
height = constraints.maxHeight,
|
||||||
width = constraints.maxWidth.toDp().value.toInt(),
|
density = LocalDensity.current.density,
|
||||||
height = constraints.maxHeight.toDp().value.toInt(),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.size(width = constraints.maxWidth, height = constraints.maxHeight)
|
.size(width = constraints.maxWidth, height = constraints.maxHeight)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.content.Context
|
||||||
|
import io.element.android.features.location.api.R
|
||||||
|
|
||||||
|
internal const val MAPTILER_BASE_URL = "https://api.maptiler.com/maps"
|
||||||
|
|
||||||
|
internal fun Context.mapId(darkMode: Boolean) = when (darkMode) {
|
||||||
|
true -> getString(R.string.maptiler_dark_map_id)
|
||||||
|
false -> getString(R.string.maptiler_light_map_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val Context.apiKey: String
|
||||||
|
get() = getString(R.string.maptiler_api_key)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.content.Context
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an URL for MapTiler's Static Maps API.
|
||||||
|
*
|
||||||
|
* https://docs.maptiler.com/cloud/api/static-maps/
|
||||||
|
*/
|
||||||
|
internal class MapTilerStaticMapUrlBuilder(
|
||||||
|
private val apiKey: String,
|
||||||
|
private val lightMapId: String,
|
||||||
|
private val darkMapId: String,
|
||||||
|
) : StaticMapUrlBuilder {
|
||||||
|
|
||||||
|
constructor(context: Context) : this(
|
||||||
|
apiKey = context.apiKey,
|
||||||
|
lightMapId = context.mapId(darkMode = false),
|
||||||
|
darkMapId = context.mapId(darkMode = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun build(
|
||||||
|
lat: Double,
|
||||||
|
lon: Double,
|
||||||
|
zoom: Double,
|
||||||
|
darkMode: Boolean,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
density: Float
|
||||||
|
): String {
|
||||||
|
val mapId = if (darkMode) darkMapId else lightMapId
|
||||||
|
val zoom = zoom.coerceIn(zoomRange)
|
||||||
|
|
||||||
|
// Request @2x density for xhdpi and above (xhdpi == 320dpi == 2x density).
|
||||||
|
val is2x = density >= 2
|
||||||
|
|
||||||
|
// Scale requested width/height according to the reported display density.
|
||||||
|
val (width, height) = coerceWidthAndHeight(
|
||||||
|
width = (width / density).roundToInt(),
|
||||||
|
height = (height / density).roundToInt(),
|
||||||
|
is2x = is2x,
|
||||||
|
)
|
||||||
|
|
||||||
|
val scale = if (is2x) "@2x" else ""
|
||||||
|
|
||||||
|
// Since Maptiler doesn't support arbitrary dpi scaling, we stick to 2x sized
|
||||||
|
// images even on displays with density higher than 2x, thereby yielding an
|
||||||
|
// image smaller than the available space in pixels.
|
||||||
|
// The resulting image will have to be scaled to fit the available space in order
|
||||||
|
// to keep the perceived content size constant at the expense of sharpness.
|
||||||
|
return "$MAPTILER_BASE_URL/${mapId}/static/${lon},${lat},${zoom}/${width}x${height}${scale}.webp?key=${apiKey}&attribution=bottomleft"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun coerceWidthAndHeight(width: Int, height: Int, is2x: Boolean): Pair<Int, Int> {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
// This effectively yields an URL which asks for a 0x0 image which will result in an HTTP error,
|
||||||
|
// but it's better than e.g. asking for a 1x1 image which would be unreadable and increase usage costs.
|
||||||
|
return 0 to 0
|
||||||
|
}
|
||||||
|
val aspectRatio = width.toDouble() / height.toDouble()
|
||||||
|
val range = if (is2x) widthHeightRange2x else widthHeightRange
|
||||||
|
return if (width >= height) {
|
||||||
|
width.coerceIn(range).let { width ->
|
||||||
|
width to (width / aspectRatio).roundToInt()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
height.coerceIn(range).let { height ->
|
||||||
|
(height * aspectRatio).roundToInt() to height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val widthHeightRange = 1..2048 // API will error if outside 1-2048 range @1x.
|
||||||
|
private val widthHeightRange2x = 1..1024 // API will error if outside 1-1024 range @2x.
|
||||||
|
private val zoomRange = 0.0..22.0 // API will error if outside 0-22 range.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:JvmName("TileServerStyleUriBuilderKt")
|
||||||
|
|
||||||
|
package io.element.android.features.location.api.internal
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
internal class MapTilerTileServerStyleUriBuilder(
|
||||||
|
private val apiKey: String,
|
||||||
|
private val lightMapId: String,
|
||||||
|
private val darkMapId: String,
|
||||||
|
) : TileServerStyleUriBuilder {
|
||||||
|
|
||||||
|
constructor(context: Context) : this(
|
||||||
|
apiKey = context.apiKey,
|
||||||
|
lightMapId = context.mapId(darkMode = false),
|
||||||
|
darkMapId = context.mapId(darkMode = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun build(darkMode: Boolean): String {
|
||||||
|
val mapId = if (darkMode) darkMapId else lightMapId
|
||||||
|
return "${MAPTILER_BASE_URL}/${mapId}/style.json?key=${apiKey}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 android.content.Context
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import io.element.android.features.location.api.R
|
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the URL to an image that contains a statically-generated map of the given location.
|
|
||||||
*/
|
|
||||||
fun staticMapUrl(
|
|
||||||
context: Context,
|
|
||||||
lat: Double,
|
|
||||||
lon: Double,
|
|
||||||
zoom: Double,
|
|
||||||
width: Int,
|
|
||||||
height: Int,
|
|
||||||
darkMode: Boolean,
|
|
||||||
): String {
|
|
||||||
return "${context.baseUrl(darkMode)}/static/${lon},${lat},${zoom}/${width}x${height}@2x.webp?key=${context.apiKey}&attribution=bottomleft"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to remember the tile server URL based on the current theme.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun rememberTileStyleUrl(): String {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val darkMode = !ElementTheme.isLightTheme
|
|
||||||
return remember(darkMode) {
|
|
||||||
tileStyleUrl(
|
|
||||||
context = context,
|
|
||||||
darkMode = darkMode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the URL to a MapLibre style document, used for rendering dynamic maps.
|
|
||||||
*/
|
|
||||||
private fun tileStyleUrl(
|
|
||||||
context: Context,
|
|
||||||
darkMode: Boolean,
|
|
||||||
): String {
|
|
||||||
return "${context.baseUrl(darkMode)}/style.json?key=${context.apiKey}"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Context.baseUrl(darkMode: Boolean) =
|
|
||||||
"https://api.maptiler.com/maps/" +
|
|
||||||
if (darkMode)
|
|
||||||
getString(R.string.maptiler_dark_map_id)
|
|
||||||
else
|
|
||||||
getString(R.string.maptiler_light_map_id)
|
|
||||||
|
|
||||||
private val Context.apiKey: String
|
|
||||||
get() = getString(R.string.maptiler_api_key)
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.content.Context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an URL for a 3rd party service provider static maps API.
|
||||||
|
*/
|
||||||
|
interface StaticMapUrlBuilder {
|
||||||
|
fun build(
|
||||||
|
lat: Double,
|
||||||
|
lon: Double,
|
||||||
|
zoom: Double,
|
||||||
|
darkMode: Boolean,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
density: Float,
|
||||||
|
): String
|
||||||
|
}
|
||||||
|
|
||||||
|
fun StaticMapUrlBuilder(context: Context): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder(context = context)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.content.Context
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a style URI for a MapLibre compatible tile server.
|
||||||
|
*
|
||||||
|
* Used for rendering dynamic maps.
|
||||||
|
*/
|
||||||
|
interface TileServerStyleUriBuilder {
|
||||||
|
fun build(
|
||||||
|
darkMode: Boolean,
|
||||||
|
): String
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TileServerStyleUriBuilder(context: Context): TileServerStyleUriBuilder = MapTilerTileServerStyleUriBuilder(context = context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides and remembers a style URI for a MapLibre compatible tile server.
|
||||||
|
*
|
||||||
|
* Used for rendering dynamic maps.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberTileStyleUrl(): String {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val darkMode = !ElementTheme.isLightTheme
|
||||||
|
return remember(darkMode) {
|
||||||
|
TileServerStyleUriBuilder(context).build(darkMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.junit.Test
|
||||||
|
|
||||||
|
class MapTilerStaticMapUrlBuilderTest {
|
||||||
|
|
||||||
|
private val builder = MapTilerStaticMapUrlBuilder(
|
||||||
|
apiKey = "anApiKey",
|
||||||
|
lightMapId = "aLightMapId",
|
||||||
|
darkMapId = "aDarkMapId",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `static map 1x density`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 800,
|
||||||
|
height = 600,
|
||||||
|
density = 1f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `static map 1,5x density`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 1200,
|
||||||
|
height = 900,
|
||||||
|
density = 1.5f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `static map 2x density`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 1600,
|
||||||
|
height = 1200,
|
||||||
|
density = 2f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `static map 3x density`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 2400,
|
||||||
|
height = 1800,
|
||||||
|
density = 3f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `too big image is coerced keeping aspect ratio`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 4096,
|
||||||
|
height = 2048,
|
||||||
|
density = 1f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/2048x1024.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 2048,
|
||||||
|
height = 4096,
|
||||||
|
density = 1f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x2048.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 4096,
|
||||||
|
height = 2048,
|
||||||
|
density = 2f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x512@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 2048,
|
||||||
|
height = 4096,
|
||||||
|
density = 2f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/512x1024@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = Int.MAX_VALUE,
|
||||||
|
height = Int.MAX_VALUE,
|
||||||
|
density = 2f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x1024@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `too small image is coerced to 0x0`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 0,
|
||||||
|
height = 0,
|
||||||
|
density = 1f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 0,
|
||||||
|
height = 0,
|
||||||
|
density = 2f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = Int.MIN_VALUE,
|
||||||
|
height = Int.MIN_VALUE,
|
||||||
|
density = 1f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.junit.Test
|
||||||
|
|
||||||
|
class MapTilerTileServerStyleUriBuilderTest {
|
||||||
|
|
||||||
|
private val builder = MapTilerTileServerStyleUriBuilder(
|
||||||
|
apiKey = "anApiKey",
|
||||||
|
lightMapId = "aLightMapId",
|
||||||
|
darkMapId = "aDarkMapId",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `light map uri`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(darkMode = false)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/style.json?key=anApiKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dark map uri`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(darkMode = true)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aDarkMapId/style.json?key=anApiKey")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,6 @@ import com.mapbox.mapboxsdk.camera.CameraPosition
|
|||||||
import com.mapbox.mapboxsdk.geometry.LatLng
|
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
||||||
import io.element.android.features.location.impl.MapDefaults
|
import io.element.android.features.location.impl.MapDefaults
|
||||||
import io.element.android.features.location.impl.send.SendLocationState
|
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|||||||
Reference in New Issue
Block a user