fix loading on startup

This commit is contained in:
stopflock
2025-08-12 23:41:43 -05:00
parent a7185d12ae
commit ff9be33142
4 changed files with 64 additions and 106 deletions

View File

@@ -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;

View File

@@ -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<HomeScreen> {
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Flock Map'),
actions: [
IconButton(
tooltip: _followMe ? 'Disable followme' : 'Enable followme',
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<TileProviderWithCache>(
create: (_) => TileProviderWithCache(),
child: Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Flock Map'),
actions: [
IconButton(
tooltip: _followMe ? 'Disable followme' : 'Enable followme',
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,
),
);
}
}

View File

@@ -89,7 +89,6 @@ class _MapViewState extends State<MapView> {
late final MapController _controller;
final MapDataProvider _mapDataProvider = MapDataProvider();
final Debouncer _debounce = Debouncer(kDebounceCameraRefresh);
Debouncer? _debounceTileLayerUpdate;
StreamSubscription<Position>? _positionSub;
LatLng? _currentLatLng;
@@ -99,7 +98,7 @@ class _MapViewState extends State<MapView> {
@override
void initState() {
super.initState();
_debounceTileLayerUpdate = Debouncer(kDebounceTileLayerUpdate);
// _debounceTileLayerUpdate removed
OfflineAreaService();
_controller = widget.controller;
_initLocation();
@@ -130,13 +129,13 @@ class _MapViewState extends State<MapView> {
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<MapView> {
);
}
// 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<MapView> {
});
}
Future<void> _refreshCameras() async {
final appState = context.read<AppState>();
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<MapView> {
),
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<TileProviderWithCache>(context),
urlTemplate: 'unused-{z}-{x}-{y}',
tileSize: 256,
tileBuilder: (ctx, tileWidget, tileImage) {

View File

@@ -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<String, Uint8List> _tileCache = {};
static Map<String, Uint8List> 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) {