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),
+ )
+ }
}