better limits for min/max zoom validations, separate lat+lon validation, less verbose comments

This commit is contained in:
stopflock
2026-07-25 16:42:38 -05:00
parent dd8c9b88cc
commit af2a08bf3d
5 changed files with 87 additions and 98 deletions
+17 -13
View File
@@ -1,23 +1,27 @@
/// Shared validation helpers for geographic coordinates and zoom levels.
///
/// Used anywhere a [LatLng]-like value or zoom level comes from an external
/// or platform source (GPS fixes, persisted preferences, live map camera
/// state) that isn't guaranteed to be finite/in-range. Centralizing this
/// avoids subtly inconsistent validation logic being duplicated across the
/// GPS controller, map position persistence, and live camera guards.
/// Used wherever a coordinate or zoom level comes from an external source
/// (GPS fixes, persisted preferences, live map camera state) that isn't
/// guaranteed to be finite/in-range.
library;
/// Validate that a latitude or longitude value is finite and within the
/// valid range for geographic coordinates (-180 to 180).
///
/// Note: this intentionally uses the wider +/-180 range (rather than +/-90
/// for latitude) so a single helper can validate both latitude and
/// longitude values without the caller needing to track which is which.
bool isValidCoordinate(double value) {
import '../dev_config.dart' show kAbsoluteMaxZoom;
/// Validate that a latitude value is finite and within -90 to 90.
bool isValidLatitude(double value) {
return !value.isNaN && !value.isInfinite && value >= -90.0 && value <= 90.0;
}
/// Validate that a longitude value is finite and within -180 to 180.
bool isValidLongitude(double value) {
return !value.isNaN && !value.isInfinite && value >= -180.0 && value <= 180.0;
}
/// Validate that a zoom level is finite and within a sane range.
bool isValidZoom(double zoom, {double min = 0.0, double max = 25.0}) {
///
/// Defaults to 1.0 to [kAbsoluteMaxZoom]. `num` is used (rather than
/// `double`) so the `int` constant [kAbsoluteMaxZoom] can be used directly
/// as a default parameter value.
bool isValidZoom(double zoom, {num min = 1.0, num max = kAbsoluteMaxZoom}) {
return !zoom.isNaN && !zoom.isInfinite && zoom >= min && zoom <= max;
}
+4 -7
View File
@@ -189,13 +189,10 @@ class GpsController {
/// Handle incoming GPS position
void _onPositionReceived(Position position) {
// Guard against malformed fixes from the platform location stack (some
// OEM/fused location providers occasionally emit NaN/Infinite lat/lng,
// especially while a fix is still being acquired). An unvalidated bad
// value here can crash the app at launch (via initialCenter) or corrupt
// the live map camera (via follow-me animateTo), so treat it like any
// other transient location error rather than trusting it.
if (!isValidCoordinate(position.latitude) || !isValidCoordinate(position.longitude)) {
// Reject malformed fixes (occasionally NaN/Infinite from the platform
// location stack) before they reach map state.
if (!isValidLatitude(position.latitude) || !isValidLongitude(position.longitude)) {
debugPrint(
'[GpsController] Ignoring invalid GPS position: '
'lat=${position.latitude}, lng=${position.longitude}',
+11 -27
View File
@@ -3,9 +3,7 @@ import 'package:flutter_map_animations/flutter_map_animations.dart';
import 'package:latlong2/latlong.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../services/coordinate_validation.dart' as coord_validation;
import '../../services/coordinate_validation.dart';
/// Manages map position persistence and initial positioning.
/// Handles saving/loading last map position and moving to initial locations.
@@ -32,10 +30,9 @@ class MapPositionManager {
final lng = prefs.getDouble('last_map_longitude');
final zoom = prefs.getDouble('last_map_zoom');
if (lat != null && lng != null &&
coord_validation.isValidCoordinate(lat) && coord_validation.isValidCoordinate(lng)) {
final validZoom = zoom != null && _isValidZoom(zoom) ? zoom : 15.0;
if (lat != null && lng != null &&
isValidLatitude(lat) && isValidLongitude(lng)) {
final validZoom = zoom != null && isValidZoom(zoom) ? zoom : 15.0;
_initialLocation = LatLng(lat, lng);
_initialZoom = validZoom;
debugPrint('[MapPositionManager] Loaded last map position: ${_initialLocation!.latitude}, ${_initialLocation!.longitude}, zoom: $_initialZoom');
@@ -54,10 +51,9 @@ class MapPositionManager {
try {
final zoom = _initialZoom ?? 15.0;
// Double-check coordinates are valid before moving
if (coord_validation.isValidCoordinate(_initialLocation!.latitude) &&
coord_validation.isValidCoordinate(_initialLocation!.longitude) &&
_isValidZoom(zoom)) {
if (isValidLatitude(_initialLocation!.latitude) &&
isValidLongitude(_initialLocation!.longitude) &&
isValidZoom(zoom)) {
controller.mapController.move(_initialLocation!, zoom);
_hasMovedToInitialLocation = true;
debugPrint('[MapPositionManager] Moved to initial location: ${_initialLocation!.latitude}, ${_initialLocation!.longitude}');
@@ -75,10 +71,9 @@ class MapPositionManager {
Future<void> saveMapPosition(LatLng location, double zoom) async {
try {
// Validate coordinates and zoom before saving
if (!coord_validation.isValidCoordinate(location.latitude) ||
!coord_validation.isValidCoordinate(location.longitude) ||
!_isValidZoom(zoom)) {
if (!isValidLatitude(location.latitude) ||
!isValidLongitude(location.longitude) ||
!isValidZoom(zoom)) {
debugPrint('[MapPositionManager] Invalid map position, not saving: lat=${location.latitude}, lng=${location.longitude}, zoom=$zoom');
return;
}
@@ -93,8 +88,6 @@ class MapPositionManager {
}
}
/// Clear any stored map position (useful for recovery from invalid data)
static Future<void> clearStoredMapPosition() async {
try {
@@ -107,13 +100,4 @@ class MapPositionManager {
debugPrint('[MapPositionManager] Failed to clear stored map position: $e');
}
}
/// Validate that a zoom level is valid
bool _isValidZoom(double zoom) {
return !zoom.isNaN &&
!zoom.isInfinite &&
zoom >= 1.0 &&
zoom <= 25.0;
}
}
}
+17 -29
View File
@@ -84,14 +84,12 @@ class MapViewState extends State<MapView> {
// Track map center to clear queue on significant panning
LatLng? _lastCenter;
// Track the last known-good (finite, in-range) camera center/zoom so we
// can self-heal if the camera ever gets pushed into a degenerate state
// (e.g. by an edge case in flutter_map's internal gesture-to-camera
// pipeline). This mirrors the validation used for persisted map position
// in MapPositionManager, applied to the *live* camera as well.
// Last known-good camera center/zoom, used to self-heal if the camera
// ever gets pushed into an invalid state.
LatLng? _lastValidCenter;
double? _lastValidZoom;
// State for proximity alert banner
bool _showProximityBanner = false;
@@ -287,22 +285,19 @@ class MapViewState extends State<MapView> {
return (!appState.offlineMode && appState.isInSearchMode) ? 60.0 : 0.0;
}
/// Get a known-good fallback location for [initialCenter], preferring a
/// validated GPS location, then a validated persisted position, then a
/// hardcoded default. GpsController already validates incoming fixes, but
/// this extra check keeps launch resilient even if that guard is ever
/// bypassed (e.g. future refactor).
/// Known-good fallback location for [initialCenter]: validated GPS
/// location, then validated persisted position, then a hardcoded default.
LatLng get _safeInitialCenter {
final gpsLocation = _gpsController.currentLocation;
if (gpsLocation != null &&
isValidCoordinate(gpsLocation.latitude) &&
isValidCoordinate(gpsLocation.longitude)) {
isValidLatitude(gpsLocation.latitude) &&
isValidLongitude(gpsLocation.longitude)) {
return gpsLocation;
}
final persisted = _positionManager.initialLocation;
if (persisted != null &&
isValidCoordinate(persisted.latitude) &&
isValidCoordinate(persisted.longitude)) {
isValidLatitude(persisted.latitude) &&
isValidLongitude(persisted.longitude)) {
return persisted;
}
return LatLng(37.7749, -122.4194);
@@ -311,6 +306,7 @@ class MapViewState extends State<MapView> {
@override
void didUpdateWidget(covariant MapView oldWidget) {
super.didUpdateWidget(oldWidget);
@@ -456,23 +452,15 @@ class MapViewState extends State<MapView> {
maxZoom: (appState.selectedTileType?.maxZoom ?? 18).toDouble(),
interactionOptions: _interactionManager.getInteractionOptions(editSession),
onPositionChanged: (pos, gesture) {
// Self-heal if the camera has been pushed into a degenerate
// state (non-finite or wildly out-of-range center/zoom). This
// has been observed to occur silently during ordinary panning
// (no crash, no console output) — likely an edge case in
// flutter_map's internal gesture-to-camera math — leaving the
// map showing a blank grey view with no tiles or markers,
// since every subsequent gesture computes its new position as
// a delta from the already-broken center. Snap back to the
// last known-good position the moment this is detected rather
// than requiring the user to discover the GPS button "fixes"
// it (which works only because it supplies a fresh valid
// destination).
if (!isValidCoordinate(pos.center.latitude) ||
!isValidCoordinate(pos.center.longitude) ||
// Self-heal if the camera center/zoom ever becomes invalid
// (observed to happen silently during ordinary panning,
// leaving a blank grey map with no tiles/markers). Snap back
// to the last known-good position immediately.
if (!isValidLatitude(pos.center.latitude) ||
!isValidLongitude(pos.center.longitude) ||
!isValidZoom(pos.zoom)) {
debugPrint(
'[MapView] Detected invalid camera position '
'(lat=${pos.center.latitude}, lng=${pos.center.longitude}, zoom=${pos.zoom}) '
'- snapping back to last known-good position',