From 3868236816dbb66778416e8a592bf220d7164e06 Mon Sep 17 00:00:00 2001 From: stopflock Date: Sat, 22 Nov 2025 22:26:04 -0600 Subject: [PATCH] Improve subdomain notation, fix error catching for xyz in tile URL --- DEVELOPER.md | 31 +++++++++++++- assets/changelog.json | 4 +- lib/localizations/de.json | 2 +- lib/localizations/en.json | 2 +- lib/localizations/es.json | 2 +- lib/localizations/fr.json | 2 +- lib/localizations/it.json | 2 +- lib/localizations/pt.json | 2 +- lib/localizations/zh.json | 2 +- lib/models/tile_provider.dart | 22 +++++++--- lib/screens/tile_provider_editor_screen.dart | 27 +++++++++--- test/models/tile_provider_test.dart | 43 +++++++++++++------- 12 files changed, 106 insertions(+), 35 deletions(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index 499bce5..b7b59c4 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -340,7 +340,36 @@ Most users should contribute to production; testing modes add complexity bool get showUploadModeSelector => kDebugMode; ``` -### 11. Navigation & Routing (Implemented, Awaiting Integration) +### 11. Tile Provider System & URL Templates + +**Design approach:** +- **Flexible URL templates**: Support multiple coordinate systems and load-balancing patterns +- **Built-in providers**: Curated set of high-quality, reliable tile sources +- **Custom providers**: Users can add any tile service with full validation +- **API key management**: Secure storage with per-provider API keys + +**Supported URL placeholders:** +``` +{x}, {y}, {z} - Standard TMS tile coordinates +{quadkey} - Bing Maps quadkey format (alternative to x/y/z) +{0_3} - Subdomain 0-3 for load balancing +{1_4} - Subdomain 1-4 for providers using 1-based indexing +{api_key} - API key insertion point (optional) +``` + +**Built-in providers:** +- **OpenStreetMap**: Standard street map tiles, no API key required +- **Bing Maps**: High-quality satellite imagery using quadkey system, no API key required +- **Mapbox**: Satellite and street tiles, requires API key +- **OpenTopoMap**: Topographic maps, no API key required + +**Validation logic:** +URL templates must contain either `{quadkey}` OR all of `{x}`, `{y}`, and `{z}`. This allows for both standard tile services and specialized formats like Bing Maps. + +**Why this approach:** +Provides maximum flexibility while maintaining simplicity. Users can add any tile service without code changes, while built-in providers offer immediate functionality. The quadkey system enables access to high-quality satellite imagery without API key requirements. + +### 12. Navigation & Routing (Implemented, Awaiting Integration) **Current state:** - **Search functionality**: Fully implemented and active diff --git a/assets/changelog.json b/assets/changelog.json index 452a39e..1e101c5 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -2,7 +2,9 @@ "1.5.1": { "content": [ "• NEW: Bing satellite imagery - high-quality satellite tiles used by the iD editor, no API key required", - "• IMPROVED: Added support for quadkey tile formats and multi-subdomain providers" + "• IMPROVED: Enhanced tile provider system with quadkey format support (for Bing Maps and similar providers)", + "• IMPROVED: Flexible subdomain patterns - supports both 0-3 and 1-4 subdomain ranges for load balancing", + "• IMPROVED: Tile URL validation now accepts either {quadkey} or {x}/{y}/{z} coordinate systems" ] }, "1.5.0": { diff --git a/lib/localizations/de.json b/lib/localizations/de.json index eaa7eba..7d83de7 100644 --- a/lib/localizations/de.json +++ b/lib/localizations/de.json @@ -235,7 +235,7 @@ "urlTemplate": "URL-Vorlage", "urlTemplateHint": "https://beispiel.com/{z}/{x}/{y}.png", "urlTemplateRequired": "URL-Vorlage ist erforderlich", - "urlTemplatePlaceholders": "URL muss {z}, {x} und {y} Platzhalter enthalten", + "urlTemplatePlaceholders": "URL muss entweder {quadkey} oder {z}, {x} und {y} Platzhalter enthalten", "attribution": "Zuschreibung", "attributionHint": "© Karten-Anbieter", "attributionRequired": "Zuschreibung ist erforderlich", diff --git a/lib/localizations/en.json b/lib/localizations/en.json index 2ef9eb0..f69e0e3 100644 --- a/lib/localizations/en.json +++ b/lib/localizations/en.json @@ -267,7 +267,7 @@ "urlTemplate": "URL Template", "urlTemplateHint": "https://example.com/{z}/{x}/{y}.png", "urlTemplateRequired": "URL template is required", - "urlTemplatePlaceholders": "URL must contain {z}, {x}, and {y} placeholders", + "urlTemplatePlaceholders": "URL must contain either {quadkey} or {z}, {x}, and {y} placeholders", "attribution": "Attribution", "attributionHint": "© Map Provider", "attributionRequired": "Attribution is required", diff --git a/lib/localizations/es.json b/lib/localizations/es.json index e0aadb2..571b756 100644 --- a/lib/localizations/es.json +++ b/lib/localizations/es.json @@ -267,7 +267,7 @@ "urlTemplate": "Plantilla de URL", "urlTemplateHint": "https://ejemplo.com/{z}/{x}/{y}.png", "urlTemplateRequired": "La plantilla de URL es requerida", - "urlTemplatePlaceholders": "La URL debe contener marcadores {z}, {x} y {y}", + "urlTemplatePlaceholders": "La URL debe contener marcadores {quadkey} o {z}, {x} y {y}", "attribution": "Atribución", "attributionHint": "© Proveedor de Mapas", "attributionRequired": "La atribución es requerida", diff --git a/lib/localizations/fr.json b/lib/localizations/fr.json index 9ed93d8..21a23c0 100644 --- a/lib/localizations/fr.json +++ b/lib/localizations/fr.json @@ -267,7 +267,7 @@ "urlTemplate": "Modèle d'URL", "urlTemplateHint": "https://exemple.com/{z}/{x}/{y}.png", "urlTemplateRequired": "Le modèle d'URL est requis", - "urlTemplatePlaceholders": "L'URL doit contenir les marqueurs {z}, {x} et {y}", + "urlTemplatePlaceholders": "L'URL doit contenir soit {quadkey} soit les marqueurs {z}, {x} et {y}", "attribution": "Attribution", "attributionHint": "© Fournisseur de Cartes", "attributionRequired": "L'attribution est requise", diff --git a/lib/localizations/it.json b/lib/localizations/it.json index ada046f..b6eee25 100644 --- a/lib/localizations/it.json +++ b/lib/localizations/it.json @@ -267,7 +267,7 @@ "urlTemplate": "Template URL", "urlTemplateHint": "https://esempio.com/{z}/{x}/{y}.png", "urlTemplateRequired": "Il template URL è obbligatorio", - "urlTemplatePlaceholders": "L'URL deve contenere i segnaposto {z}, {x} e {y}", + "urlTemplatePlaceholders": "L'URL deve contenere o {quadkey} o i segnaposto {z}, {x} e {y}", "attribution": "Attribuzione", "attributionHint": "© Fornitore Mappe", "attributionRequired": "L'attribuzione è obbligatoria", diff --git a/lib/localizations/pt.json b/lib/localizations/pt.json index 4fba388..23fb4db 100644 --- a/lib/localizations/pt.json +++ b/lib/localizations/pt.json @@ -267,7 +267,7 @@ "urlTemplate": "Modelo de URL", "urlTemplateHint": "https://exemplo.com/{z}/{x}/{y}.png", "urlTemplateRequired": "Modelo de URL é obrigatório", - "urlTemplatePlaceholders": "URL deve conter os marcadores {z}, {x} e {y}", + "urlTemplatePlaceholders": "URL deve conter {quadkey} ou os marcadores {z}, {x} e {y}", "attribution": "Atribuição", "attributionHint": "© Provedor de Mapas", "attributionRequired": "Atribuição é obrigatória", diff --git a/lib/localizations/zh.json b/lib/localizations/zh.json index ee67a5a..11013db 100644 --- a/lib/localizations/zh.json +++ b/lib/localizations/zh.json @@ -267,7 +267,7 @@ "urlTemplate": "URL 模板", "urlTemplateHint": "https://example.com/{z}/{x}/{y}.png", "urlTemplateRequired": "URL 模板为必填项", - "urlTemplatePlaceholders": "URL 必须包含 {z}、{x} 和 {y} 占位符", + "urlTemplatePlaceholders": "URL 必须包含 {quadkey} 或 {z}、{x} 和 {y} 占位符", "attribution": "归属", "attributionHint": "© 地图提供商", "attributionRequired": "归属为必填项", diff --git a/lib/models/tile_provider.dart b/lib/models/tile_provider.dart index 6b6b764..8f7d81c 100644 --- a/lib/models/tile_provider.dart +++ b/lib/models/tile_provider.dart @@ -20,6 +20,13 @@ class TileType { }); /// Create URL for a specific tile, replacing template variables + /// + /// Supported placeholders: + /// - {x}, {y}, {z}: Standard tile coordinates + /// - {quadkey}: Bing Maps quadkey format (alternative to x/y/z) + /// - {0_3}: Subdomain 0-3 for load balancing + /// - {1_4}: Subdomain 1-4 for providers that use 1-based indexing + /// - {api_key}: API key placeholder (optional) String getTileUrl(int z, int x, int y, {String? apiKey}) { String url = urlTemplate; @@ -29,10 +36,15 @@ class TileType { url = url.replaceAll('{quadkey}', quadkey); } - // Handle subdomains (for Bing Maps and other multi-subdomain providers) - if (url.contains('{subdomain}')) { - final subdomain = (x + y) % 4; // Distribute across 0-3 subdomains - url = url.replaceAll('{subdomain}', subdomain.toString()); + // Handle subdomains for load balancing + if (url.contains('{0_3}')) { + final subdomain = (x + y) % 4; // 0, 1, 2, 3 + url = url.replaceAll('{0_3}', subdomain.toString()); + } + + if (url.contains('{1_4}')) { + final subdomain = ((x + y) % 4) + 1; // 1, 2, 3, 4 + url = url.replaceAll('{1_4}', subdomain.toString()); } // Standard x/y/z replacement @@ -196,7 +208,7 @@ class DefaultTileProviders { TileType( id: 'bing_satellite', name: 'Satellite', - urlTemplate: 'https://ecn.t{subdomain}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=1&n=z', + urlTemplate: 'https://ecn.t{0_3}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=1&n=z', attribution: '© Microsoft Corporation', maxZoom: 20, ), diff --git a/lib/screens/tile_provider_editor_screen.dart b/lib/screens/tile_provider_editor_screen.dart index 6ca2013..01a51d2 100644 --- a/lib/screens/tile_provider_editor_screen.dart +++ b/lib/screens/tile_provider_editor_screen.dart @@ -318,9 +318,15 @@ class _TileTypeDialogState extends State<_TileTypeDialog> { ), validator: (value) { if (value?.trim().isEmpty == true) return locService.t('tileTypeEditor.urlTemplateRequired'); - if (!value!.contains('{z}') || !value.contains('{x}') || !value.contains('{y}')) { + + // Check for either quadkey OR x+y+z placeholders + final hasQuadkey = value!.contains('{quadkey}'); + final hasXYZ = value.contains('{x}') && value.contains('{y}') && value.contains('{z}'); + + if (!hasQuadkey && !hasXYZ) { return locService.t('tileTypeEditor.urlTemplatePlaceholders'); } + return null; }, ), @@ -403,11 +409,20 @@ class _TileTypeDialogState extends State<_TileTypeDialog> { }); try { - // Use a sample tile from configured preview location - final url = _urlController.text - .replaceAll('{z}', kPreviewTileZoom.toString()) - .replaceAll('{x}', kPreviewTileX.toString()) - .replaceAll('{y}', kPreviewTileY.toString()); + // Create a temporary TileType to use the getTileUrl method + final tempTileType = TileType( + id: 'preview', + name: 'Preview', + urlTemplate: _urlController.text.trim(), + attribution: 'Preview', + ); + + final url = tempTileType.getTileUrl( + kPreviewTileZoom, + kPreviewTileX, + kPreviewTileY, + apiKey: null, // Don't use API key for preview + ); final response = await http.get(Uri.parse(url)); diff --git a/test/models/tile_provider_test.dart b/test/models/tile_provider_test.dart index 2ad8ea1..f26810a 100644 --- a/test/models/tile_provider_test.dart +++ b/test/models/tile_provider_test.dart @@ -15,24 +15,37 @@ void main() { expect(url, 'https://example.com/3/2/1.png'); }); - test('getTileUrl handles subdomain replacement', () { - final tileType = TileType( - id: 'test', - name: 'Test', - urlTemplate: 'https://a{subdomain}.example.com/{z}/{x}/{y}.png', + test('getTileUrl handles subdomain patterns', () { + final tileType0_3 = TileType( + id: 'test_0_3', + name: 'Test 0-3', + urlTemplate: 'https://s{0_3}.example.com/{z}/{x}/{y}.png', attribution: 'Test', ); - // Test subdomain distribution (should be consistent) - final url1 = tileType.getTileUrl(1, 2, 3); - final url2 = tileType.getTileUrl(1, 2, 3); - expect(url1, url2); // Same input should give same output + final tileType1_4 = TileType( + id: 'test_1_4', + name: 'Test 1-4', + urlTemplate: 'https://s{1_4}.example.com/{z}/{x}/{y}.png', + attribution: 'Test', + ); - // Test that different tiles can get different subdomains - final url3 = tileType.getTileUrl(1, 0, 0); - final url4 = tileType.getTileUrl(1, 1, 1); - expect(url3, contains('a0.example.com')); - expect(url4, contains('a2.example.com')); + // Test 0-3 range + final url_0_3_a = tileType0_3.getTileUrl(1, 0, 0); + final url_0_3_b = tileType0_3.getTileUrl(1, 3, 0); + expect(url_0_3_a, contains('s0.example.com')); + expect(url_0_3_b, contains('s3.example.com')); + + // Test 1-4 range + final url_1_4_a = tileType1_4.getTileUrl(1, 0, 0); + final url_1_4_b = tileType1_4.getTileUrl(1, 3, 0); + expect(url_1_4_a, contains('s1.example.com')); + expect(url_1_4_b, contains('s4.example.com')); + + // Test consistency + final url1 = tileType0_3.getTileUrl(1, 2, 3); + final url2 = tileType0_3.getTileUrl(1, 2, 3); + expect(url1, url2); // Same input should give same output }); test('getTileUrl handles Bing Maps quadkey conversion', () { @@ -109,7 +122,7 @@ void main() { expect(satelliteType.id, 'bing_satellite'); expect(satelliteType.name, 'Satellite'); expect(satelliteType.urlTemplate, contains('quadkey')); - expect(satelliteType.urlTemplate, contains('subdomain')); + expect(satelliteType.urlTemplate, contains('0_3')); expect(satelliteType.requiresApiKey, isFalse); expect(satelliteType.attribution, '© Microsoft Corporation'); });