Browse Source
In an effort to make it easier for forks to (a) use their own API keys (b) change map styles or maybe even providers, move the MapTiler key out of the source code and pass it in via env var or property. Also refactor the utility classes slightly to keep all the URL related functions together, to reduce the chance of collisions when maintaining such forks.pull/886/head
Chris Smith
1 year ago
9 changed files with 128 additions and 218 deletions
@ -0,0 +1,42 @@ |
|||||||
|
# Use of maps |
||||||
|
|
||||||
|
<!--- TOC --> |
||||||
|
|
||||||
|
* [Overview](#overview) |
||||||
|
* [Local development with MapTiler](#local-development-with-maptiler) |
||||||
|
* [Making releasable builds with MapTiler](#making-releasable-builds-with-maptiler) |
||||||
|
* [Using other map sources or MapTiler styles](#using-other-map-sources-or-maptiler-styles) |
||||||
|
|
||||||
|
<!--- END --> |
||||||
|
|
||||||
|
## Overview |
||||||
|
|
||||||
|
Element Android uses [MapTiler](https://www.maptiler.com/) to provide map |
||||||
|
imagery where required. MapTiler requires an API key, which we bake in to |
||||||
|
the app at release time. |
||||||
|
|
||||||
|
## Local development with MapTiler |
||||||
|
|
||||||
|
If you're developing the application and want maps to render properly you can |
||||||
|
sign up for the [MapTiler free tier](https://www.maptiler.com/cloud/pricing/). |
||||||
|
|
||||||
|
Place your API key in `local.properties` with the key |
||||||
|
`services.maptiler.apikey`, e.g.: |
||||||
|
|
||||||
|
```properties |
||||||
|
services.maptiler.apikey=abCd3fGhijK1mN0pQr5t |
||||||
|
``` |
||||||
|
|
||||||
|
## Making releasable builds with MapTiler |
||||||
|
|
||||||
|
To insert the MapTiler API key when building an APK, set the |
||||||
|
`ELEMENT_ANDROID_MAPTILER_API_KEY` environment variable in your build |
||||||
|
environment. |
||||||
|
|
||||||
|
## Using other map sources or MapTiler styles |
||||||
|
|
||||||
|
If you wish to use an alternative map provider, or custom MapTiler styles, |
||||||
|
you can customise the functions in |
||||||
|
`features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt`. |
||||||
|
We've kept this file small and self contained to minimise the chances of merge |
||||||
|
collisions in forks. |
@ -0,0 +1,55 @@ |
|||||||
|
/* |
||||||
|
* 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 |
||||||
|
|
||||||
|
/** |
||||||
|
* 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 "${baseUrl(darkMode)}/static/${lon},${lat},${zoom}/${width}x${height}@2x.webp?key=${context.apiKey}&attribution=bottomleft" |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Provides the URL to a MapLibre style document, used for rendering dynamic maps. |
||||||
|
*/ |
||||||
|
fun tileStyleUrl( |
||||||
|
context: Context, |
||||||
|
darkMode: Boolean, |
||||||
|
): String { |
||||||
|
return "${baseUrl(darkMode)}/style.json?key=${context.apiKey}" |
||||||
|
} |
||||||
|
|
||||||
|
private fun baseUrl(darkMode: Boolean) = |
||||||
|
"https://api.maptiler.com/maps/" + |
||||||
|
if (darkMode) |
||||||
|
"dea61faf-292b-4774-9660-58fcef89a7f3" |
||||||
|
else |
||||||
|
"9bc819c8-e627-474a-a348-ec144fe3d810" |
||||||
|
|
||||||
|
private val Context.apiKey: String |
||||||
|
get() = getString(R.string.maptiler_api_key) |
@ -1,91 +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 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_2X = "@2x" |
|
||||||
private const val STATIC_MAP_MAX_WIDTH_HEIGHT = 2048 |
|
||||||
private const val STATIC_MAP_MAX_ZOOM = 22.0 |
|
||||||
|
|
||||||
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" |
|
||||||
} |
|
||||||
|
|
||||||
internal enum class AttributionPlacement(val value: String) { |
|
||||||
BottomRight("bottomright"), |
|
||||||
BottomLeft("bottomleft"), |
|
||||||
TopLeft("topleft"), |
|
||||||
TopRight("topright"), |
|
||||||
Hidden("false"), |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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, |
|
||||||
doubleScale: Boolean, |
|
||||||
attributionPlacement: AttributionPlacement, |
|
||||||
): 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() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
val mapId = if (darkMode) DARK_MAP_ID else LIGHT_MAP_ID |
|
||||||
val scaleSuffix = if (doubleScale) STATIC_MAP_SCALE_2X else "" |
|
||||||
|
|
||||||
return "$BASE_URL/maps/$mapId/static/${lon},${lat},${zoom}/${width}x${height}${scaleSuffix}.$STATIC_MAP_FORMAT" + |
|
||||||
"?key=$API_KEY&attribution=${attributionPlacement.value}" |
|
||||||
} |
|
@ -1,117 +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 com.google.common.truth.Truth.assertThat |
|
||||||
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, |
|
||||||
doubleScale = false, |
|
||||||
attributionPlacement = AttributionPlacement.BottomLeft, |
|
||||||
) |
|
||||||
).isEqualTo( |
|
||||||
"https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200.webp" + |
|
||||||
"?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft" |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `buildStaticMapsApiUrl builds dark mode url`() { |
|
||||||
assertThat( |
|
||||||
buildStaticMapsApiUrl( |
|
||||||
lat = 1.234, |
|
||||||
lon = 5.678, |
|
||||||
desiredZoom = 1.2, |
|
||||||
desiredWidth = 100, |
|
||||||
desiredHeight = 200, |
|
||||||
darkMode = true, |
|
||||||
doubleScale = false, |
|
||||||
attributionPlacement = AttributionPlacement.BottomLeft, |
|
||||||
) |
|
||||||
).isEqualTo( |
|
||||||
"https://api.maptiler.com/maps/dea61faf-292b-4774-9660-58fcef89a7f3/static/5.678,1.234,1.2/100x200.webp" + |
|
||||||
"?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft" |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `buildStaticMapsApiUrl builds double scale mode url`() { |
|
||||||
assertThat( |
|
||||||
buildStaticMapsApiUrl( |
|
||||||
lat = 1.234, |
|
||||||
lon = 5.678, |
|
||||||
desiredZoom = 1.2, |
|
||||||
desiredWidth = 100, |
|
||||||
desiredHeight = 200, |
|
||||||
darkMode = false, |
|
||||||
doubleScale = true, |
|
||||||
attributionPlacement = AttributionPlacement.BottomLeft, |
|
||||||
) |
|
||||||
).isEqualTo( |
|
||||||
"https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200@2x.webp" + |
|
||||||
"?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft" |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `buildStaticMapsApiUrl builds no attribution url`() { |
|
||||||
assertThat( |
|
||||||
buildStaticMapsApiUrl( |
|
||||||
lat = 1.234, |
|
||||||
lon = 5.678, |
|
||||||
desiredZoom = 1.2, |
|
||||||
desiredWidth = 100, |
|
||||||
desiredHeight = 200, |
|
||||||
darkMode = false, |
|
||||||
doubleScale = false, |
|
||||||
attributionPlacement = AttributionPlacement.Hidden, |
|
||||||
) |
|
||||||
).isEqualTo( |
|
||||||
"https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200.webp" + |
|
||||||
"?key=fU3vlMsMn4Jb6dnEIFsx&attribution=false" |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
@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, |
|
||||||
doubleScale = false, |
|
||||||
attributionPlacement = AttributionPlacement.BottomLeft, |
|
||||||
) |
|
||||||
).isEqualTo( |
|
||||||
"https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,22.0/2048x1024.webp" + |
|
||||||
"?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft" |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue