mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-06 16:56:41 +10:00
add stadiamaps.com
This commit is contained in:
+4
-2
@@ -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>(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user