Add custom cave map mode

This commit is contained in:
Janez T
2026-03-16 11:30:53 +01:00
parent 420690d683
commit 450b8382e8

View File

@@ -108,6 +108,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
14.5058, 14.5058,
); // Ljubljana, Slovenia ); // Ljubljana, Slovenia
static const double _defaultZoom = 13.0; static const double _defaultZoom = 13.0;
static const double _customMapMinZoom = -4.0;
static const double _customMapMaxZoom = 40.0;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@@ -404,7 +406,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
: mapProvider.targetLocation!; : mapProvider.targetLocation!;
final zoom = final zoom =
mapProvider.targetCoordinateSpace == MapCoordinateSpace.customMap mapProvider.targetCoordinateSpace == MapCoordinateSpace.customMap
? (mapProvider.targetZoom ?? 2.0).clamp(-4.0, 8.0).toDouble() ? (mapProvider.targetZoom ?? 2.0)
.clamp(_customMapMinZoom, _customMapMaxZoom)
.toDouble()
: mapProvider.targetZoom ?? _defaultZoom; : mapProvider.targetZoom ?? _defaultZoom;
_mapController.move(location, zoom); _mapController.move(location, zoom);
@@ -482,6 +486,25 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
); );
} }
Future<void> _selectMapLayer(MapProvider mapProvider, MapLayer layer) async {
await mapProvider.exitCustomMapMode();
if (!mounted) {
return;
}
setState(() {
_currentLayer = layer;
if (_isMapReady && _mapController.camera.zoom > layer.maxZoom) {
_mapController.move(
_mapController.camera.center,
layer.isWms ? 11.0 : layer.maxZoom,
);
}
});
await _saveSettings();
}
void _showLayerSelector(BuildContext context) { void _showLayerSelector(BuildContext context) {
final rootContext = this.context; final rootContext = this.context;
showModalBottomSheet( showModalBottomSheet(
@@ -513,6 +536,53 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
child: ListView( child: ListView(
shrinkWrap: true, shrinkWrap: true,
children: [ children: [
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
final customMapConfig = mapProvider.customMapConfig;
if (customMapConfig == null) {
return const SizedBox.shrink();
}
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'Saved maps',
style: Theme.of(context).textTheme.labelLarge
?.copyWith(color: Colors.grey[600]),
),
),
),
ListTile(
leading: mapProvider.isUsingCustomMap
? const Icon(
Icons.check_circle,
color: Colors.green,
)
: const Icon(Icons.radio_button_unchecked),
title: Text(customMapConfig.displayName),
subtitle: Text(
'Custom picture map • Map ID ${customMapConfig.mapId}',
),
onTap: () async {
await mapProvider.enterCustomMapMode();
if (!context.mounted) {
return;
}
Navigator.pop(context);
},
),
const Divider(),
],
);
},
),
// Online layers section // Online layers section
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -528,24 +598,21 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
), ),
...MapLayer.allLayers.map( ...MapLayer.allLayers.map(
(layer) => ListTile( (layer) => ListTile(
leading: _currentLayer == layer leading:
!rootContext.read<MapProvider>().isUsingCustomMap &&
_currentLayer == layer
? const Icon(Icons.check_circle, color: Colors.green) ? const Icon(Icons.check_circle, color: Colors.green)
: const Icon(Icons.radio_button_unchecked), : const Icon(Icons.radio_button_unchecked),
title: Text(layer.getLocalizedName(context)), title: Text(layer.getLocalizedName(context)),
subtitle: Text(layer.attribution), subtitle: Text(layer.attribution),
onTap: () async { onTap: () async {
setState(() { await _selectMapLayer(
_currentLayer = layer; rootContext.read<MapProvider>(),
// Clamp zoom level if current zoom exceeds new layer's max layer,
if (_isMapReady && );
_mapController.camera.zoom > layer.maxZoom) { if (!context.mounted) {
_mapController.move( return;
_mapController.camera.center, }
layer.maxZoom,
);
}
});
_saveSettings();
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
@@ -554,50 +621,40 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
if (AppLocalizations.of(context)!.localeName == 'sl' || if (AppLocalizations.of(context)!.localeName == 'sl' ||
AppLocalizations.of(context)!.localeName == 'hr') ...[ AppLocalizations.of(context)!.localeName == 'hr') ...[
ListTile( ListTile(
leading: _currentLayer == _slovenianAerialLayer leading:
!rootContext.read<MapProvider>().isUsingCustomMap &&
_currentLayer == _slovenianAerialLayer
? const Icon(Icons.check_circle, color: Colors.green) ? const Icon(Icons.check_circle, color: Colors.green)
: const Icon(Icons.radio_button_unchecked), : const Icon(Icons.radio_button_unchecked),
title: Text(_slovenianAerialLayer.name), title: Text(_slovenianAerialLayer.name),
subtitle: Text(_slovenianAerialLayer.attribution), subtitle: Text(_slovenianAerialLayer.attribution),
onTap: () async { onTap: () async {
setState(() { await _selectMapLayer(
_currentLayer = _slovenianAerialLayer; rootContext.read<MapProvider>(),
// Clamp zoom level if current zoom exceeds new layer's max _slovenianAerialLayer,
// For WMS layers, use a middle zoom (11) instead of max zoom to avoid extreme close-up );
if (_isMapReady && if (!context.mounted) {
_mapController.camera.zoom > return;
_slovenianAerialLayer.maxZoom) { }
_mapController.move(
_mapController.camera.center,
11.0, // Middle zoom for WMS
);
}
});
_saveSettings();
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
ListTile( ListTile(
leading: _currentLayer == _dtk25Layer leading:
!rootContext.read<MapProvider>().isUsingCustomMap &&
_currentLayer == _dtk25Layer
? const Icon(Icons.check_circle, color: Colors.green) ? const Icon(Icons.check_circle, color: Colors.green)
: const Icon(Icons.radio_button_unchecked), : const Icon(Icons.radio_button_unchecked),
title: Text(AppLocalizations.of(context)!.topographicMap), title: Text(AppLocalizations.of(context)!.topographicMap),
subtitle: Text(_dtk25Layer.attribution), subtitle: Text(_dtk25Layer.attribution),
onTap: () async { onTap: () async {
setState(() { await _selectMapLayer(
_currentLayer = _dtk25Layer; rootContext.read<MapProvider>(),
// Clamp zoom level if current zoom exceeds new layer's max _dtk25Layer,
// For WMS layers, use a middle zoom (11) instead of max zoom to avoid extreme close-up );
if (_isMapReady && if (!context.mounted) {
_mapController.camera.zoom > return;
_dtk25Layer.maxZoom) { }
_mapController.move(
_mapController.camera.center,
11.0, // Middle zoom for WMS
);
}
});
_saveSettings();
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
@@ -605,6 +662,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
// WMS Overlays section (only for Slovenian/Croatian regions and when WMS base layer is selected) // WMS Overlays section (only for Slovenian/Croatian regions and when WMS base layer is selected)
if ((AppLocalizations.of(context)!.localeName == 'sl' || if ((AppLocalizations.of(context)!.localeName == 'sl' ||
AppLocalizations.of(context)!.localeName == 'hr') && AppLocalizations.of(context)!.localeName == 'hr') &&
!rootContext.read<MapProvider>().isUsingCustomMap &&
_currentLayer.isWms) ...[ _currentLayer.isWms) ...[
const Divider(), const Divider(),
Padding( Padding(
@@ -845,23 +903,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
'Map ID ${customMapConfig.mapId}${customMapConfig.isCalibrated ? ' • Scale set' : ' • Not calibrated'}', 'Map ID ${customMapConfig.mapId}${customMapConfig.isCalibrated ? ' • Scale set' : ' • Not calibrated'}',
), ),
), ),
SwitchListTile(
secondary: const Icon(Icons.map_outlined),
title: const Text('Use custom map'),
subtitle: const Text(
'Hide GPS-based layers and work in image space',
),
value: mapProvider.isUsingCustomMap,
onChanged: (value) async {
if (value) {
await mapProvider.enterCustomMapMode();
} else {
await mapProvider.exitCustomMapMode();
}
if (!context.mounted) return;
Navigator.pop(context);
},
),
ListTile( ListTile(
leading: const Icon(Icons.swap_horizontal_circle), leading: const Icon(Icons.swap_horizontal_circle),
title: const Text('Replace image'), title: const Text('Replace image'),
@@ -1919,8 +1960,10 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
) )
: null, : null,
minZoom: isCustomMapMode ? -4 : 0, minZoom: isCustomMapMode ? _customMapMinZoom : 0,
maxZoom: isCustomMapMode ? 8 : _currentLayer.maxZoom, maxZoom: isCustomMapMode
? _customMapMaxZoom
: _currentLayer.maxZoom,
cameraConstraint: isCustomMapMode cameraConstraint: isCustomMapMode
? CameraConstraint.contain( ? CameraConstraint.contain(
bounds: customMapConfig.displayBounds, bounds: customMapConfig.displayBounds,
@@ -2099,6 +2142,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
imageProvider: FileImage( imageProvider: FileImage(
File(customMapConfig.filePath), File(customMapConfig.filePath),
), ),
filterQuality: FilterQuality.none,
), ),
], ],
) )