mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-23 17:21:06 +10:00
add stadiamaps.com
This commit is contained in:
+4
-2
@@ -40,7 +40,9 @@ void main() async {
|
|||||||
final bleDebugLogService = BleDebugLogService();
|
final bleDebugLogService = BleDebugLogService();
|
||||||
final appDebugLogService = AppDebugLogService();
|
final appDebugLogService = AppDebugLogService();
|
||||||
final backgroundService = BackgroundService();
|
final backgroundService = BackgroundService();
|
||||||
final mapTileCacheService = MapTileCacheService();
|
final mapTileCacheService = MapTileCacheService(
|
||||||
|
appSettingsService: appSettingsService,
|
||||||
|
);
|
||||||
final chatTextScaleService = ChatTextScaleService();
|
final chatTextScaleService = ChatTextScaleService();
|
||||||
final translationService = TranslationService(appSettingsService);
|
final translationService = TranslationService(appSettingsService);
|
||||||
final uiViewStateService = UiViewStateService();
|
final uiViewStateService = UiViewStateService();
|
||||||
@@ -172,7 +174,7 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
ChangeNotifierProvider.value(value: translationService),
|
ChangeNotifierProvider.value(value: translationService),
|
||||||
ChangeNotifierProvider.value(value: uiViewStateService),
|
ChangeNotifierProvider.value(value: uiViewStateService),
|
||||||
Provider.value(value: storage),
|
Provider.value(value: storage),
|
||||||
Provider.value(value: mapTileCacheService),
|
ChangeNotifierProvider.value(value: mapTileCacheService),
|
||||||
ChangeNotifierProvider.value(value: timeoutPredictionService),
|
ChangeNotifierProvider.value(value: timeoutPredictionService),
|
||||||
],
|
],
|
||||||
child: Consumer<AppSettingsService>(
|
child: Consumer<AppSettingsService>(
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ class AppSettings {
|
|||||||
final Map<String, double>? mapCacheBounds;
|
final Map<String, double>? mapCacheBounds;
|
||||||
final int mapCacheMinZoom;
|
final int mapCacheMinZoom;
|
||||||
final int mapCacheMaxZoom;
|
final int mapCacheMaxZoom;
|
||||||
|
final String mapRasterSourceId;
|
||||||
|
final String mapTileEndpointId;
|
||||||
|
final String? mapTileApiKey;
|
||||||
final bool notificationsEnabled;
|
final bool notificationsEnabled;
|
||||||
final bool notifyOnNewMessage;
|
final bool notifyOnNewMessage;
|
||||||
final bool notifyOnNewChannelMessage;
|
final bool notifyOnNewChannelMessage;
|
||||||
@@ -144,6 +147,9 @@ class AppSettings {
|
|||||||
this.mapCacheBounds,
|
this.mapCacheBounds,
|
||||||
this.mapCacheMinZoom = 10,
|
this.mapCacheMinZoom = 10,
|
||||||
this.mapCacheMaxZoom = 15,
|
this.mapCacheMaxZoom = 15,
|
||||||
|
this.mapRasterSourceId = 'osm_standard',
|
||||||
|
this.mapTileEndpointId = 'standard',
|
||||||
|
this.mapTileApiKey,
|
||||||
this.notificationsEnabled = true,
|
this.notificationsEnabled = true,
|
||||||
this.notifyOnNewMessage = true,
|
this.notifyOnNewMessage = true,
|
||||||
this.notifyOnNewChannelMessage = true,
|
this.notifyOnNewChannelMessage = true,
|
||||||
@@ -204,6 +210,9 @@ class AppSettings {
|
|||||||
'map_cache_bounds': mapCacheBounds,
|
'map_cache_bounds': mapCacheBounds,
|
||||||
'map_cache_min_zoom': mapCacheMinZoom,
|
'map_cache_min_zoom': mapCacheMinZoom,
|
||||||
'map_cache_max_zoom': mapCacheMaxZoom,
|
'map_cache_max_zoom': mapCacheMaxZoom,
|
||||||
|
'map_raster_source_id': mapRasterSourceId,
|
||||||
|
'map_tile_endpoint_id': mapTileEndpointId,
|
||||||
|
'map_tile_api_key': mapTileApiKey,
|
||||||
'notifications_enabled': notificationsEnabled,
|
'notifications_enabled': notificationsEnabled,
|
||||||
'notify_on_new_message': notifyOnNewMessage,
|
'notify_on_new_message': notifyOnNewMessage,
|
||||||
'notify_on_new_channel_message': notifyOnNewChannelMessage,
|
'notify_on_new_channel_message': notifyOnNewChannelMessage,
|
||||||
@@ -267,6 +276,10 @@ class AppSettings {
|
|||||||
),
|
),
|
||||||
mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10,
|
mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10,
|
||||||
mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15,
|
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,
|
notificationsEnabled: json['notifications_enabled'] as bool? ?? true,
|
||||||
notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true,
|
notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true,
|
||||||
notifyOnNewChannelMessage:
|
notifyOnNewChannelMessage:
|
||||||
@@ -374,6 +387,9 @@ class AppSettings {
|
|||||||
Object? mapCacheBounds = _unset,
|
Object? mapCacheBounds = _unset,
|
||||||
int? mapCacheMinZoom,
|
int? mapCacheMinZoom,
|
||||||
int? mapCacheMaxZoom,
|
int? mapCacheMaxZoom,
|
||||||
|
String? mapRasterSourceId,
|
||||||
|
String? mapTileEndpointId,
|
||||||
|
Object? mapTileApiKey = _unset,
|
||||||
bool? notificationsEnabled,
|
bool? notificationsEnabled,
|
||||||
bool? notifyOnNewMessage,
|
bool? notifyOnNewMessage,
|
||||||
bool? notifyOnNewChannelMessage,
|
bool? notifyOnNewChannelMessage,
|
||||||
@@ -422,6 +438,11 @@ class AppSettings {
|
|||||||
: mapCacheBounds as Map<String, double>?,
|
: mapCacheBounds as Map<String, double>?,
|
||||||
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
|
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
|
||||||
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
|
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
|
||||||
|
mapRasterSourceId: mapRasterSourceId ?? this.mapRasterSourceId,
|
||||||
|
mapTileEndpointId: mapTileEndpointId ?? this.mapTileEndpointId,
|
||||||
|
mapTileApiKey: mapTileApiKey == _unset
|
||||||
|
? this.mapTileApiKey
|
||||||
|
: mapTileApiKey as String?,
|
||||||
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
||||||
notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage,
|
notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage,
|
||||||
notifyOnNewChannelMessage:
|
notifyOnNewChannelMessage:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import '../l10n/l10n.dart';
|
|||||||
import '../models/app_settings.dart';
|
import '../models/app_settings.dart';
|
||||||
import '../models/translation_support.dart';
|
import '../models/translation_support.dart';
|
||||||
import '../services/app_settings_service.dart';
|
import '../services/app_settings_service.dart';
|
||||||
|
import '../services/map_tile_cache_service.dart';
|
||||||
import '../services/notification_service.dart';
|
import '../services/notification_service.dart';
|
||||||
import '../services/translation_service.dart';
|
import '../services/translation_service.dart';
|
||||||
import '../widgets/adaptive_app_bar_title.dart';
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
@@ -529,6 +530,39 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
onTap: () => _showUnitsDialog(context, settingsService),
|
onTap: () => _showUnitsDialog(context, settingsService),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
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(
|
ListTile(
|
||||||
leading: const Icon(Icons.download_outlined),
|
leading: const Icon(Icons.download_outlined),
|
||||||
title: Text(context.l10n.appSettings_offlineMapCache),
|
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(
|
Widget _buildTranslationCard(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
AppSettingsService settingsService,
|
AppSettingsService settingsService,
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ class _ChannelMessagePathMapScreenState
|
|||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
final settings = context.watch<AppSettingsService>().settings;
|
final settings = context.watch<AppSettingsService>().settings;
|
||||||
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.watch<MapTileCacheService>();
|
||||||
final primaryPath = _selectPrimaryPath(
|
final primaryPath = _selectPrimaryPath(
|
||||||
widget.message.pathBytes,
|
widget.message.pathBytes,
|
||||||
widget.message.pathVariants,
|
widget.message.pathVariants,
|
||||||
@@ -559,7 +559,7 @@ class _ChannelMessagePathMapScreenState
|
|||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate: kMapTileUrlTemplate,
|
urlTemplate: tileCache.urlTemplate,
|
||||||
tileProvider: tileCache.tileProvider,
|
tileProvider: tileCache.tileProvider,
|
||||||
userAgentPackageName:
|
userAgentPackageName:
|
||||||
MapTileCacheService.userAgentPackageName,
|
MapTileCacheService.userAgentPackageName,
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final settings = context.watch<AppSettingsService>().settings;
|
final settings = context.watch<AppSettingsService>().settings;
|
||||||
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.watch<MapTileCacheService>();
|
||||||
final endpoints = _visibleEndpoints();
|
final endpoints = _visibleEndpoints();
|
||||||
final mapPoints = [
|
final mapPoints = [
|
||||||
if (_start != null) _start!.point,
|
if (_start != null) _start!.point,
|
||||||
@@ -470,7 +470,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate: kMapTileUrlTemplate,
|
urlTemplate: tileCache.urlTemplate,
|
||||||
tileProvider: tileCache.tileProvider,
|
tileProvider: tileCache.tileProvider,
|
||||||
userAgentPackageName: MapTileCacheService.userAgentPackageName,
|
userAgentPackageName: MapTileCacheService.userAgentPackageName,
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
|
|
||||||
Future<void> _startDownload() async {
|
Future<void> _startDownload() async {
|
||||||
final bounds = _selectedBounds;
|
final bounds = _selectedBounds;
|
||||||
|
final cacheService = context.read<MapTileCacheService>();
|
||||||
if (bounds == null) {
|
if (bounds == null) {
|
||||||
showDismissibleSnackBar(
|
showDismissibleSnackBar(
|
||||||
context,
|
context,
|
||||||
@@ -187,6 +188,16 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
return;
|
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>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
@@ -209,8 +220,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
|
|
||||||
if (confirmed != true || !mounted) return;
|
if (confirmed != true || !mounted) return;
|
||||||
|
|
||||||
final cacheService = context.read<MapTileCacheService>();
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isDownloading = true;
|
_isDownloading = true;
|
||||||
_completedTiles = 0;
|
_completedTiles = 0;
|
||||||
@@ -278,7 +287,8 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.watch<MapTileCacheService>();
|
||||||
|
final source = tileCache.source;
|
||||||
final selectedBounds = _selectedBounds;
|
final selectedBounds = _selectedBounds;
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||||
@@ -319,7 +329,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate: kMapTileUrlTemplate,
|
urlTemplate: tileCache.urlTemplate,
|
||||||
tileProvider: tileCache.tileProvider,
|
tileProvider: tileCache.tileProvider,
|
||||||
userAgentPackageName:
|
userAgentPackageName:
|
||||||
MapTileCacheService.userAgentPackageName,
|
MapTileCacheService.userAgentPackageName,
|
||||||
@@ -423,6 +433,13 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
Text(l10n.mapCache_estimatedTiles(_estimatedTiles)),
|
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) ...[
|
if (_isDownloading) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
LinearProgressIndicator(value: progressValue),
|
LinearProgressIndicator(value: progressValue),
|
||||||
@@ -441,7 +458,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.download),
|
icon: const Icon(Icons.download),
|
||||||
label: Text(l10n.mapCache_downloadTilesButton),
|
label: Text(l10n.mapCache_downloadTilesButton),
|
||||||
onPressed: _isDownloading || selectedBounds == null
|
onPressed:
|
||||||
|
_isDownloading ||
|
||||||
|
selectedBounds == null ||
|
||||||
|
!source.allowsBulkDownload
|
||||||
? null
|
? null
|
||||||
: _startDownload,
|
: _startDownload,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
|
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
|
||||||
builder: (context, connector, settingsService, pathHistory, child) {
|
builder: (context, connector, settingsService, pathHistory, child) {
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.watch<MapTileCacheService>();
|
||||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||||
final settings = settingsService.settings;
|
final settings = settingsService.settings;
|
||||||
final allContacts = connector.allContacts;
|
final allContacts = connector.allContacts;
|
||||||
@@ -563,7 +563,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate: kMapTileUrlTemplate,
|
urlTemplate: tileCache.urlTemplate,
|
||||||
tileProvider: tileCache.tileProvider,
|
tileProvider: tileCache.tileProvider,
|
||||||
userAgentPackageName:
|
userAgentPackageName:
|
||||||
MapTileCacheService.userAgentPackageName,
|
MapTileCacheService.userAgentPackageName,
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
final settings = context.watch<AppSettingsService>().settings;
|
final settings = context.watch<AppSettingsService>().settings;
|
||||||
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.watch<MapTileCacheService>();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -909,7 +909,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate: kMapTileUrlTemplate,
|
urlTemplate: tileCache.urlTemplate,
|
||||||
tileProvider: tileCache.tileProvider,
|
tileProvider: tileCache.tileProvider,
|
||||||
userAgentPackageName: MapTileCacheService.userAgentPackageName,
|
userAgentPackageName: MapTileCacheService.userAgentPackageName,
|
||||||
maxZoom: 19,
|
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 {
|
Future<void> setNotificationsEnabled(bool value) async {
|
||||||
await updateSettings(_settings.copyWith(notificationsEnabled: value));
|
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:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/widgets.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_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
|
||||||
const String kMapTileUrlTemplate =
|
import '../models/app_settings.dart';
|
||||||
'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
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 {
|
class MapTileCacheProgress {
|
||||||
final int completed;
|
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 cacheKey = 'map_tile_cache';
|
||||||
static const String userAgentPackageName = 'com.meshcore.open';
|
static const String userAgentPackageName = 'com.meshcore.open';
|
||||||
static const int defaultMinZoom = 10;
|
static const int defaultMinZoom = 10;
|
||||||
static const int defaultMaxZoom = 15;
|
static const int defaultMaxZoom = 15;
|
||||||
|
|
||||||
|
final AppSettingsService appSettingsService;
|
||||||
final BaseCacheManager cacheManager;
|
final BaseCacheManager cacheManager;
|
||||||
late final TileProvider tileProvider;
|
late final TileProvider tileProvider;
|
||||||
|
|
||||||
MapTileCacheService({BaseCacheManager? cacheManager})
|
MapTileCacheService({
|
||||||
: cacheManager =
|
required this.appSettingsService,
|
||||||
cacheManager ??
|
BaseCacheManager? cacheManager,
|
||||||
CacheManager(
|
}) : cacheManager =
|
||||||
Config(
|
cacheManager ??
|
||||||
cacheKey,
|
CacheManager(
|
||||||
stalePeriod: const Duration(days: 365),
|
Config(
|
||||||
maxNrOfCacheObjects: 200000,
|
cacheKey,
|
||||||
),
|
stalePeriod: const Duration(days: 365),
|
||||||
) {
|
maxNrOfCacheObjects: 200000,
|
||||||
|
),
|
||||||
|
) {
|
||||||
tileProvider = CachedNetworkTileProvider(cacheManager: this.cacheManager);
|
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 => {
|
Map<String, String> get defaultHeaders => {
|
||||||
'User-Agent': 'flutter_map ($userAgentPackageName)',
|
'User-Agent': 'flutter_map ($userAgentPackageName)',
|
||||||
};
|
};
|
||||||
@@ -89,6 +259,7 @@ class MapTileCacheService {
|
|||||||
final total = estimateTileCount(bounds, safeMin, safeMax);
|
final total = estimateTileCount(bounds, safeMin, safeMax);
|
||||||
final authHeaders = headers ?? defaultHeaders;
|
final authHeaders = headers ?? defaultHeaders;
|
||||||
final safeConcurrency = math.max(1, concurrentDownloads);
|
final safeConcurrency = math.max(1, concurrentDownloads);
|
||||||
|
final currentTemplate = urlTemplate;
|
||||||
int completed = 0;
|
int completed = 0;
|
||||||
int failed = 0;
|
int failed = 0;
|
||||||
|
|
||||||
@@ -124,7 +295,7 @@ class MapTileCacheService {
|
|||||||
final tileBounds = _tileBoundsForBounds(bounds, zoom);
|
final tileBounds = _tileBoundsForBounds(bounds, zoom);
|
||||||
for (int x = tileBounds.minX; x <= tileBounds.maxX; x++) {
|
for (int x = tileBounds.minX; x <= tileBounds.maxX; x++) {
|
||||||
for (int y = tileBounds.minY; y <= tileBounds.maxY; y++) {
|
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);
|
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) {
|
_TileBounds _tileBoundsForBounds(LatLngBounds bounds, int zoom) {
|
||||||
final north = _clampLatitude(bounds.north);
|
final north = _clampLatitude(bounds.north);
|
||||||
final south = _clampLatitude(bounds.south);
|
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) {
|
int _lonToTileX(double lon, int zoom, int maxIndex) {
|
||||||
final n = 1 << zoom;
|
final n = 1 << zoom;
|
||||||
final value = ((lon + 180.0) / 360.0 * n).floor();
|
final value = ((lon + 180.0) / 360.0 * n).floor();
|
||||||
@@ -205,8 +401,8 @@ class MapTileCacheService {
|
|||||||
return lat.clamp(-maxLat, maxLat);
|
return lat.clamp(-maxLat, maxLat);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _buildTileUrl(int x, int y, int zoom) {
|
String _buildTileUrl(int x, int y, int zoom, {required String urlTemplate}) {
|
||||||
return kMapTileUrlTemplate
|
return urlTemplate
|
||||||
.replaceAll('{z}', zoom.toString())
|
.replaceAll('{z}', zoom.toString())
|
||||||
.replaceAll('{x}', x.toString())
|
.replaceAll('{x}', x.toString())
|
||||||
.replaceAll('{y}', y.toString());
|
.replaceAll('{y}', y.toString());
|
||||||
|
|||||||
Reference in New Issue
Block a user