From ff9be3314258cb5fd4697b19d9000fe530bef4e8 Mon Sep 17 00:00:00 2001 From: stopflock Date: Tue, 12 Aug 2025 23:41:43 -0500 Subject: [PATCH] fix loading on startup --- lib/dev_config.dart | 2 +- lib/screens/home_screen.dart | 98 ++++++++++++----------- lib/widgets/map_view.dart | 54 ++----------- lib/widgets/tile_provider_with_cache.dart | 16 ++-- 4 files changed, 64 insertions(+), 106 deletions(-) diff --git a/lib/dev_config.dart b/lib/dev_config.dart index 79a22ea..b12ba62 100644 --- a/lib/dev_config.dart +++ b/lib/dev_config.dart @@ -11,9 +11,9 @@ const double kDirectionConeHalfAngle = 20.0; // degrees const double kDirectionConeBaseLength = 0.0012; // multiplier // Marker/camera interaction +const int kCameraMinZoomLevel = 10; // Minimum zoom to show cameras or warning const Duration kMarkerTapTimeout = Duration(milliseconds: 250); const Duration kDebounceCameraRefresh = Duration(milliseconds: 500); -const Duration kDebounceTileLayerUpdate = Duration(milliseconds: 50); // Tile/OSM fetch retry parameters (for tunable backoff) const int kTileFetchMaxAttempts = 3; diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 38a0615..832656b 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -5,6 +5,7 @@ import 'package:latlong2/latlong.dart'; import 'package:flock_map_app/dev_config.dart'; import '../app_state.dart'; import '../widgets/map_view.dart'; +import '../widgets/tile_provider_with_cache.dart'; import 'package:flutter_map/flutter_map.dart'; import '../services/offline_area_service.dart'; import '../widgets/add_camera_sheet.dart'; @@ -36,54 +37,57 @@ class _HomeScreenState extends State { Widget build(BuildContext context) { final appState = context.watch(); - return Scaffold( - key: _scaffoldKey, - appBar: AppBar( - title: const Text('Flock Map'), - actions: [ - IconButton( - tooltip: _followMe ? 'Disable follow‑me' : 'Enable follow‑me', - icon: Icon(_followMe ? Icons.gps_fixed : Icons.gps_off), - onPressed: () => setState(() => _followMe = !_followMe), - ), - IconButton( - icon: const Icon(Icons.settings), - onPressed: () => Navigator.pushNamed(context, '/settings'), - ), - ], - ), - body: MapView( - controller: _mapController, - followMe: _followMe, - onUserGesture: () { - if (_followMe) setState(() => _followMe = false); - }, - ), - floatingActionButton: appState.session == null - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - FloatingActionButton.extended( - onPressed: _openAddCameraSheet, - icon: const Icon(Icons.add_location_alt), - label: const Text('Tag Camera'), - heroTag: 'tag_camera_fab', - ), - const SizedBox(height: 12), - FloatingActionButton.extended( - onPressed: () => showDialog( - context: context, - builder: (ctx) => DownloadAreaDialog(controller: _mapController), + return ChangeNotifierProvider( + create: (_) => TileProviderWithCache(), + child: Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text('Flock Map'), + actions: [ + IconButton( + tooltip: _followMe ? 'Disable follow‑me' : 'Enable follow‑me', + icon: Icon(_followMe ? Icons.gps_fixed : Icons.gps_off), + onPressed: () => setState(() => _followMe = !_followMe), + ), + IconButton( + icon: const Icon(Icons.settings), + onPressed: () => Navigator.pushNamed(context, '/settings'), + ), + ], + ), + body: MapView( + controller: _mapController, + followMe: _followMe, + onUserGesture: () { + if (_followMe) setState(() => _followMe = false); + }, + ), + floatingActionButton: appState.session == null + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + FloatingActionButton.extended( + onPressed: _openAddCameraSheet, + icon: const Icon(Icons.add_location_alt), + label: const Text('Tag Camera'), + heroTag: 'tag_camera_fab', ), - icon: const Icon(Icons.download_for_offline), - label: const Text('Download'), - heroTag: 'download_fab', - ), - ], - ) - : null, - floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + const SizedBox(height: 12), + FloatingActionButton.extended( + onPressed: () => showDialog( + context: context, + builder: (ctx) => DownloadAreaDialog(controller: _mapController), + ), + icon: const Icon(Icons.download_for_offline), + label: const Text('Download'), + heroTag: 'download_fab', + ), + ], + ) + : null, + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + ), ); } } diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 1d6dbd5..98f2343 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -89,7 +89,6 @@ class _MapViewState extends State { late final MapController _controller; final MapDataProvider _mapDataProvider = MapDataProvider(); final Debouncer _debounce = Debouncer(kDebounceCameraRefresh); - Debouncer? _debounceTileLayerUpdate; StreamSubscription? _positionSub; LatLng? _currentLatLng; @@ -99,7 +98,7 @@ class _MapViewState extends State { @override void initState() { super.initState(); - _debounceTileLayerUpdate = Debouncer(kDebounceTileLayerUpdate); + // _debounceTileLayerUpdate removed OfflineAreaService(); _controller = widget.controller; _initLocation(); @@ -130,13 +129,13 @@ class _MapViewState extends State { return; } final zoom = _controller.camera.zoom; - if (zoom < 10) { + if (zoom < kCameraMinZoomLevel) { // Show a snackbar-style bubble, if desired if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Cameras not drawn below zoom level 10'), - duration: Duration(seconds: 2), + SnackBar( + content: Text('Cameras not drawn below zoom level $kCameraMinZoomLevel'), + duration: const Duration(seconds: 2), ), ); } @@ -149,8 +148,6 @@ class _MapViewState extends State { ); } -// Duplicate dispose in _MapViewState removed. Only one dispose() remains with all proper teardown. - @override void didUpdateWidget(covariant MapView oldWidget) { super.didUpdateWidget(oldWidget); @@ -182,37 +179,6 @@ class _MapViewState extends State { }); } - Future _refreshCameras() async { - final appState = context.read(); - LatLngBounds? bounds; - try { - bounds = _controller.camera.visibleBounds; - } catch (_) { - return; // controller not ready yet - } - // If too zoomed out, do NOT fetch cameras; show info - final zoom = _controller.camera.zoom; - if (zoom < 10) { - // No-op: camera overlays handled via provider and cache, no local _cameras assignment needed. - // Show a snackbar-style bubble, if desired - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Cameras not drawn below zoom level 10'), - duration: Duration(seconds: 2), - ), - ); - } - return; - } - try { - // (Legacy _cameras assignment removed—now handled via provider and cache updates) - } on OfflineModeException catch (_) { - // Swallow the error in offline mode - // (Legacy _cameras assignment removed—handled via provider) - } - } - double _safeZoom() { try { return _controller.camera.zoom; @@ -303,15 +269,7 @@ class _MapViewState extends State { ), children: [ TileLayer( - tileProvider: TileProviderWithCache( - onTileCacheUpdated: () { - print('[MapView] onTileCacheUpdated fired (tile loaded)'); - if (_debounceTileLayerUpdate != null) _debounceTileLayerUpdate!(() { - print('[MapView] Running debounced setState due to tile cache update'); - if (mounted) setState(() {}); - }); - }, - ), + tileProvider: Provider.of(context), urlTemplate: 'unused-{z}-{x}-{y}', tileSize: 256, tileBuilder: (ctx, tileWidget, tileImage) { diff --git a/lib/widgets/tile_provider_with_cache.dart b/lib/widgets/tile_provider_with_cache.dart index d354b2a..7fd04a3 100644 --- a/lib/widgets/tile_provider_with_cache.dart +++ b/lib/widgets/tile_provider_with_cache.dart @@ -1,27 +1,26 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import '../services/map_data_provider.dart'; import '../app_state.dart'; /// Singleton in-memory tile cache and async provider for custom tiles. -class TileProviderWithCache extends TileProvider { +class TileProviderWithCache extends TileProvider with ChangeNotifier { static final Map _tileCache = {}; static Map get tileCache => _tileCache; - final VoidCallback? onTileCacheUpdated; - TileProviderWithCache({this.onTileCacheUpdated}); + TileProviderWithCache(); @override ImageProvider getImage(TileCoordinates coords, TileLayer options, {MapSource source = MapSource.auto}) { final key = '${coords.z}/${coords.x}/${coords.y}'; if (_tileCache.containsKey(key)) { - return MemoryImage(_tileCache[key]!); + final bytes = _tileCache[key]!; + return MemoryImage(bytes); } else { _fetchAndCacheTile(coords, key, source: source); - // Always return a placeholder until the real tile is cached, regardless of source/offline/online. + // Always return a placeholder until the real tile is cached return const AssetImage('assets/transparent_1x1.png'); } } @@ -41,10 +40,7 @@ class TileProviderWithCache extends TileProvider { if (bytes.isNotEmpty) { _tileCache[key] = Uint8List.fromList(bytes); print('[TileProviderWithCache] Cached tile $key, bytes=${bytes.length}'); - if (onTileCacheUpdated != null) { - print('[TileProviderWithCache] Calling onTileCacheUpdated for $key'); - SchedulerBinding.instance.addPostFrameCallback((_) => onTileCacheUpdated!()); - } + notifyListeners(); // This updates any listening widgets } // If bytes were empty, don't cache (will re-attempt next time) } catch (e) {