mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
783 lines
24 KiB
Dart
783 lines
24 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../models/custom_map_config.dart';
|
|
import '../models/location_trail.dart';
|
|
import '../models/map_coordinate_space.dart';
|
|
import '../models/map_drawing.dart';
|
|
import '../models/sar_marker.dart';
|
|
import '../services/profiles_feature_service.dart';
|
|
import '../utils/custom_map_id.dart';
|
|
|
|
class MapProvider with ChangeNotifier {
|
|
static const String _customMapConfigKey = 'custom_map_config_v1';
|
|
static const String _customMapModeKey = 'custom_map_mode_v1';
|
|
static const String _customMapsDirName = 'custom_maps';
|
|
|
|
MapProvider() {
|
|
unawaited(_loadInitialState());
|
|
}
|
|
|
|
final ImagePicker _imagePicker = ImagePicker();
|
|
|
|
LatLng? _targetLocation;
|
|
LatLngBounds? _targetBounds;
|
|
double? _targetZoom;
|
|
bool _shouldAnimate = false;
|
|
MapCoordinateSpace _targetCoordinateSpace = MapCoordinateSpace.geo;
|
|
String? _targetMapId;
|
|
|
|
LocationTrail? _currentTrail;
|
|
bool _isTrailVisible = true;
|
|
bool _isTrailRecordingEnabled = true;
|
|
final List<LocationTrail> _trailHistory = [];
|
|
|
|
bool _showCadastralOverlay = false;
|
|
bool _showForestRoadsOverlay = false;
|
|
bool _showHikingTrailsOverlay = false;
|
|
bool _showMainRoadsOverlay = false;
|
|
bool _showHouseNumbersOverlay = false;
|
|
bool _showFireHazardZonesOverlay = false;
|
|
bool _showHistoricalFiresOverlay = false;
|
|
bool _showFirebreaksOverlay = false;
|
|
bool _showKrasFireZonesOverlay = false;
|
|
bool _showPlaceNamesOverlay = false;
|
|
bool _showMunicipalityBordersOverlay = false;
|
|
|
|
bool _hideRepeatersOnMap = false;
|
|
|
|
LocationTrail? _importedTrail;
|
|
|
|
bool _isSelectingDownloadArea = false;
|
|
LatLngBounds? _downloadAreaBounds;
|
|
|
|
CustomMapConfig? _customMapConfig;
|
|
bool _isUsingCustomMap = false;
|
|
|
|
LatLng? get targetLocation => _targetLocation;
|
|
LatLngBounds? get targetBounds => _targetBounds;
|
|
double? get targetZoom => _targetZoom;
|
|
bool get shouldAnimate => _shouldAnimate;
|
|
MapCoordinateSpace get targetCoordinateSpace => _targetCoordinateSpace;
|
|
String? get targetMapId => _targetMapId;
|
|
LocationTrail? get currentTrail => _currentTrail;
|
|
bool get isTrailVisible => _isTrailVisible;
|
|
bool get isTrailRecordingEnabled => _isTrailRecordingEnabled;
|
|
List<LocationTrail> get trailHistory => List.unmodifiable(_trailHistory);
|
|
bool get isTrailActive => _currentTrail?.isActive ?? false;
|
|
|
|
bool get showCadastralOverlay => _showCadastralOverlay;
|
|
bool get showForestRoadsOverlay => _showForestRoadsOverlay;
|
|
bool get showHikingTrailsOverlay => _showHikingTrailsOverlay;
|
|
bool get showMainRoadsOverlay => _showMainRoadsOverlay;
|
|
bool get showHouseNumbersOverlay => _showHouseNumbersOverlay;
|
|
bool get showFireHazardZonesOverlay => _showFireHazardZonesOverlay;
|
|
bool get showHistoricalFiresOverlay => _showHistoricalFiresOverlay;
|
|
bool get showFirebreaksOverlay => _showFirebreaksOverlay;
|
|
bool get showKrasFireZonesOverlay => _showKrasFireZonesOverlay;
|
|
bool get showPlaceNamesOverlay => _showPlaceNamesOverlay;
|
|
bool get showMunicipalityBordersOverlay => _showMunicipalityBordersOverlay;
|
|
|
|
bool get hideRepeatersOnMap => _hideRepeatersOnMap;
|
|
|
|
LocationTrail? get importedTrail => _importedTrail;
|
|
|
|
bool get isSelectingDownloadArea => _isSelectingDownloadArea;
|
|
LatLngBounds? get downloadAreaBounds => _downloadAreaBounds;
|
|
|
|
CustomMapConfig? get customMapConfig => _customMapConfig;
|
|
bool get hasCustomMap => _customMapConfig != null;
|
|
bool get isUsingCustomMap => _isUsingCustomMap && _customMapConfig != null;
|
|
bool get shouldHideGpsData => isUsingCustomMap;
|
|
|
|
LatLngBounds? get customMapBounds => _customMapConfig?.bounds;
|
|
|
|
Future<void> reloadProfileScopedState() async {
|
|
await _loadInitialState();
|
|
}
|
|
|
|
bool matchesActiveCustomMap(String? mapId) {
|
|
return hasCustomMap &&
|
|
normalizeCustomMapId(_customMapConfig!.mapId) ==
|
|
normalizeCustomMapId(mapId);
|
|
}
|
|
|
|
void navigateToLocation({
|
|
required LatLng location,
|
|
double zoom = 15.0,
|
|
bool animate = true,
|
|
}) {
|
|
if (_isUsingCustomMap) {
|
|
_isUsingCustomMap = false;
|
|
unawaited(_saveCustomMapState());
|
|
}
|
|
_targetLocation = location;
|
|
_targetBounds = null;
|
|
_targetZoom = zoom;
|
|
_shouldAnimate = animate;
|
|
_targetCoordinateSpace = MapCoordinateSpace.geo;
|
|
_targetMapId = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
String? navigateToMapPoint({
|
|
required LatLng point,
|
|
required MapCoordinateSpace coordinateSpace,
|
|
String? mapId,
|
|
double zoom = 15.0,
|
|
bool animate = true,
|
|
}) {
|
|
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
|
if (!matchesActiveCustomMap(mapId)) {
|
|
return 'Load the matching custom map to view this item.';
|
|
}
|
|
if (!_isUsingCustomMap) {
|
|
_isUsingCustomMap = true;
|
|
unawaited(_saveCustomMapState());
|
|
}
|
|
} else if (_isUsingCustomMap) {
|
|
_isUsingCustomMap = false;
|
|
unawaited(_saveCustomMapState());
|
|
}
|
|
|
|
_targetLocation = point;
|
|
_targetBounds = null;
|
|
_targetZoom = zoom;
|
|
_shouldAnimate = animate;
|
|
_targetCoordinateSpace = coordinateSpace;
|
|
_targetMapId = mapId;
|
|
notifyListeners();
|
|
return null;
|
|
}
|
|
|
|
String? navigateToBounds({
|
|
required LatLngBounds bounds,
|
|
required MapCoordinateSpace coordinateSpace,
|
|
String? mapId,
|
|
bool animate = true,
|
|
}) {
|
|
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
|
if (!matchesActiveCustomMap(mapId)) {
|
|
return 'Load the matching custom map to view this item.';
|
|
}
|
|
if (!_isUsingCustomMap) {
|
|
_isUsingCustomMap = true;
|
|
unawaited(_saveCustomMapState());
|
|
}
|
|
} else if (_isUsingCustomMap) {
|
|
_isUsingCustomMap = false;
|
|
unawaited(_saveCustomMapState());
|
|
}
|
|
|
|
_targetBounds = bounds;
|
|
_targetLocation = bounds.center;
|
|
_targetZoom = null;
|
|
_shouldAnimate = animate;
|
|
_targetCoordinateSpace = coordinateSpace;
|
|
_targetMapId = mapId;
|
|
notifyListeners();
|
|
return null;
|
|
}
|
|
|
|
void clearNavigation() {
|
|
_targetLocation = null;
|
|
_targetBounds = null;
|
|
_targetZoom = null;
|
|
_shouldAnimate = false;
|
|
_targetCoordinateSpace = MapCoordinateSpace.geo;
|
|
_targetMapId = null;
|
|
}
|
|
|
|
String? navigateToDrawing(String drawingId, dynamic drawingProvider) {
|
|
final drawing = drawingProvider.getDrawingById(drawingId) as MapDrawing?;
|
|
if (drawing == null) {
|
|
return 'Drawing not found.';
|
|
}
|
|
|
|
if (drawing.coordinateSpace == MapCoordinateSpace.customMap) {
|
|
if (!matchesActiveCustomMap(drawing.mapId)) {
|
|
return 'Load the matching custom map to view this drawing.';
|
|
}
|
|
return navigateToBounds(
|
|
bounds: drawing.getBounds(),
|
|
coordinateSpace: drawing.coordinateSpace,
|
|
mapId: drawing.mapId,
|
|
);
|
|
}
|
|
|
|
final bounds = drawing.getBounds();
|
|
final latDiff = (bounds.north - bounds.south).abs();
|
|
final lonDiff = (bounds.east - bounds.west).abs();
|
|
final maxDiff = latDiff > lonDiff ? latDiff : lonDiff;
|
|
|
|
double zoom = 15.0;
|
|
if (maxDiff < 0.001) {
|
|
zoom = 17.0;
|
|
} else if (maxDiff < 0.005) {
|
|
zoom = 16.0;
|
|
} else if (maxDiff < 0.01) {
|
|
zoom = 15.0;
|
|
} else if (maxDiff < 0.05) {
|
|
zoom = 13.0;
|
|
} else if (maxDiff < 0.1) {
|
|
zoom = 12.0;
|
|
} else {
|
|
zoom = 10.0;
|
|
}
|
|
|
|
navigateToLocation(
|
|
location: drawing.getCenter(),
|
|
zoom: zoom,
|
|
animate: true,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
String? navigateToSarMarker(SarMarker marker) {
|
|
if (marker.coordinateSpace == MapCoordinateSpace.customMap) {
|
|
return navigateToMapPoint(
|
|
point: marker.location,
|
|
coordinateSpace: MapCoordinateSpace.customMap,
|
|
mapId: marker.mapId,
|
|
);
|
|
}
|
|
navigateToLocation(location: marker.location, zoom: 15.0, animate: true);
|
|
return null;
|
|
}
|
|
|
|
Future<bool> loadCustomMapFromGallery() async {
|
|
final picked = await _imagePicker.pickImage(source: ImageSource.gallery);
|
|
if (picked == null) {
|
|
return false;
|
|
}
|
|
final bytes = await picked.readAsBytes();
|
|
await setCustomMapImage(bytes: bytes, displayName: picked.name);
|
|
return true;
|
|
}
|
|
|
|
Future<void> replaceCustomMap() async {
|
|
await loadCustomMapFromGallery();
|
|
}
|
|
|
|
Future<void> setCustomMapImage({
|
|
required Uint8List bytes,
|
|
required String displayName,
|
|
}) async {
|
|
final dimensions = await _decodeImageSize(bytes);
|
|
final mapId = normalizeCustomMapId(sha256.convert(bytes).toString())!;
|
|
final documentsDir = await getApplicationDocumentsDirectory();
|
|
final dir = Directory('${documentsDir.path}/$_customMapsDirName');
|
|
if (!await dir.exists()) {
|
|
await dir.create(recursive: true);
|
|
}
|
|
|
|
final extension = _normalizedExtension(displayName);
|
|
final nextPath = '${dir.path}/custom_map_$mapId.$extension';
|
|
final nextFile = File(nextPath);
|
|
await nextFile.writeAsBytes(bytes, flush: true);
|
|
|
|
final previousPath = _customMapConfig?.filePath;
|
|
_customMapConfig = CustomMapConfig(
|
|
filePath: nextPath,
|
|
displayName: displayName,
|
|
mapId: mapId,
|
|
imageWidth: dimensions.$1,
|
|
imageHeight: dimensions.$2,
|
|
);
|
|
_isUsingCustomMap = true;
|
|
await _saveCustomMapState();
|
|
|
|
if (previousPath != null && previousPath != nextPath) {
|
|
unawaited(_deleteFileIfExists(previousPath));
|
|
}
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> setCustomMapCalibration({
|
|
required LatLng pointA,
|
|
required LatLng pointB,
|
|
required double metersPerPixel,
|
|
}) async {
|
|
if (_customMapConfig == null) return;
|
|
_customMapConfig = _customMapConfig!.copyWith(
|
|
calibrationPointA: pointA,
|
|
calibrationPointB: pointB,
|
|
metersPerPixel: metersPerPixel,
|
|
);
|
|
await _saveCustomMapState();
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> clearCustomMapCalibration() async {
|
|
if (_customMapConfig == null) return;
|
|
_customMapConfig = _customMapConfig!.copyWith(
|
|
clearMetersPerPixel: true,
|
|
clearCalibrationPointA: true,
|
|
clearCalibrationPointB: true,
|
|
);
|
|
await _saveCustomMapState();
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> removeCustomMap() async {
|
|
final filePath = _customMapConfig?.filePath;
|
|
_customMapConfig = null;
|
|
_isUsingCustomMap = false;
|
|
clearNavigation();
|
|
await _saveCustomMapState();
|
|
if (filePath != null) {
|
|
await _deleteFileIfExists(filePath);
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> enterCustomMapMode() async {
|
|
if (!hasCustomMap || _isUsingCustomMap) return;
|
|
_isUsingCustomMap = true;
|
|
await _saveCustomMapState();
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> exitCustomMapMode() async {
|
|
if (!_isUsingCustomMap) return;
|
|
_isUsingCustomMap = false;
|
|
await _saveCustomMapState();
|
|
notifyListeners();
|
|
}
|
|
|
|
void updateZoom(double zoom) {
|
|
_targetZoom = zoom;
|
|
notifyListeners();
|
|
}
|
|
|
|
void startTrail() {
|
|
if (_currentTrail != null && _currentTrail!.isActive) {
|
|
endTrail();
|
|
}
|
|
|
|
_currentTrail = LocationTrail(
|
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
|
startTime: DateTime.now(),
|
|
);
|
|
_isTrailVisible = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
void addTrailPoint(LatLng position, {double? accuracy, double? speed}) {
|
|
if (!_isTrailRecordingEnabled) {
|
|
return;
|
|
}
|
|
|
|
if (_currentTrail == null || !_currentTrail!.isActive) {
|
|
startTrail();
|
|
}
|
|
|
|
_currentTrail!.addPoint(
|
|
TrailPoint(
|
|
position: position,
|
|
timestamp: DateTime.now(),
|
|
accuracy: accuracy,
|
|
speed: speed,
|
|
),
|
|
);
|
|
notifyListeners();
|
|
}
|
|
|
|
void endTrail() {
|
|
if (_currentTrail != null) {
|
|
_currentTrail!.isActive = false;
|
|
_currentTrail!.endTime = DateTime.now();
|
|
if (_currentTrail!.points.isNotEmpty) {
|
|
_trailHistory.add(_currentTrail!);
|
|
}
|
|
_currentTrail = null;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void toggleTrailVisibility() {
|
|
_isTrailVisible = !_isTrailVisible;
|
|
notifyListeners();
|
|
}
|
|
|
|
void setTrailRecordingEnabled(bool enabled) {
|
|
if (_isTrailRecordingEnabled == enabled) {
|
|
return;
|
|
}
|
|
|
|
_isTrailRecordingEnabled = enabled;
|
|
if (!enabled) {
|
|
if (_currentTrail != null) {
|
|
endTrail();
|
|
} else {
|
|
notifyListeners();
|
|
}
|
|
return;
|
|
}
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
void clearCurrentTrail() {
|
|
if (_currentTrail != null) {
|
|
_currentTrail = null;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void clearAllTrails() {
|
|
_currentTrail = null;
|
|
_trailHistory.clear();
|
|
notifyListeners();
|
|
}
|
|
|
|
double get totalTrailDistance {
|
|
if (_currentTrail == null) return 0;
|
|
return _currentTrail!.totalDistance;
|
|
}
|
|
|
|
Duration get trailDuration {
|
|
if (_currentTrail == null) return Duration.zero;
|
|
return _currentTrail!.duration;
|
|
}
|
|
|
|
Future<void> toggleCadastralOverlay() async {
|
|
_showCadastralOverlay = !_showCadastralOverlay;
|
|
notifyListeners();
|
|
await _saveOverlayState();
|
|
}
|
|
|
|
Future<void> toggleForestRoadsOverlay() async {
|
|
_showForestRoadsOverlay = !_showForestRoadsOverlay;
|
|
notifyListeners();
|
|
await _saveOverlayState();
|
|
}
|
|
|
|
Future<void> toggleHikingTrailsOverlay() async {
|
|
_showHikingTrailsOverlay = !_showHikingTrailsOverlay;
|
|
notifyListeners();
|
|
await _saveOverlayState();
|
|
}
|
|
|
|
Future<void> toggleMainRoadsOverlay() async {
|
|
_showMainRoadsOverlay = !_showMainRoadsOverlay;
|
|
notifyListeners();
|
|
await _saveOverlayState();
|
|
}
|
|
|
|
Future<void> toggleHouseNumbersOverlay() async {
|
|
_showHouseNumbersOverlay = !_showHouseNumbersOverlay;
|
|
notifyListeners();
|
|
await _saveOverlayState();
|
|
}
|
|
|
|
Future<void> toggleFireHazardZonesOverlay() async {
|
|
_showFireHazardZonesOverlay = !_showFireHazardZonesOverlay;
|
|
notifyListeners();
|
|
await _saveOverlayState();
|
|
}
|
|
|
|
Future<void> toggleHistoricalFiresOverlay() async {
|
|
_showHistoricalFiresOverlay = !_showHistoricalFiresOverlay;
|
|
notifyListeners();
|
|
await _saveOverlayState();
|
|
}
|
|
|
|
Future<void> toggleFirebreaksOverlay() async {
|
|
_showFirebreaksOverlay = !_showFirebreaksOverlay;
|
|
notifyListeners();
|
|
await _saveOverlayState();
|
|
}
|
|
|
|
Future<void> toggleKrasFireZonesOverlay() async {
|
|
_showKrasFireZonesOverlay = !_showKrasFireZonesOverlay;
|
|
notifyListeners();
|
|
await _saveOverlayState();
|
|
}
|
|
|
|
Future<void> togglePlaceNamesOverlay() async {
|
|
_showPlaceNamesOverlay = !_showPlaceNamesOverlay;
|
|
notifyListeners();
|
|
await _saveOverlayState();
|
|
}
|
|
|
|
Future<void> toggleMunicipalityBordersOverlay() async {
|
|
_showMunicipalityBordersOverlay = !_showMunicipalityBordersOverlay;
|
|
notifyListeners();
|
|
await _saveOverlayState();
|
|
}
|
|
|
|
Future<void> loadOverlayState() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
_showCadastralOverlay =
|
|
prefs.getBool(_scopedKey('map_show_cadastral_overlay')) ?? false;
|
|
_showForestRoadsOverlay =
|
|
prefs.getBool(_scopedKey('map_show_forest_roads_overlay')) ?? false;
|
|
_showHikingTrailsOverlay =
|
|
prefs.getBool(_scopedKey('map_show_hiking_trails_overlay')) ?? false;
|
|
_showMainRoadsOverlay =
|
|
prefs.getBool(_scopedKey('map_show_main_roads_overlay')) ?? false;
|
|
_showHouseNumbersOverlay =
|
|
prefs.getBool(_scopedKey('map_show_house_numbers_overlay')) ?? false;
|
|
_showFireHazardZonesOverlay =
|
|
prefs.getBool(_scopedKey('map_show_fire_hazard_zones_overlay')) ??
|
|
false;
|
|
_showHistoricalFiresOverlay =
|
|
prefs.getBool(_scopedKey('map_show_historical_fires_overlay')) ?? false;
|
|
_showFirebreaksOverlay =
|
|
prefs.getBool(_scopedKey('map_show_firebreaks_overlay')) ?? false;
|
|
_showKrasFireZonesOverlay =
|
|
prefs.getBool(_scopedKey('map_show_kras_fire_zones_overlay')) ?? false;
|
|
_showPlaceNamesOverlay =
|
|
prefs.getBool(_scopedKey('map_show_place_names_overlay')) ?? false;
|
|
_showMunicipalityBordersOverlay =
|
|
prefs.getBool(_scopedKey('map_show_municipality_borders_overlay')) ??
|
|
false;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> _loadInitialState() async {
|
|
await Future.wait([
|
|
loadOverlayState(),
|
|
loadRepeaterVisibilitySettings(),
|
|
_loadCustomMapState(),
|
|
_removeDeprecatedSettings(),
|
|
]);
|
|
}
|
|
|
|
Future<void> _removeDeprecatedSettings() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(_scopedKey('map_show_all_contact_trails'));
|
|
}
|
|
|
|
Future<void> _saveOverlayState() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setBool(
|
|
_scopedKey('map_show_cadastral_overlay'),
|
|
_showCadastralOverlay,
|
|
);
|
|
await prefs.setBool(
|
|
_scopedKey('map_show_forest_roads_overlay'),
|
|
_showForestRoadsOverlay,
|
|
);
|
|
await prefs.setBool(
|
|
_scopedKey('map_show_hiking_trails_overlay'),
|
|
_showHikingTrailsOverlay,
|
|
);
|
|
await prefs.setBool(
|
|
_scopedKey('map_show_main_roads_overlay'),
|
|
_showMainRoadsOverlay,
|
|
);
|
|
await prefs.setBool(
|
|
_scopedKey('map_show_house_numbers_overlay'),
|
|
_showHouseNumbersOverlay,
|
|
);
|
|
await prefs.setBool(
|
|
_scopedKey('map_show_fire_hazard_zones_overlay'),
|
|
_showFireHazardZonesOverlay,
|
|
);
|
|
await prefs.setBool(
|
|
_scopedKey('map_show_historical_fires_overlay'),
|
|
_showHistoricalFiresOverlay,
|
|
);
|
|
await prefs.setBool(
|
|
_scopedKey('map_show_firebreaks_overlay'),
|
|
_showFirebreaksOverlay,
|
|
);
|
|
await prefs.setBool(
|
|
_scopedKey('map_show_kras_fire_zones_overlay'),
|
|
_showKrasFireZonesOverlay,
|
|
);
|
|
await prefs.setBool(
|
|
_scopedKey('map_show_place_names_overlay'),
|
|
_showPlaceNamesOverlay,
|
|
);
|
|
await prefs.setBool(
|
|
_scopedKey('map_show_municipality_borders_overlay'),
|
|
_showMunicipalityBordersOverlay,
|
|
);
|
|
}
|
|
|
|
Future<void> setHideRepeatersOnMap(bool hide) async {
|
|
if (_hideRepeatersOnMap == hide) return;
|
|
_hideRepeatersOnMap = hide;
|
|
notifyListeners();
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setBool(_scopedKey('map_hide_repeaters'), _hideRepeatersOnMap);
|
|
}
|
|
|
|
Future<void> loadRepeaterVisibilitySettings() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
_hideRepeatersOnMap =
|
|
prefs.getBool(_scopedKey('map_hide_repeaters')) ?? false;
|
|
notifyListeners();
|
|
}
|
|
|
|
void setImportedTrail(LocationTrail trail) {
|
|
_importedTrail = trail;
|
|
notifyListeners();
|
|
}
|
|
|
|
void clearImportedTrail() {
|
|
_importedTrail = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
void replaceCurrentTrailWithImport(LocationTrail importedTrail) {
|
|
if (_currentTrail != null && _currentTrail!.isActive) {
|
|
endTrail();
|
|
}
|
|
_currentTrail = importedTrail;
|
|
_isTrailVisible = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
void enterDownloadAreaMode(LatLngBounds initialBounds) {
|
|
_isSelectingDownloadArea = true;
|
|
_downloadAreaBounds = initialBounds;
|
|
notifyListeners();
|
|
}
|
|
|
|
void exitDownloadAreaMode() {
|
|
_isSelectingDownloadArea = false;
|
|
_downloadAreaBounds = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
void updateDownloadAreaBounds(LatLngBounds bounds) {
|
|
_downloadAreaBounds = bounds;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> _loadCustomMapState() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final configJson = prefs.getString(_scopedKey(_customMapConfigKey));
|
|
if (configJson != null && configJson.isNotEmpty) {
|
|
final decoded = jsonDecode(configJson);
|
|
if (decoded is Map<String, dynamic>) {
|
|
final config = CustomMapConfig.fromJson(decoded);
|
|
if (config != null && await File(config.filePath).exists()) {
|
|
_customMapConfig = config;
|
|
} else {
|
|
_customMapConfig = null;
|
|
}
|
|
}
|
|
} else {
|
|
_customMapConfig = null;
|
|
}
|
|
_isUsingCustomMap =
|
|
(prefs.getBool(_scopedKey(_customMapModeKey)) ?? false) &&
|
|
_customMapConfig != null;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> _saveCustomMapState() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
if (_customMapConfig == null) {
|
|
await prefs.remove(_scopedKey(_customMapConfigKey));
|
|
} else {
|
|
await prefs.setString(
|
|
_scopedKey(_customMapConfigKey),
|
|
jsonEncode(_customMapConfig!.toJson()),
|
|
);
|
|
}
|
|
await prefs.setBool(_scopedKey(_customMapModeKey), _isUsingCustomMap);
|
|
}
|
|
|
|
Future<(int, int)> _decodeImageSize(Uint8List bytes) async {
|
|
final codec = await ui.instantiateImageCodec(bytes);
|
|
final frame = await codec.getNextFrame();
|
|
return (frame.image.width, frame.image.height);
|
|
}
|
|
|
|
String _normalizedExtension(String displayName) {
|
|
final dotIndex = displayName.lastIndexOf('.');
|
|
if (dotIndex == -1 || dotIndex == displayName.length - 1) {
|
|
return 'png';
|
|
}
|
|
return displayName.substring(dotIndex + 1).toLowerCase();
|
|
}
|
|
|
|
Future<void> _deleteFileIfExists(String path) async {
|
|
final file = File(path);
|
|
if (await file.exists()) {
|
|
await file.delete();
|
|
}
|
|
}
|
|
|
|
Map<String, dynamic> exportWorkspaceJson() {
|
|
return {
|
|
'currentTrail': _currentTrail?.toJson(),
|
|
'trailHistory': _trailHistory.map((trail) => trail.toJson()).toList(),
|
|
'importedTrail': _importedTrail?.toJson(),
|
|
'isTrailVisible': _isTrailVisible,
|
|
'isTrailRecordingEnabled': _isTrailRecordingEnabled,
|
|
'showCadastralOverlay': _showCadastralOverlay,
|
|
'showForestRoadsOverlay': _showForestRoadsOverlay,
|
|
'showHikingTrailsOverlay': _showHikingTrailsOverlay,
|
|
'showMainRoadsOverlay': _showMainRoadsOverlay,
|
|
'showHouseNumbersOverlay': _showHouseNumbersOverlay,
|
|
'showFireHazardZonesOverlay': _showFireHazardZonesOverlay,
|
|
'showHistoricalFiresOverlay': _showHistoricalFiresOverlay,
|
|
'showFirebreaksOverlay': _showFirebreaksOverlay,
|
|
'showKrasFireZonesOverlay': _showKrasFireZonesOverlay,
|
|
'showPlaceNamesOverlay': _showPlaceNamesOverlay,
|
|
'showMunicipalityBordersOverlay': _showMunicipalityBordersOverlay,
|
|
'hideRepeatersOnMap': _hideRepeatersOnMap,
|
|
};
|
|
}
|
|
|
|
void applyWorkspaceJson(Map<String, dynamic> json) {
|
|
_currentTrail = json['currentTrail'] is Map<String, dynamic>
|
|
? LocationTrail.fromJson(json['currentTrail'] as Map<String, dynamic>)
|
|
: null;
|
|
_trailHistory
|
|
..clear()
|
|
..addAll(
|
|
(json['trailHistory'] as List<dynamic>? ?? const [])
|
|
.whereType<Map<String, dynamic>>()
|
|
.map(LocationTrail.fromJson),
|
|
);
|
|
_importedTrail = json['importedTrail'] is Map<String, dynamic>
|
|
? LocationTrail.fromJson(json['importedTrail'] as Map<String, dynamic>)
|
|
: null;
|
|
_isTrailVisible = json['isTrailVisible'] as bool? ?? true;
|
|
_isTrailRecordingEnabled =
|
|
json['isTrailRecordingEnabled'] as bool? ?? true;
|
|
_showCadastralOverlay = json['showCadastralOverlay'] as bool? ?? false;
|
|
_showForestRoadsOverlay = json['showForestRoadsOverlay'] as bool? ?? false;
|
|
_showHikingTrailsOverlay =
|
|
json['showHikingTrailsOverlay'] as bool? ?? false;
|
|
_showMainRoadsOverlay = json['showMainRoadsOverlay'] as bool? ?? false;
|
|
_showHouseNumbersOverlay =
|
|
json['showHouseNumbersOverlay'] as bool? ?? false;
|
|
_showFireHazardZonesOverlay =
|
|
json['showFireHazardZonesOverlay'] as bool? ?? false;
|
|
_showHistoricalFiresOverlay =
|
|
json['showHistoricalFiresOverlay'] as bool? ?? false;
|
|
_showFirebreaksOverlay = json['showFirebreaksOverlay'] as bool? ?? false;
|
|
_showKrasFireZonesOverlay =
|
|
json['showKrasFireZonesOverlay'] as bool? ?? false;
|
|
_showPlaceNamesOverlay = json['showPlaceNamesOverlay'] as bool? ?? false;
|
|
_showMunicipalityBordersOverlay =
|
|
json['showMunicipalityBordersOverlay'] as bool? ?? false;
|
|
_hideRepeatersOnMap = json['hideRepeatersOnMap'] as bool? ?? false;
|
|
notifyListeners();
|
|
}
|
|
|
|
String _scopedKey(String baseKey) {
|
|
return ProfileStorageScope.scopedKey(baseKey);
|
|
}
|
|
}
|