add stadiamaps.com

This commit is contained in:
ericz
2026-05-23 11:41:06 +02:00
parent a50c0d0b2d
commit 225d07b440
10 changed files with 507 additions and 32 deletions
+4 -2
View File
@@ -40,7 +40,9 @@ void main() async {
final bleDebugLogService = BleDebugLogService();
final appDebugLogService = AppDebugLogService();
final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService();
final mapTileCacheService = MapTileCacheService(
appSettingsService: appSettingsService,
);
final chatTextScaleService = ChatTextScaleService();
final translationService = TranslationService(appSettingsService);
final uiViewStateService = UiViewStateService();
@@ -172,7 +174,7 @@ class MeshCoreApp extends StatelessWidget {
ChangeNotifierProvider.value(value: translationService),
ChangeNotifierProvider.value(value: uiViewStateService),
Provider.value(value: storage),
Provider.value(value: mapTileCacheService),
ChangeNotifierProvider.value(value: mapTileCacheService),
ChangeNotifierProvider.value(value: timeoutPredictionService),
],
child: Consumer<AppSettingsService>(
+21
View File
@@ -91,6 +91,9 @@ class AppSettings {
final Map<String, double>? mapCacheBounds;
final int mapCacheMinZoom;
final int mapCacheMaxZoom;
final String mapRasterSourceId;
final String mapTileEndpointId;
final String? mapTileApiKey;
final bool notificationsEnabled;
final bool notifyOnNewMessage;
final bool notifyOnNewChannelMessage;
@@ -144,6 +147,9 @@ class AppSettings {
this.mapCacheBounds,
this.mapCacheMinZoom = 10,
this.mapCacheMaxZoom = 15,
this.mapRasterSourceId = 'osm_standard',
this.mapTileEndpointId = 'standard',
this.mapTileApiKey,
this.notificationsEnabled = true,
this.notifyOnNewMessage = true,
this.notifyOnNewChannelMessage = true,
@@ -204,6 +210,9 @@ class AppSettings {
'map_cache_bounds': mapCacheBounds,
'map_cache_min_zoom': mapCacheMinZoom,
'map_cache_max_zoom': mapCacheMaxZoom,
'map_raster_source_id': mapRasterSourceId,
'map_tile_endpoint_id': mapTileEndpointId,
'map_tile_api_key': mapTileApiKey,
'notifications_enabled': notificationsEnabled,
'notify_on_new_message': notifyOnNewMessage,
'notify_on_new_channel_message': notifyOnNewChannelMessage,
@@ -267,6 +276,10 @@ class AppSettings {
),
mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10,
mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15,
mapRasterSourceId:
json['map_raster_source_id'] as String? ?? 'osm_standard',
mapTileEndpointId: json['map_tile_endpoint_id'] as String? ?? 'standard',
mapTileApiKey: json['map_tile_api_key'] as String?,
notificationsEnabled: json['notifications_enabled'] as bool? ?? true,
notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true,
notifyOnNewChannelMessage:
@@ -374,6 +387,9 @@ class AppSettings {
Object? mapCacheBounds = _unset,
int? mapCacheMinZoom,
int? mapCacheMaxZoom,
String? mapRasterSourceId,
String? mapTileEndpointId,
Object? mapTileApiKey = _unset,
bool? notificationsEnabled,
bool? notifyOnNewMessage,
bool? notifyOnNewChannelMessage,
@@ -422,6 +438,11 @@ class AppSettings {
: mapCacheBounds as Map<String, double>?,
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
mapRasterSourceId: mapRasterSourceId ?? this.mapRasterSourceId,
mapTileEndpointId: mapTileEndpointId ?? this.mapTileEndpointId,
mapTileApiKey: mapTileApiKey == _unset
? this.mapTileApiKey
: mapTileApiKey as String?,
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage,
notifyOnNewChannelMessage:
+217
View File
@@ -8,6 +8,7 @@ import '../l10n/l10n.dart';
import '../models/app_settings.dart';
import '../models/translation_support.dart';
import '../services/app_settings_service.dart';
import '../services/map_tile_cache_service.dart';
import '../services/notification_service.dart';
import '../services/translation_service.dart';
import '../widgets/adaptive_app_bar_title.dart';
@@ -529,6 +530,39 @@ class AppSettingsScreen extends StatelessWidget {
onTap: () => _showUnitsDialog(context, settingsService),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.layers_outlined),
title: const Text('Raster Tile Source'),
subtitle: Text(
_mapRasterSourceSummary(settingsService.settings),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showMapRasterSourceDialog(context, settingsService),
),
if (_isStadiaSource(settingsService.settings)) ...[
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.public_outlined),
title: const Text('Stadia Endpoint'),
subtitle: Text(
_mapRasterEndpointSummary(settingsService.settings),
),
trailing: const Icon(Icons.chevron_right),
onTap: () =>
_showMapRasterEndpointDialog(context, settingsService),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.key_outlined),
title: const Text('Stadia API Key'),
subtitle: Text(_mapApiKeySummary(settingsService.settings)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showMapApiKeyDialog(context, settingsService),
),
],
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.download_outlined),
title: Text(context.l10n.appSettings_offlineMapCache),
@@ -553,6 +587,189 @@ class AppSettingsScreen extends StatelessWidget {
);
}
String _mapRasterSourceSummary(AppSettings settings) {
final source = MapRasterSourceCatalog.fromSettings(settings);
return '${source.label} - ${source.description}';
}
bool _isStadiaSource(AppSettings settings) {
return MapRasterSourceCatalog.fromSettings(settings).isStadia;
}
String _mapRasterEndpointSummary(AppSettings settings) {
final endpoint = MapRasterEndpointCatalog.fromSettings(settings);
return '${endpoint.label} - ${endpoint.description}';
}
String _mapApiKeySummary(AppSettings settings) {
final apiKey = settings.mapTileApiKey?.trim();
if (apiKey == null || apiKey.isEmpty) {
return 'Required for mobile and non-local Stadia Maps usage';
}
return 'Configured: ${_maskApiKey(apiKey)}';
}
String _maskApiKey(String value) {
if (value.length <= 8) return '********';
return '${value.substring(0, 4)}...${value.substring(value.length - 4)}';
}
void _showMapRasterSourceDialog(
BuildContext context,
AppSettingsService settingsService,
) {
String selectedId = settingsService.settings.mapRasterSourceId;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setState) => AlertDialog(
title: const Text('Raster Tile Source'),
content: SizedBox(
width: 360,
child: RadioGroup<String>(
groupValue: selectedId,
onChanged: (value) {
if (value == null) return;
setState(() {
selectedId = value;
});
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final option in MapRasterSourceCatalog.presets)
RadioListTile<String>(
value: option.id,
title: Text(option.label),
subtitle: Text(option.description),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_cancel),
),
TextButton(
onPressed: () async {
await settingsService.setMapRasterSourceId(selectedId);
if (!dialogContext.mounted) return;
Navigator.pop(dialogContext);
},
child: Text(context.l10n.common_save),
),
],
),
),
);
}
void _showMapRasterEndpointDialog(
BuildContext context,
AppSettingsService settingsService,
) {
String selectedId = settingsService.settings.mapTileEndpointId;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setState) => AlertDialog(
title: const Text('Stadia Endpoint'),
content: SizedBox(
width: 360,
child: RadioGroup<String>(
groupValue: selectedId,
onChanged: (value) {
if (value == null) return;
setState(() {
selectedId = value;
});
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final option in MapRasterEndpointCatalog.presets)
RadioListTile<String>(
value: option.id,
title: Text(option.label),
subtitle: Text(option.description),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_cancel),
),
TextButton(
onPressed: () async {
await settingsService.setMapTileEndpointId(selectedId);
if (!dialogContext.mounted) return;
Navigator.pop(dialogContext);
},
child: Text(context.l10n.common_save),
),
],
),
),
);
}
void _showMapApiKeyDialog(
BuildContext context,
AppSettingsService settingsService,
) {
final controller = TextEditingController(
text: settingsService.settings.mapTileApiKey ?? '',
);
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Stadia API Key'),
content: SizedBox(
width: 420,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Enter your Stadia Maps API key. This app uses it for raster tile requests on mobile and other non-local environments.',
),
const SizedBox(height: 12),
TextField(
controller: controller,
autofocus: true,
autocorrect: false,
enableSuggestions: false,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'sk_...',
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_cancel),
),
TextButton(
onPressed: () async {
await settingsService.setMapTileApiKey(controller.text);
if (!dialogContext.mounted) return;
Navigator.pop(dialogContext);
},
child: Text(context.l10n.common_save),
),
],
),
);
}
Widget _buildTranslationCard(
BuildContext context,
AppSettingsService settingsService,
+2 -2
View File
@@ -441,7 +441,7 @@ class _ChannelMessagePathMapScreenState
builder: (context, connector, _) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
final tileCache = context.watch<MapTileCacheService>();
final primaryPath = _selectPrimaryPath(
widget.message.pathBytes,
widget.message.pathVariants,
@@ -559,7 +559,7 @@ class _ChannelMessagePathMapScreenState
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
urlTemplate: tileCache.urlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
+2 -2
View File
@@ -391,7 +391,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
Widget build(BuildContext context) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
final tileCache = context.watch<MapTileCacheService>();
final endpoints = _visibleEndpoints();
final mapPoints = [
if (_start != null) _start!.point,
@@ -470,7 +470,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
urlTemplate: tileCache.urlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName: MapTileCacheService.userAgentPackageName,
maxZoom: 19,
+25 -5
View File
@@ -171,6 +171,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
Future<void> _startDownload() async {
final bounds = _selectedBounds;
final cacheService = context.read<MapTileCacheService>();
if (bounds == null) {
showDismissibleSnackBar(
context,
@@ -187,6 +188,16 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
return;
}
if (!cacheService.source.allowsBulkDownload) {
showDismissibleSnackBar(
context,
content: Text(
'Offline bulk downloads are disabled for ${cacheService.source.label} in this app configuration.',
),
);
return;
}
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
@@ -209,8 +220,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
if (confirmed != true || !mounted) return;
final cacheService = context.read<MapTileCacheService>();
setState(() {
_isDownloading = true;
_completedTiles = 0;
@@ -278,7 +287,8 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
@override
Widget build(BuildContext context) {
final tileCache = context.read<MapTileCacheService>();
final tileCache = context.watch<MapTileCacheService>();
final source = tileCache.source;
final selectedBounds = _selectedBounds;
final l10n = context.l10n;
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
@@ -319,7 +329,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
urlTemplate: tileCache.urlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
@@ -423,6 +433,13 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
},
),
Text(l10n.mapCache_estimatedTiles(_estimatedTiles)),
if (!source.allowsBulkDownload) ...[
const SizedBox(height: 8),
Text(
'Offline bulk downloads are disabled for ${source.label}.',
style: TextStyle(color: Colors.orange[800]),
),
],
if (_isDownloading) ...[
const SizedBox(height: 8),
LinearProgressIndicator(value: progressValue),
@@ -441,7 +458,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
child: ElevatedButton.icon(
icon: const Icon(Icons.download),
label: Text(l10n.mapCache_downloadTilesButton),
onPressed: _isDownloading || selectedBounds == null
onPressed:
_isDownloading ||
selectedBounds == null ||
!source.allowsBulkDownload
? null
: _startDownload,
),
+2 -2
View File
@@ -205,7 +205,7 @@ class _MapScreenState extends State<MapScreen> {
Widget build(BuildContext context) {
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
builder: (context, connector, settingsService, pathHistory, child) {
final tileCache = context.read<MapTileCacheService>();
final tileCache = context.watch<MapTileCacheService>();
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
final settings = settingsService.settings;
final allContacts = connector.allContacts;
@@ -563,7 +563,7 @@ class _MapScreenState extends State<MapScreen> {
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
urlTemplate: tileCache.urlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
+2 -2
View File
@@ -535,7 +535,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
builder: (context, connector, _) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
final tileCache = context.watch<MapTileCacheService>();
return Scaffold(
appBar: AppBar(
@@ -909,7 +909,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
urlTemplate: tileCache.urlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName: MapTileCacheService.userAgentPackageName,
maxZoom: 19,
+19
View File
@@ -112,6 +112,25 @@ class AppSettingsService extends ChangeNotifier {
);
}
Future<void> setMapRasterSourceId(String value) async {
await updateSettings(_settings.copyWith(mapRasterSourceId: value));
}
Future<void> setMapTileEndpointId(String value) async {
await updateSettings(_settings.copyWith(mapTileEndpointId: value));
}
Future<void> setMapTileApiKey(String? value) async {
final normalized = value?.trim();
await updateSettings(
_settings.copyWith(
mapTileApiKey: (normalized == null || normalized.isEmpty)
? null
: normalized,
),
);
}
Future<void> setNotificationsEnabled(bool value) async {
await updateSettings(_settings.copyWith(notificationsEnabled: value));
}
+213 -17
View File
@@ -2,11 +2,169 @@ import 'dart:math' as math;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_map/flutter_map.dart';
const String kMapTileUrlTemplate =
'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
import '../models/app_settings.dart';
import 'app_settings_service.dart';
enum MapRasterSourcePreset {
osmStandard('osm_standard'),
stamenTerrain('stamen_terrain'),
alidadeSmoothDark('alidade_smooth_dark'),
outdoors('outdoors'),
osmBright('osm_bright');
const MapRasterSourcePreset(this.id);
final String id;
static MapRasterSourcePreset fromId(String id) {
for (final value in values) {
if (value.id == id) return value;
}
return MapRasterSourcePreset.osmStandard;
}
}
enum MapRasterEndpointPreset {
standard('standard'),
eu('eu');
const MapRasterEndpointPreset(this.id);
final String id;
static MapRasterEndpointPreset fromId(String id) {
for (final value in values) {
if (value.id == id) return value;
}
return MapRasterEndpointPreset.standard;
}
}
@immutable
class MapRasterSourceDefinition {
const MapRasterSourceDefinition({
required this.id,
required this.label,
required this.description,
this.isStadia = false,
this.allowsBulkDownload = false,
});
final String id;
final String label;
final String description;
final bool isStadia;
final bool allowsBulkDownload;
}
@immutable
class MapRasterEndpointDefinition {
const MapRasterEndpointDefinition({
required this.id,
required this.label,
required this.description,
required this.host,
});
final String id;
final String label;
final String description;
final String host;
}
class MapRasterSourceCatalog {
static const MapRasterSourceDefinition osmStandard =
MapRasterSourceDefinition(
id: 'osm_standard',
label: 'OpenStreetMap Standard',
description: 'Direct tiles from tile.openstreetmap.org',
);
static const MapRasterSourceDefinition stamenTerrain =
MapRasterSourceDefinition(
id: 'stamen_terrain',
label: 'Stamen Terrain',
description: 'Terrain-focused style with hill shading',
isStadia: true,
allowsBulkDownload: true,
);
static const MapRasterSourceDefinition alidadeSmoothDark =
MapRasterSourceDefinition(
id: 'alidade_smooth_dark',
label: 'Alidade Smooth Dark',
description: 'Dark basemap with smooth contrast',
isStadia: true,
allowsBulkDownload: true,
);
static const MapRasterSourceDefinition outdoors = MapRasterSourceDefinition(
id: 'outdoors',
label: 'Outdoors',
description: 'Outdoor-focused map with trails and terrain context',
isStadia: true,
allowsBulkDownload: true,
);
static const MapRasterSourceDefinition osmBright = MapRasterSourceDefinition(
id: 'osm_bright',
label: 'OSM Bright',
description: 'Bright general-purpose OpenStreetMap style',
isStadia: true,
allowsBulkDownload: true,
);
static const List<MapRasterSourceDefinition> presets = [
osmStandard,
stamenTerrain,
alidadeSmoothDark,
outdoors,
osmBright,
];
static MapRasterSourceDefinition fromSettings(AppSettings settings) {
final preset = MapRasterSourcePreset.fromId(settings.mapRasterSourceId);
switch (preset) {
case MapRasterSourcePreset.osmStandard:
return osmStandard;
case MapRasterSourcePreset.alidadeSmoothDark:
return alidadeSmoothDark;
case MapRasterSourcePreset.outdoors:
return outdoors;
case MapRasterSourcePreset.osmBright:
return osmBright;
case MapRasterSourcePreset.stamenTerrain:
return stamenTerrain;
}
}
}
class MapRasterEndpointCatalog {
static const MapRasterEndpointDefinition standard =
MapRasterEndpointDefinition(
id: 'standard',
label: 'Standard Endpoint',
description: 'Global CDN routing to the fastest Stadia server',
host: 'tiles.stadiamaps.com',
);
static const MapRasterEndpointDefinition eu = MapRasterEndpointDefinition(
id: 'eu',
label: 'EU Endpoint',
description: 'Route tile requests to Stadia EU servers',
host: 'tiles-eu.stadiamaps.com',
);
static const List<MapRasterEndpointDefinition> presets = [standard, eu];
static MapRasterEndpointDefinition fromSettings(AppSettings settings) {
final preset = MapRasterEndpointPreset.fromId(settings.mapTileEndpointId);
switch (preset) {
case MapRasterEndpointPreset.eu:
return eu;
case MapRasterEndpointPreset.standard:
return standard;
}
}
}
class MapTileCacheProgress {
final int completed;
@@ -32,28 +190,40 @@ class MapTileCacheResult {
});
}
class MapTileCacheService {
class MapTileCacheService extends ChangeNotifier {
static const String cacheKey = 'map_tile_cache';
static const String userAgentPackageName = 'com.meshcore.open';
static const int defaultMinZoom = 10;
static const int defaultMaxZoom = 15;
final AppSettingsService appSettingsService;
final BaseCacheManager cacheManager;
late final TileProvider tileProvider;
MapTileCacheService({BaseCacheManager? cacheManager})
: cacheManager =
cacheManager ??
CacheManager(
Config(
cacheKey,
stalePeriod: const Duration(days: 365),
maxNrOfCacheObjects: 200000,
),
) {
MapTileCacheService({
required this.appSettingsService,
BaseCacheManager? cacheManager,
}) : cacheManager =
cacheManager ??
CacheManager(
Config(
cacheKey,
stalePeriod: const Duration(days: 365),
maxNrOfCacheObjects: 200000,
),
) {
tileProvider = CachedNetworkTileProvider(cacheManager: this.cacheManager);
appSettingsService.addListener(_handleSettingsChanged);
}
MapRasterSourceDefinition get source =>
MapRasterSourceCatalog.fromSettings(appSettingsService.settings);
MapRasterEndpointDefinition get endpoint =>
MapRasterEndpointCatalog.fromSettings(appSettingsService.settings);
String get urlTemplate => _buildUrlTemplate(appSettingsService.settings);
Map<String, String> get defaultHeaders => {
'User-Agent': 'flutter_map ($userAgentPackageName)',
};
@@ -89,6 +259,7 @@ class MapTileCacheService {
final total = estimateTileCount(bounds, safeMin, safeMax);
final authHeaders = headers ?? defaultHeaders;
final safeConcurrency = math.max(1, concurrentDownloads);
final currentTemplate = urlTemplate;
int completed = 0;
int failed = 0;
@@ -124,7 +295,7 @@ class MapTileCacheService {
final tileBounds = _tileBoundsForBounds(bounds, zoom);
for (int x = tileBounds.minX; x <= tileBounds.maxX; x++) {
for (int y = tileBounds.minY; y <= tileBounds.maxY; y++) {
final url = _buildTileUrl(x, y, zoom);
final url = _buildTileUrl(x, y, zoom, urlTemplate: currentTemplate);
await queueDownload(url);
}
}
@@ -167,6 +338,16 @@ class MapTileCacheService {
);
}
void _handleSettingsChanged() {
notifyListeners();
}
@override
void dispose() {
appSettingsService.removeListener(_handleSettingsChanged);
super.dispose();
}
_TileBounds _tileBoundsForBounds(LatLngBounds bounds, int zoom) {
final north = _clampLatitude(bounds.north);
final south = _clampLatitude(bounds.south);
@@ -185,6 +366,21 @@ class MapTileCacheService {
);
}
String _buildUrlTemplate(AppSettings settings) {
final source = MapRasterSourceCatalog.fromSettings(settings);
if (!source.isStadia) {
return 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
}
final endpoint = MapRasterEndpointCatalog.fromSettings(settings);
final apiKey = settings.mapTileApiKey?.trim();
final base = 'https://${endpoint.host}/tiles/${source.id}/{z}/{x}/{y}.png';
if (apiKey == null || apiKey.isEmpty) {
return base;
}
final query = Uri(queryParameters: {'api_key': apiKey}).query;
return '$base?$query';
}
int _lonToTileX(double lon, int zoom, int maxIndex) {
final n = 1 << zoom;
final value = ((lon + 180.0) / 360.0 * n).floor();
@@ -205,8 +401,8 @@ class MapTileCacheService {
return lat.clamp(-maxLat, maxLat);
}
String _buildTileUrl(int x, int y, int zoom) {
return kMapTileUrlTemplate
String _buildTileUrl(int x, int y, int zoom, {required String urlTemplate}) {
return urlTemplate
.replaceAll('{z}', zoom.toString())
.replaceAll('{x}', x.toString())
.replaceAll('{y}', y.toString());