Refactor the MapTiler configuration into a single place. (#3944)

* Refactor the MapTiler configuration into a single place.

* Merge the MapAssets catalog into the normal one.
This commit is contained in:
Doug
2025-03-26 14:39:09 +00:00
committed by GitHub
parent e0edafc48e
commit 51886b0c78
31 changed files with 190 additions and 220 deletions
@@ -1,14 +0,0 @@
//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
extension MapTilerStyleBuilder {
init(appSettings: AppSettings) {
self.init(baseURL: appSettings.mapTilerBaseURL, key: appSettings.mapTilerApiKey)
}
}
@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,22 +0,0 @@
{
"images" : [
{
"filename" : "lightMapBlurred.pdf",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "DarkMapBlurred.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -34,7 +34,7 @@ struct MapLibreMapView: UIViewRepresentable {
@Environment(\.colorScheme) private var colorScheme
let builder: MapTilerStyleBuilderProtocol
let mapURLBuilder: MapTilerURLBuilderProtocol
let options: Options
@@ -64,7 +64,7 @@ struct MapLibreMapView: UIViewRepresentable {
// Don't set the same value twice. Otherwise, if there is an error loading the map, a loop
// is caused as the `error` binding being set, which triggers this update, which sets a
// new URL, which causes another error, and so it goes on round and round in a circle.
let dynamicMapURL = builder.dynamicMapURL(for: .init(colorScheme))
let dynamicMapURL = mapURLBuilder.dynamicMapURL(for: .init(colorScheme))
if mapView.styleURL != dynamicMapURL {
mapView.styleURL = dynamicMapURL
}
@@ -85,7 +85,7 @@ struct MapLibreMapView: UIViewRepresentable {
}
private func makeMapView() -> MGLMapView {
let mapView = MGLMapView(frame: .zero, styleURL: colorScheme == .dark ? builder.dynamicMapURL(for: .dark) : builder.dynamicMapURL(for: .light))
let mapView = MGLMapView(frame: .zero, styleURL: mapURLBuilder.dynamicMapURL(for: colorScheme == .dark ? .dark : .light))
mapView.logoViewPosition = .topLeft
mapView.attributionButtonPosition = .topLeft
mapView.attributionButtonMargins = .init(x: mapView.logoView.frame.maxX + 8, y: mapView.logoView.center.y / 2)
@@ -24,6 +24,14 @@ enum MapLibreError: Error {
case failedLocatingUser
}
/// The style to show a map in.
///
/// There can be any number of styles, we have defined one for light and another for dark.
enum MapTilerStyle {
case light
case dark
}
enum MapTilerAttributionPlacement: String {
case bottomRight = "bottomright"
case bottomLeft = "bottomleft"
@@ -11,7 +11,7 @@ import SwiftUI
struct MapLibreStaticMapView<PinAnnotation: View>: View {
private let coordinates: CLLocationCoordinate2D
private let zoomLevel: Double
private let mapTilerStatic: MapTilerStaticMapProtocol
private let mapURLBuilder: MapTilerURLBuilderProtocol
private let mapTilerAttributionPlacement: MapTilerAttributionPlacement
private let mapSize: CGSize
private let pinAnnotationView: PinAnnotation
@@ -22,12 +22,12 @@ struct MapLibreStaticMapView<PinAnnotation: View>: View {
init(coordinates: CLLocationCoordinate2D,
zoomLevel: Double,
attributionPlacement: MapTilerAttributionPlacement,
mapTilerStatic: MapTilerStaticMapProtocol,
mapURLBuilder: MapTilerURLBuilderProtocol,
mapSize: CGSize,
@ViewBuilder pinAnnotationView: () -> PinAnnotation) {
self.coordinates = coordinates
self.zoomLevel = zoomLevel
self.mapTilerStatic = mapTilerStatic
self.mapURLBuilder = mapURLBuilder
mapTilerAttributionPlacement = attributionPlacement
self.mapSize = mapSize
self.pinAnnotationView = pinAnnotationView()
@@ -35,11 +35,11 @@ struct MapLibreStaticMapView<PinAnnotation: View>: View {
var body: some View {
GeometryReader { geometry in
if let url = mapTilerStatic.staticMapURL(for: colorScheme.mapStyle,
coordinates: coordinates,
zoomLevel: zoomLevel,
size: mapSize, // temporary using a fixed size since the refresh doesn't work properly on the UITableView based timeline
attribution: mapTilerAttributionPlacement) {
if let url = mapURLBuilder.staticMapURL(for: colorScheme.mapStyle,
coordinates: coordinates,
zoomLevel: zoomLevel,
size: mapSize, // temporary using a fixed size since the refresh doesn't work properly on the UITableView based timeline
attribution: mapTilerAttributionPlacement) {
AsyncImage(url: url) { phase in
switch phase {
case .empty:
@@ -66,7 +66,7 @@ struct MapLibreStaticMapView<PinAnnotation: View>: View {
}
private var placeholderImage: some View {
Image("mapBlurred")
Image(asset: Asset.Images.mapBlurred)
.resizable()
.scaledToFill()
}
@@ -104,15 +104,22 @@ struct MapLibreStaticMapView_Previews: PreviewProvider, TestablePreview {
MapLibreStaticMapView(coordinates: CLLocationCoordinate2D(),
zoomLevel: 15,
attributionPlacement: .bottomLeft,
mapTilerStatic: MapTilerStaticMapMock(), mapSize: .init(width: 300, height: 200)) {
mapURLBuilder: MapTilerURLBuilderMock(),
mapSize: .init(width: 300, height: 200)) {
Image(systemName: "mappin.circle.fill")
.padding(.bottom, 35)
}
}
}
private struct MapTilerStaticMapMock: MapTilerStaticMapProtocol {
func staticMapURL(for style: MapTilerStyle, coordinates: CLLocationCoordinate2D, zoomLevel: Double, size: CGSize, attribution: MapTilerAttributionPlacement) -> URL? {
private struct MapTilerURLBuilderMock: MapTilerURLBuilderProtocol {
func dynamicMapURL(for style: MapTilerStyle) -> URL? { nil }
func staticMapURL(for style: MapTilerStyle,
coordinates: CLLocationCoordinate2D,
zoomLevel: Double,
size: CGSize,
attribution: MapTilerAttributionPlacement) -> URL? {
switch style {
case .light:
return URL(string: "https://www.maptiler.com/img/cloud/home/map5.webp")
@@ -1,20 +0,0 @@
//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
struct MapTilerAuthorization {
private let key: String
init(key: String) {
self.key = key
}
func authorizeURL(_ url: URL) -> URL {
url.appending(queryItems: [URLQueryItem(name: "key", value: key)])
}
}
@@ -0,0 +1,62 @@
//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import CoreLocation
/// All of the configuration necessary to use MapTiler maps.
///
/// The style IDs need to be generated with the account that the API key belongs to. For more information read
/// [FORKING.md](https://github.com/element-hq/element-x-ios/blob/develop/docs/FORKING.md#setup-the-location-sharing)
struct MapTilerConfiguration {
let baseURL: URL
let apiKey: String
/// A MapLibre style ID for a light-mode map.
let lightStyleID: String
/// A MapLibre style ID for a dark-mode map.
let darkStyleID: String
}
extension MapTilerConfiguration: MapTilerURLBuilderProtocol {
func dynamicMapURL(for style: MapTilerStyle) -> URL? {
var url = makeNewURL(for: style)
url.appendPathComponent("style.json", conformingTo: .json)
return url
}
func staticMapURL(for style: MapTilerStyle,
coordinates: CLLocationCoordinate2D,
zoomLevel: Double,
size: CGSize,
attribution: MapTilerAttributionPlacement) -> URL? {
var url = makeNewURL(for: style)
url.appendPathComponent(String(format: "static/%f,%f,%f/%dx%d@2x.png",
coordinates.longitude,
coordinates.latitude,
zoomLevel,
Int(size.width),
Int(size.height)),
conformingTo: .png)
url.append(queryItems: [.init(name: "attribution", value: attribution.rawValue)])
return url
}
// MARK: Private
private func makeNewURL(for style: MapTilerStyle) -> URL {
var url: URL = baseURL
url.appendPathComponent(styleID(for: style), conformingTo: .item)
url.append(queryItems: [URLQueryItem(name: "key", value: apiKey)])
return url
}
private func styleID(for style: MapTilerStyle) -> String {
switch style {
case .light: lightStyleID
case .dark: darkStyleID
}
}
}
@@ -1,27 +0,0 @@
//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import CoreLocation
struct MapTilerStaticMap: MapTilerStaticMapProtocol {
private let baseURL: URL
private let key: String
init(baseURL: URL, key: String) {
self.baseURL = baseURL
self.key = key
}
func staticMapURL(for style: MapTilerStyle, coordinates: CLLocationCoordinate2D, zoomLevel: Double, size: CGSize, attribution: MapTilerAttributionPlacement) -> URL? {
var url: URL = baseURL
url.appendPathComponent(style.rawValue, conformingTo: .item)
url.appendPathComponent(String(format: "static/%f,%f,%f/%dx%d@2x.png", coordinates.longitude, coordinates.latitude, zoomLevel, Int(size.width), Int(size.height)), conformingTo: .png)
url.append(queryItems: [.init(name: "attribution", value: attribution.rawValue)])
let authorization = MapTilerAuthorization(key: key)
return authorization.authorizeURL(url)
}
}
@@ -1,14 +0,0 @@
//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
/// The style for a map.
/// Values should be Map Libre style IDs generated with an account where the API key belongs to.
/// For more information read [FORKING.md](https://github.com/element-hq/element-x-ios/blob/develop/docs/FORKING.md#setup-the-location-sharing).
enum MapTilerStyle: String {
case light = "9bc819c8-e627-474a-a348-ec144fe3d810"
case dark = "dea61faf-292b-4774-9660-58fcef89a7f3"
}
@@ -1,26 +0,0 @@
//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
struct MapTilerStyleBuilder: MapTilerStyleBuilderProtocol {
private let baseURL: URL
private let key: String
init(baseURL: URL, key: String) {
self.baseURL = baseURL
self.key = key
}
func dynamicMapURL(for style: MapTilerStyle) -> URL? {
var url: URL = baseURL
url.appendPathComponent(style.rawValue, conformingTo: .item)
url.appendPathComponent("style.json", conformingTo: .json)
let authorization = MapTilerAuthorization(key: key)
return authorization.authorizeURL(url)
}
}
@@ -1,12 +0,0 @@
//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
protocol MapTilerStyleBuilderProtocol {
func dynamicMapURL(for style: MapTilerStyle) -> URL?
}
@@ -7,7 +7,9 @@
import CoreLocation
protocol MapTilerStaticMapProtocol {
protocol MapTilerURLBuilderProtocol {
func dynamicMapURL(for style: MapTilerStyle) -> URL?
func staticMapURL(for style: MapTilerStyle,
coordinates: CLLocationCoordinate2D,
zoomLevel: Double,