From 7a402b424478ccab696255fb84e574ad3b098391 Mon Sep 17 00:00:00 2001 From: Alexander Farber Date: Tue, 13 Oct 2026 16:16:41 +0200 Subject: [PATCH] Load Stamen tiles from Stadia Maps with an API key - Add a "stadia" provider to ApiKeyManager (manifest key STADIA_API_KEY) - STAMEN_TONER and STAMEN_WATERCOLOR send the key and show the usual "API key required" overlay when it is missing - Move stamenTonerHybrid() and stamenTerrainLines() from the shut down Stamen server to the Stadia lines styles - Update MAP_TYPES.md, the MapType KDoc and Example11 --- docs/MAP_TYPES.md | 113 +++++++++++------- docs/TESTING_UNIT.md | 6 +- examples/Example11MapTypes/README.md | 4 +- .../example11maptypes/MainActivity.kt | 10 +- .../de/afarber/openmapview/ApiKeyManager.kt | 17 ++- .../kotlin/de/afarber/openmapview/MapType.kt | 13 +- .../openmapview/PredefinedTileProviders.kt | 36 ++++-- .../de/afarber/openmapview/TileSource.kt | 10 +- .../PredefinedTileProvidersTest.kt | 23 +++- .../de/afarber/openmapview/TileSourceTest.kt | 33 +++++ 10 files changed, 186 insertions(+), 79 deletions(-) diff --git a/docs/MAP_TYPES.md b/docs/MAP_TYPES.md index 299c830..89334d4 100644 --- a/docs/MAP_TYPES.md +++ b/docs/MAP_TYPES.md @@ -4,7 +4,7 @@ This guide helps you choose and configure the right map type for your OpenMapVie ## Quick Start -OpenMapView provides **15 map types** from various tile providers. Eleven types work without any configuration (including `NONE`), while four premium types require free API keys. +OpenMapView provides **15 map types** from various tile providers. Nine types work without any configuration (including `NONE`), while six premium types require free API keys. ```kotlin val mapView = OpenMapView(context) @@ -147,41 +147,7 @@ Contemporary design with vibrant colors and clear labels. mapView.setMapType(MapType.CARTO_VOYAGER) ``` -#### 9. STAMEN_TONER - High-Contrast Black & White -**`MapType.STAMEN_TONER`** (constant value: 13) - -Stark black and white design with maximum contrast. - -- **Best for**: Data visualization, marker overlays, minimalist design -- **Features**: High contrast, clear roads and boundaries -- **Highlights**: Most popular Stamen style, ideal for making colored overlays highly visible -- **Tile Server**: tiles.stadiamaps.com (Stadia Maps) -- **Max Zoom**: 20 -- **Free Tier**: 200,000 tiles/month for local development -- **Attribution**: © OpenStreetMap contributors. Tiles © Stamen Design, © Stadia Maps - -```kotlin -mapView.setMapType(MapType.STAMEN_TONER) -``` - -#### 10. STAMEN_WATERCOLOR - Artistic Watercolor Rendering -**`MapType.STAMEN_WATERCOLOR`** (constant value: 14) - -Unique hand-painted watercolor aesthetic with soft colors. - -- **Best for**: Creative apps, travel journals, artistic visualization -- **Features**: Hand-painted appearance, soft color palette -- **Highlights**: Artistic interpretation of geographic features, completely unique style -- **Tile Server**: tiles.stadiamaps.com (Stadia Maps) -- **Max Zoom**: 16 -- **Free Tier**: 200,000 tiles/month for local development -- **Attribution**: © OpenStreetMap contributors. Tiles © Stamen Design, © Stadia Maps - -```kotlin -mapView.setMapType(MapType.STAMEN_WATERCOLOR) -``` - -#### 11. NONE - No Base Tiles +#### 9. NONE - No Base Tiles **`MapType.NONE`** (constant value: 0) No base map tiles displayed (useful for custom tile sources or offline maps). @@ -198,7 +164,7 @@ These map types require free API keys from third-party tile providers. When an A - Keeps the map fully interactive (pan, zoom still work) - Logs a warning to help with debugging -#### 12. CYCLEMAP - Professional Cycling Map +#### 10. CYCLEMAP - Professional Cycling Map **`MapType.CYCLEMAP`** (constant value: 3) **Requires Thunderforest API Key** Professional cycling map with elevation contours and comprehensive infrastructure. @@ -224,7 +190,7 @@ ApiKeyManager.setApiKey("thunderforest", "your_key_here") mapView.setMapType(MapType.CYCLEMAP) ``` -#### 13. TRANSPORT - Public Transit Map +#### 11. TRANSPORT - Public Transit Map **`MapType.TRANSPORT`** (constant value: 4) **Requires Thunderforest API Key** Public transport focused map with comprehensive transit information. @@ -243,7 +209,7 @@ Public transport focused map with comprehensive transit information. mapView.setMapType(MapType.TRANSPORT) ``` -#### 14. TRANSPORT_DARK - Dark Mode Public Transit Map +#### 12. TRANSPORT_DARK - Dark Mode Public Transit Map **`MapType.TRANSPORT_DARK`** (constant value: 5) **Requires Thunderforest API Key** Dark variant of the public transport map with comprehensive transit information in a dark color scheme. @@ -262,7 +228,7 @@ Dark variant of the public transport map with comprehensive transit information mapView.setMapType(MapType.TRANSPORT_DARK) ``` -#### 15. TRACESTRACK_TOPO - Topographic Map +#### 13. TRACESTRACK_TOPO - Topographic Map **`MapType.TRACESTRACK_TOPO`** (constant value: 6) **Requires Tracestrack API Key** Detailed topographic map with elevation contours and hillshading. @@ -288,6 +254,51 @@ ApiKeyManager.setApiKey("tracestrack", "your_key_here") mapView.setMapType(MapType.TRACESTRACK_TOPO) ``` +#### 14. STAMEN_TONER - High-Contrast Black & White +**`MapType.STAMEN_TONER`** (constant value: 13) **Requires Stadia Maps API Key** + +Stark black and white design with maximum contrast. + +- **Best for**: Data visualization, marker overlays, minimalist design +- **Features**: High contrast, clear roads and boundaries +- **Highlights**: Most popular Stamen style, ideal for making colored overlays highly visible +- **Tile Server**: tiles.stadiamaps.com (Stadia Maps) +- **Max Zoom**: 20 +- **Free Tier**: 200,000 tiles/month for non-commercial use +- **Get API Key**: https://client.stadiamaps.com/signup/ +- **Attribution**: © OpenStreetMap contributors. Tiles © Stamen Design, © Stadia Maps + +```kotlin +// Configure in AndroidManifest.xml: + + +// Or programmatically: +ApiKeyManager.setApiKey("stadia", "your_key_here") + +mapView.setMapType(MapType.STAMEN_TONER) +``` + +#### 15. STAMEN_WATERCOLOR - Artistic Watercolor Rendering +**`MapType.STAMEN_WATERCOLOR`** (constant value: 14) **Requires Stadia Maps API Key** + +Unique hand-painted watercolor aesthetic with soft colors. + +- **Best for**: Creative apps, travel journals, artistic visualization +- **Features**: Hand-painted appearance, soft color palette +- **Highlights**: Artistic interpretation of geographic features, completely unique style +- **Tile Server**: tiles.stadiamaps.com (Stadia Maps) +- **Max Zoom**: 16 +- **Free Tier**: 200,000 tiles/month for non-commercial use +- **Get API Key**: https://client.stadiamaps.com/signup/ +- **Attribution**: © OpenStreetMap contributors. Tiles © Stamen Design, © Stadia Maps + +```kotlin +// Same API key as STAMEN_TONER +mapView.setMapType(MapType.STAMEN_WATERCOLOR) +``` + ## API Key Configuration ### Method 1: AndroidManifest.xml (Recommended) @@ -305,6 +316,11 @@ Add API keys to your app's `AndroidManifest.xml`: + + + ``` @@ -317,6 +333,7 @@ import de.afarber.openmapview.ApiKeyManager ApiKeyManager.setApiKey("thunderforest", "your_key_here") ApiKeyManager.setApiKey("tracestrack", "your_key_here") +ApiKeyManager.setApiKey("stadia", "your_key_here") ``` ### Obtaining API Keys @@ -334,6 +351,12 @@ All providers offer generous free tiers suitable for development and small-scale - **Free Tier**: 100,000 tiles/month - **Sign Up**: Register for free account +#### Stadia Maps (STAMEN_TONER, STAMEN_WATERCOLOR) +- **Website**: https://client.stadiamaps.com/signup/ +- **Free Tier**: 200,000 tiles/month for non-commercial use +- **Sign Up**: Register for free account +- **One key works for**: STAMEN_TONER, STAMEN_WATERCOLOR and the Stamen overlays in `PredefinedTileProviders` + ### Security Best Practices 1. **Use Domain Restrictions**: Configure API keys on provider websites to restrict usage to your app's domain/bundle ID @@ -359,15 +382,15 @@ All providers offer generous free tiers suitable for development and small-scale | Emergency/humanitarian | `HUMANITARIAN` | | Data visualization (light) | `CARTO_LIGHT` | | Night mode / dark theme | `CARTO_DARK` | -| High-contrast data viz | `STAMEN_TONER` | -| Creative/artistic app | `STAMEN_WATERCOLOR` | +| High-contrast data viz | `STAMEN_TONER` (requires API key) | +| Creative/artistic app | `STAMEN_WATERCOLOR` (requires API key) | | Custom tiles only | `NONE` | ### Performance Considerations - **Free maps without limits** (STANDARD, CYCLOSM, HUMANITARIAN, OPNVKARTE, OPENTOPOMAP): No usage limits, best for production apps -- **Free maps with generous limits** (CARTO_LIGHT, CARTO_DARK, CARTO_VOYAGER, STAMEN_TONER, STAMEN_WATERCOLOR): Free tiers available (75k-200k tiles/month) -- **Premium maps** with API keys (CYCLEMAP, TRANSPORT, TRANSPORT_DARK, TRACESTRACK_TOPO): Monitor monthly tile limits (free tiers: 100k-150k tiles/month) +- **Free maps with generous limits** (CARTO_LIGHT, CARTO_DARK, CARTO_VOYAGER): Free tier available (75k tiles/month) +- **Premium maps** with API keys (CYCLEMAP, TRANSPORT, TRANSPORT_DARK, TRACESTRACK_TOPO, STAMEN_TONER, STAMEN_WATERCOLOR): Monitor monthly tile limits (free tiers: 100k-200k tiles/month) - **Tile caching**: OpenMapView automatically caches tiles in memory and on disk to minimize requests - **Subdomain rotation**: Maps with subdomain support automatically distribute load across multiple servers for better performance @@ -379,7 +402,7 @@ In addition to base map types, OpenMapView supports **tile overlays** that rende - **OpenRailwayMap**: Railway infrastructure - **OpenSnowMap**: Ski pistes, lifts - **Waymarked Trails**: Hiking, cycling, MTB, skating trails -- **Stamen Overlays**: Labels, terrain lines +- **Stamen Overlays**: Toner and terrain lines (roads, railways, borders), need a Stadia Maps API key See `PredefinedTileProviders.kt` for predefined overlay sources, or create custom overlays using `TileOverlay` and `UrlTileProvider`. diff --git a/docs/TESTING_UNIT.md b/docs/TESTING_UNIT.md index 18adea7..f35c01b 100644 --- a/docs/TESTING_UNIT.md +++ b/docs/TESTING_UNIT.md @@ -52,7 +52,7 @@ The `--continue` flag ensures all tests run even if some fail, useful for gettin ## Test Structure -### Current Test Coverage (458 tests across 35 test classes) +### Current Test Coverage (461 tests across 35 test classes) | Test Class | Tests | Description | |------------|-------|-------------| @@ -78,14 +78,14 @@ The `--continue` flag ensures all tests run even if some fail, useful for gettin | **OptionsBuildersTest** | 13 | Google-style *Options builders for markers, shapes and overlays | | **PolygonTest** | 20 | Polygon creation, validation, holes, and styling | | **PolylineTest** | 31 | Polyline creation, validation, and styling | -| **PredefinedTileProvidersTest** | 1 | URL templates of the predefined overlay tile providers | +| **PredefinedTileProvidersTest** | 2 | URL templates of the predefined overlay tile providers, Stadia key lookup | | **ProjectionTest** | 12 | Web Mercator projection calculations | | **SnapshotTest** | 8 | Map snapshot API and callbacks | | **TileCacheTest** | 9 | Memory LRU cache behavior, clearing, namespaces, disk writes | | **TileDownloaderTest** | 5 | HTTP tile downloading with mocked Ktor client, HTTP errors, cancellation | | **TileOverlayTest** | 5 | Tile overlay defaults, transparency validation, copy helpers | | **TileRetryTrackerTest** | 4 | Exponential backoff for failed tiles | -| **TileSourceTest** | 7 | Tile source URL generation and attribution | +| **TileSourceTest** | 9 | Tile source URL generation, attribution and API keys | | **TileTest** | 6 | Tile content equality, validation and toString | | **UiSettingsTest** | 25 | UI settings (gestures, zoom controls, not-implemented features) | | **UrlTileProviderTest** | 4 | Overlay tile downloading, HTTP 404 handling, shared HTTP client | diff --git a/examples/Example11MapTypes/README.md b/examples/Example11MapTypes/README.md index 48c2b1f..7693ac9 100644 --- a/examples/Example11MapTypes/README.md +++ b/examples/Example11MapTypes/README.md @@ -78,8 +78,8 @@ try { | Carto Light | `MapType.CARTO_LIGHT` | CARTO | | | Carto Dark | `MapType.CARTO_DARK` | CARTO | | | Carto Voyager | `MapType.CARTO_VOYAGER` | CARTO | | -| Stamen Toner | `MapType.STAMEN_TONER` | Stadia Maps | | -| Stamen Watercolor | `MapType.STAMEN_WATERCOLOR` | Stadia Maps | | +| Stamen Toner | `MapType.STAMEN_TONER` | Stadia Maps | Stadia Maps | +| Stamen Watercolor | `MapType.STAMEN_WATERCOLOR` | Stadia Maps | Stadia Maps | See the [Map Types Guide](../../docs/MAP_TYPES.md) for details on each type and on configuring API keys. diff --git a/examples/Example11MapTypes/src/main/kotlin/de/afarber/openmapview/example11maptypes/MainActivity.kt b/examples/Example11MapTypes/src/main/kotlin/de/afarber/openmapview/example11maptypes/MainActivity.kt index 49a64c9..67ce74b 100644 --- a/examples/Example11MapTypes/src/main/kotlin/de/afarber/openmapview/example11maptypes/MainActivity.kt +++ b/examples/Example11MapTypes/src/main/kotlin/de/afarber/openmapview/example11maptypes/MainActivity.kt @@ -265,19 +265,19 @@ fun MapTypeControls( modifier = Modifier.fillMaxWidth(), ) - // Stamen Toner + // Stamen Toner (requires API key) MapTypeButton( text = "Stamen Toner", - description = "High-contrast black & white", + description = "Black & white (API key required)", enabled = currentMapType != MapType.STAMEN_TONER, onClick = { onMapTypeChange(MapType.STAMEN_TONER) }, modifier = Modifier.fillMaxWidth(), ) - // Stamen Watercolor + // Stamen Watercolor (requires API key) MapTypeButton( text = "Stamen Watercolor", - description = "Artistic watercolor rendering", + description = "Watercolor (API key required)", enabled = currentMapType != MapType.STAMEN_WATERCOLOR, onClick = { onMapTypeChange(MapType.STAMEN_WATERCOLOR) }, modifier = Modifier.fillMaxWidth(), @@ -286,7 +286,7 @@ fun MapTypeControls( // API Key Info Text( text = "API key required: Cycle Map, Transport, Transport Dark, " + - "Tracestrack Topo\n\nConfigure in AndroidManifest.xml\nSee docs/MAP_TYPES.md", + "Tracestrack Topo, Stamen Toner, Stamen Watercolor\n\nConfigure in AndroidManifest.xml\nSee docs/MAP_TYPES.md", style = MaterialTheme.typography.bodySmall, fontSize = 9.sp, lineHeight = 11.sp, diff --git a/openmapview/src/main/kotlin/de/afarber/openmapview/ApiKeyManager.kt b/openmapview/src/main/kotlin/de/afarber/openmapview/ApiKeyManager.kt index 9a20678..cf99c18 100644 --- a/openmapview/src/main/kotlin/de/afarber/openmapview/ApiKeyManager.kt +++ b/openmapview/src/main/kotlin/de/afarber/openmapview/ApiKeyManager.kt @@ -15,7 +15,8 @@ import java.util.concurrent.ConcurrentHashMap /** * Manages API keys for third-party tile providers. * - * Some map types (CYCLEMAP, TRANSPORT, TRANSPORT_DARK, TRACESTRACK_TOPO) require API keys + * Some map types (CYCLEMAP, TRANSPORT, TRANSPORT_DARK, TRACESTRACK_TOPO, STAMEN_TONER, + * STAMEN_WATERCOLOR) and the Stamen overlays in [PredefinedTileProviders] require API keys * from tile providers. Keys can be configured in two ways: * * **1. AndroidManifest.xml** (Recommended): @@ -27,6 +28,9 @@ import java.util.concurrent.ConcurrentHashMap * + * * * ``` * @@ -34,6 +38,7 @@ import java.util.concurrent.ConcurrentHashMap * ```kotlin * ApiKeyManager.setApiKey("thunderforest", "your_key_here") * ApiKeyManager.setApiKey("tracestrack", "your_key_here") + * ApiKeyManager.setApiKey("stadia", "your_key_here") * ``` * * API keys set programmatically override keys from the manifest. @@ -41,10 +46,12 @@ import java.util.concurrent.ConcurrentHashMap * ## Provider Names * - `"thunderforest"` - For CYCLEMAP, TRANSPORT, and TRANSPORT_DARK map types * - `"tracestrack"` - For TRACESTRACK_TOPO map type + * - `"stadia"` - For STAMEN_TONER, STAMEN_WATERCOLOR and the Stamen overlays * * ## Obtaining API Keys * - **Thunderforest**: https://www.thunderforest.com/pricing/ (Free: 150k tiles/month) * - **Tracestrack**: https://www.tracestrack.com/en/signup (Free tier available) + * - **Stadia Maps**: https://client.stadiamaps.com/signup/ (Free tier for non-commercial use) * * @see TileSource * @see MapType @@ -53,6 +60,7 @@ object ApiKeyManager { private const val META_DATA_PREFIX = "de.afarber.openmapview." private const val THUNDERFOREST_KEY_NAME = "THUNDERFOREST_API_KEY" private const val TRACESTRACK_KEY_NAME = "TRACESTRACK_API_KEY" + private const val STADIA_KEY_NAME = "STADIA_API_KEY" /** * Thread-safe storage for API keys set programmatically. @@ -105,7 +113,7 @@ object ApiKeyManager { * * Keys set this way override keys from AndroidManifest.xml. * - * @param provider Provider name ("thunderforest" or "tracestrack") + * @param provider Provider name ("thunderforest", "tracestrack" or "stadia") * @param key API key string */ @Synchronized @@ -121,7 +129,7 @@ object ApiKeyManager { * * Checks programmatic keys first, then falls back to manifest meta-data. * - * @param provider Provider name ("thunderforest" or "tracestrack") + * @param provider Provider name ("thunderforest", "tracestrack" or "stadia") * @return API key string, or null if not configured */ @Synchronized @@ -136,6 +144,7 @@ object ApiKeyManager { when (normalizedProvider) { "thunderforest" -> META_DATA_PREFIX + THUNDERFOREST_KEY_NAME "tracestrack" -> META_DATA_PREFIX + TRACESTRACK_KEY_NAME + "stadia" -> META_DATA_PREFIX + STADIA_KEY_NAME else -> null } @@ -145,7 +154,7 @@ object ApiKeyManager { /** * Checks if an API key is configured for the given provider. * - * @param provider Provider name ("thunderforest" or "tracestrack") + * @param provider Provider name ("thunderforest", "tracestrack" or "stadia") * @return true if a key is available, false otherwise */ fun hasApiKey(provider: String): Boolean = !getApiKey(provider).isNullOrEmpty() diff --git a/openmapview/src/main/kotlin/de/afarber/openmapview/MapType.kt b/openmapview/src/main/kotlin/de/afarber/openmapview/MapType.kt index 452ddd7..32497cd 100644 --- a/openmapview/src/main/kotlin/de/afarber/openmapview/MapType.kt +++ b/openmapview/src/main/kotlin/de/afarber/openmapview/MapType.kt @@ -23,14 +23,14 @@ package de.afarber.openmapview * - [CARTO_LIGHT]: Minimalist light theme for data visualization * - [CARTO_DARK]: Dark theme for night mode * - [CARTO_VOYAGER]: Modern colorful basemap - * - [STAMEN_TONER]: High-contrast black and white - * - [STAMEN_WATERCOLOR]: Artistic watercolor rendering * * **Premium Map Types (API Key Required):** * - [CYCLEMAP]: Thunderforest cycling map (requires Thunderforest API key) * - [TRANSPORT]: Public transport focused map (requires Thunderforest API key) * - [TRANSPORT_DARK]: Dark mode public transport map (requires Thunderforest API key) * - [TRACESTRACK_TOPO]: Topographic with contours (requires Tracestrack API key) + * - [STAMEN_TONER]: High-contrast black and white (requires Stadia Maps API key) + * - [STAMEN_WATERCOLOR]: Artistic watercolor rendering (requires Stadia Maps API key) * * ## API Key Configuration * @@ -42,6 +42,9 @@ package de.afarber.openmapview * + * * ``` * * Or programmatically: @@ -57,6 +60,8 @@ package de.afarber.openmapview * - Free tier: 150,000 tiles/month * - **Tracestrack** (TRACESTRACK_TOPO): https://www.tracestrack.com/en/signup * - Free tier: 100,000 tiles/month + * - **Stadia Maps** (STAMEN_TONER, STAMEN_WATERCOLOR): https://client.stadiamaps.com/signup/ + * - Free tier: 200,000 tiles/month for non-commercial use * * @see OpenMapView.setMapType * @see OpenMapView.getMapType @@ -248,7 +253,7 @@ object MapType { * * - **Tile Server**: tiles.stadiamaps.com (Stadia Maps) * - **Max Zoom**: 20 - * - **API Key**: Optional (free tier: 200k tiles/month for local dev) + * - **API Key**: Required (Stadia Maps) * - **Use Cases**: Data visualization, marker overlays, minimalist design */ const val STAMEN_TONER: Int = 13 @@ -261,7 +266,7 @@ object MapType { * * - **Tile Server**: tiles.stadiamaps.com (Stadia Maps) * - **Max Zoom**: 16 - * - **API Key**: Optional (free tier: 200k tiles/month for local dev) + * - **API Key**: Required (Stadia Maps) * - **Use Cases**: Creative apps, travel journals, artistic visualization */ const val STAMEN_WATERCOLOR: Int = 14 diff --git a/openmapview/src/main/kotlin/de/afarber/openmapview/PredefinedTileProviders.kt b/openmapview/src/main/kotlin/de/afarber/openmapview/PredefinedTileProviders.kt index ca9e817..fe124a1 100644 --- a/openmapview/src/main/kotlin/de/afarber/openmapview/PredefinedTileProviders.kt +++ b/openmapview/src/main/kotlin/de/afarber/openmapview/PredefinedTileProviders.kt @@ -117,28 +117,29 @@ object PredefinedTileProviders { fun waymarkedTrailsSkating(): TileProvider = object : UrlTileProvider("https://tile.waymarkedtrails.org/skating/{z}/{x}/{y}.png") {} /** - * Creates a provider for Stamen Toner Hybrid overlay (labels only). + * Creates a provider for the Stamen Toner lines overlay. * - * Content: Map labels and roads on transparent background + * Content: Roads, railways and borders on transparent background, without labels. + * Stadia Maps does not serve the old combined hybrid style, so this is the lines layer. * Zoom levels: 0-20 - * Attribution: "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL" + * Attribution: "© OpenStreetMap contributors. Tiles © Stamen Design, © Stadia Maps" + * Requires a Stadia Maps API key, see [ApiKeyManager]. * - * @return TileProvider for Stamen Toner Hybrid + * @return TileProvider for Stamen Toner lines */ - fun stamenTonerHybrid(): TileProvider = - object : UrlTileProvider("https://stamen-tiles.a.ssl.fastly.net/toner-hybrid/{z}/{x}/{y}.png") {} + fun stamenTonerHybrid(): TileProvider = stadia("stamen_toner_lines") /** - * Creates a provider for Stamen Terrain Lines overlay (elevation contours only). + * Creates a provider for the Stamen Terrain lines overlay. * - * Content: Elevation contours on transparent background + * Content: Roads, railways and borders in the Stamen Terrain style on transparent background * Zoom levels: 0-18 - * Attribution: "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL" + * Attribution: "© OpenStreetMap contributors. Tiles © Stamen Design, © Stadia Maps" + * Requires a Stadia Maps API key, see [ApiKeyManager]. * - * @return TileProvider for Stamen Terrain Lines + * @return TileProvider for Stamen Terrain lines */ - fun stamenTerrainLines(): TileProvider = - object : UrlTileProvider("https://stamen-tiles.a.ssl.fastly.net/terrain-lines/{z}/{x}/{y}.png") {} + fun stamenTerrainLines(): TileProvider = stadia("stamen_terrain_lines") /** * Creates a custom URL-based tile provider with the specified URL template. @@ -156,4 +157,15 @@ object PredefinedTileProviders { * @return TileProvider for the custom URL */ fun custom(urlTemplate: String): TileProvider = object : UrlTileProvider(urlTemplate) {} + + // Stadia Maps tiles with the "stadia" key from ApiKeyManager, looked up per tile + // so that a key set after creating the provider is still used + private fun stadia(style: String): TileProvider = + object : UrlTileProvider("https://tiles.stadiamaps.com/tiles/$style/{z}/{x}/{y}.png?api_key={apikey}") { + override fun getTileUrl( + x: Int, + y: Int, + zoom: Int, + ): String = super.getTileUrl(x, y, zoom).replace("{apikey}", ApiKeyManager.getApiKey("stadia").orEmpty()) + } } diff --git a/openmapview/src/main/kotlin/de/afarber/openmapview/TileSource.kt b/openmapview/src/main/kotlin/de/afarber/openmapview/TileSource.kt index f0130a4..c3a769a 100644 --- a/openmapview/src/main/kotlin/de/afarber/openmapview/TileSource.kt +++ b/openmapview/src/main/kotlin/de/afarber/openmapview/TileSource.kt @@ -208,24 +208,30 @@ enum class TileSource( * * Stark B&W design with maximum contrast for data visualization. * Most popular Stamen style. Hosted by Stadia Maps. + * Requires Stadia Maps API key. */ STAMEN_TONER( - urlTemplate = "https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.png", + urlTemplate = "https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.png?api_key={apikey}", attributionText = "© OpenStreetMap contributors. Tiles © Stamen Design, © Stadia Maps", attributionUrl = "https://stadiamaps.com/stamen/", maxZoom = 20, + requiresApiKey = true, + apiKeyProvider = "stadia", ), /** * Stamen Watercolor - Artistic watercolor rendering. * * Unique hand-painted watercolor aesthetic. Hosted by Stadia Maps. + * Requires Stadia Maps API key. */ STAMEN_WATERCOLOR( - urlTemplate = "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg", + urlTemplate = "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg?api_key={apikey}", attributionText = "© OpenStreetMap contributors. Tiles © Stamen Design, © Stadia Maps", attributionUrl = "https://stadiamaps.com/stamen/", maxZoom = 16, + requiresApiKey = true, + apiKeyProvider = "stadia", ), ; diff --git a/openmapview/src/test/kotlin/de/afarber/openmapview/PredefinedTileProvidersTest.kt b/openmapview/src/test/kotlin/de/afarber/openmapview/PredefinedTileProvidersTest.kt index eb2752b..7d2f711 100644 --- a/openmapview/src/test/kotlin/de/afarber/openmapview/PredefinedTileProvidersTest.kt +++ b/openmapview/src/test/kotlin/de/afarber/openmapview/PredefinedTileProvidersTest.kt @@ -13,6 +13,7 @@ import io.ktor.client.engine.mock.respond import io.ktor.http.HttpStatusCode import io.ktor.utils.io.ByteReadChannel import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -20,9 +21,15 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class PredefinedTileProvidersTest { + @After + fun tearDown() { + ApiKeyManager.clearProgrammaticKeys() + } + @Test fun testProvidersRequestExpectedUrls() = runTest { + ApiKeyManager.setApiKey("stadia", "test-key") val expected = mapOf( PredefinedTileProviders.openSeaMap() to "https://tiles.openseamap.org/seamark/3/1/2.png", @@ -33,9 +40,9 @@ class PredefinedTileProvidersTest { PredefinedTileProviders.waymarkedTrailsMtb() to "https://tile.waymarkedtrails.org/mtb/3/1/2.png", PredefinedTileProviders.waymarkedTrailsSkating() to "https://tile.waymarkedtrails.org/skating/3/1/2.png", PredefinedTileProviders.stamenTonerHybrid() to - "https://stamen-tiles.a.ssl.fastly.net/toner-hybrid/3/1/2.png", + "https://tiles.stadiamaps.com/tiles/stamen_toner_lines/3/1/2.png?api_key=test-key", PredefinedTileProviders.stamenTerrainLines() to - "https://stamen-tiles.a.ssl.fastly.net/terrain-lines/3/1/2.png", + "https://tiles.stadiamaps.com/tiles/stamen_terrain_lines/3/1/2.png?api_key=test-key", PredefinedTileProviders.custom("https://example.com/{z}/{x}/{y}.png") to "https://example.com/3/1/2.png", ) @@ -44,6 +51,18 @@ class PredefinedTileProvidersTest { } } + @Test + fun testStadiaKeyIsReadPerTile() = + runTest { + val provider = PredefinedTileProviders.stamenTerrainLines() as UrlTileProvider + ApiKeyManager.setApiKey("stadia", "late-key") + + assertEquals( + "https://tiles.stadiamaps.com/tiles/stamen_terrain_lines/3/1/2.png?api_key=late-key", + requestedUrl(provider), + ) + } + // Swaps in a mock HTTP client, so no real tile server is contacted private suspend fun requestedUrl(provider: UrlTileProvider): String? { var requested: String? = null diff --git a/openmapview/src/test/kotlin/de/afarber/openmapview/TileSourceTest.kt b/openmapview/src/test/kotlin/de/afarber/openmapview/TileSourceTest.kt index 791b53c..96365c3 100644 --- a/openmapview/src/test/kotlin/de/afarber/openmapview/TileSourceTest.kt +++ b/openmapview/src/test/kotlin/de/afarber/openmapview/TileSourceTest.kt @@ -7,11 +7,18 @@ package de.afarber.openmapview +import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test class TileSourceTest { + @After + fun tearDown() { + ApiKeyManager.clearProgrammaticKeys() + } + @Test fun testTileSource_Standard() { val tile = TileCoordinate(x = 100, y = 200, zoom = 10) @@ -77,4 +84,30 @@ class TileSourceTest { ) } } + + @Test + fun testStamenSources_RequireStadiaKey() { + for (source in listOf(TileSource.STAMEN_TONER, TileSource.STAMEN_WATERCOLOR)) { + assertTrue(source.requiresApiKey) + assertEquals("stadia", source.apiKeyProvider) + assertEquals("Stadia Maps", source.getProviderName()) + assertFalse(source.hasApiKey()) + } + } + + @Test + fun testStamenSources_UseStadiaKey() { + ApiKeyManager.setApiKey("Stadia", "test-key") + val tile = TileCoordinate(x = 1, y = 2, zoom = 3) + + assertTrue(TileSource.STAMEN_TONER.hasApiKey()) + assertEquals( + "https://tiles.stadiamaps.com/tiles/stamen_toner/3/1/2.png?api_key=test-key", + TileSource.STAMEN_TONER.getTileUrl(tile), + ) + assertEquals( + "https://tiles.stadiamaps.com/tiles/stamen_watercolor/3/1/2.jpg?api_key=test-key", + TileSource.STAMEN_WATERCOLOR.getTileUrl(tile), + ) + } }