mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-08-21 18:37:53 +02:00
fix loading on startup
This commit is contained in:
+1
-1
@@ -11,9 +11,9 @@ const double kDirectionConeHalfAngle = 20.0; // degrees
|
|||||||
const double kDirectionConeBaseLength = 0.0012; // multiplier
|
const double kDirectionConeBaseLength = 0.0012; // multiplier
|
||||||
|
|
||||||
// Marker/camera interaction
|
// Marker/camera interaction
|
||||||
|
const int kCameraMinZoomLevel = 10; // Minimum zoom to show cameras or warning
|
||||||
const Duration kMarkerTapTimeout = Duration(milliseconds: 250);
|
const Duration kMarkerTapTimeout = Duration(milliseconds: 250);
|
||||||
const Duration kDebounceCameraRefresh = Duration(milliseconds: 500);
|
const Duration kDebounceCameraRefresh = Duration(milliseconds: 500);
|
||||||
const Duration kDebounceTileLayerUpdate = Duration(milliseconds: 50);
|
|
||||||
|
|
||||||
// Tile/OSM fetch retry parameters (for tunable backoff)
|
// Tile/OSM fetch retry parameters (for tunable backoff)
|
||||||
const int kTileFetchMaxAttempts = 3;
|
const int kTileFetchMaxAttempts = 3;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:latlong2/latlong.dart';
|
|||||||
import 'package:flock_map_app/dev_config.dart';
|
import 'package:flock_map_app/dev_config.dart';
|
||||||
import '../app_state.dart';
|
import '../app_state.dart';
|
||||||
import '../widgets/map_view.dart';
|
import '../widgets/map_view.dart';
|
||||||
|
import '../widgets/tile_provider_with_cache.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import '../services/offline_area_service.dart';
|
import '../services/offline_area_service.dart';
|
||||||
import '../widgets/add_camera_sheet.dart';
|
import '../widgets/add_camera_sheet.dart';
|
||||||
@@ -36,54 +37,57 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appState = context.watch<AppState>();
|
final appState = context.watch<AppState>();
|
||||||
|
|
||||||
return Scaffold(
|
return ChangeNotifierProvider<TileProviderWithCache>(
|
||||||
key: _scaffoldKey,
|
create: (_) => TileProviderWithCache(),
|
||||||
appBar: AppBar(
|
child: Scaffold(
|
||||||
title: const Text('Flock Map'),
|
key: _scaffoldKey,
|
||||||
actions: [
|
appBar: AppBar(
|
||||||
IconButton(
|
title: const Text('Flock Map'),
|
||||||
tooltip: _followMe ? 'Disable follow‑me' : 'Enable follow‑me',
|
actions: [
|
||||||
icon: Icon(_followMe ? Icons.gps_fixed : Icons.gps_off),
|
IconButton(
|
||||||
onPressed: () => setState(() => _followMe = !_followMe),
|
tooltip: _followMe ? 'Disable follow‑me' : 'Enable follow‑me',
|
||||||
),
|
icon: Icon(_followMe ? Icons.gps_fixed : Icons.gps_off),
|
||||||
IconButton(
|
onPressed: () => setState(() => _followMe = !_followMe),
|
||||||
icon: const Icon(Icons.settings),
|
),
|
||||||
onPressed: () => Navigator.pushNamed(context, '/settings'),
|
IconButton(
|
||||||
),
|
icon: const Icon(Icons.settings),
|
||||||
],
|
onPressed: () => Navigator.pushNamed(context, '/settings'),
|
||||||
),
|
),
|
||||||
body: MapView(
|
],
|
||||||
controller: _mapController,
|
),
|
||||||
followMe: _followMe,
|
body: MapView(
|
||||||
onUserGesture: () {
|
controller: _mapController,
|
||||||
if (_followMe) setState(() => _followMe = false);
|
followMe: _followMe,
|
||||||
},
|
onUserGesture: () {
|
||||||
),
|
if (_followMe) setState(() => _followMe = false);
|
||||||
floatingActionButton: appState.session == null
|
},
|
||||||
? Column(
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
floatingActionButton: appState.session == null
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
? Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
FloatingActionButton.extended(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
onPressed: _openAddCameraSheet,
|
children: [
|
||||||
icon: const Icon(Icons.add_location_alt),
|
FloatingActionButton.extended(
|
||||||
label: const Text('Tag Camera'),
|
onPressed: _openAddCameraSheet,
|
||||||
heroTag: 'tag_camera_fab',
|
icon: const Icon(Icons.add_location_alt),
|
||||||
),
|
label: const Text('Tag Camera'),
|
||||||
const SizedBox(height: 12),
|
heroTag: 'tag_camera_fab',
|
||||||
FloatingActionButton.extended(
|
|
||||||
onPressed: () => showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (ctx) => DownloadAreaDialog(controller: _mapController),
|
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.download_for_offline),
|
const SizedBox(height: 12),
|
||||||
label: const Text('Download'),
|
FloatingActionButton.extended(
|
||||||
heroTag: 'download_fab',
|
onPressed: () => showDialog(
|
||||||
),
|
context: context,
|
||||||
],
|
builder: (ctx) => DownloadAreaDialog(controller: _mapController),
|
||||||
)
|
),
|
||||||
: null,
|
icon: const Icon(Icons.download_for_offline),
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
label: const Text('Download'),
|
||||||
|
heroTag: 'download_fab',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ class _MapViewState extends State<MapView> {
|
|||||||
late final MapController _controller;
|
late final MapController _controller;
|
||||||
final MapDataProvider _mapDataProvider = MapDataProvider();
|
final MapDataProvider _mapDataProvider = MapDataProvider();
|
||||||
final Debouncer _debounce = Debouncer(kDebounceCameraRefresh);
|
final Debouncer _debounce = Debouncer(kDebounceCameraRefresh);
|
||||||
Debouncer? _debounceTileLayerUpdate;
|
|
||||||
|
|
||||||
StreamSubscription<Position>? _positionSub;
|
StreamSubscription<Position>? _positionSub;
|
||||||
LatLng? _currentLatLng;
|
LatLng? _currentLatLng;
|
||||||
@@ -99,7 +98,7 @@ class _MapViewState extends State<MapView> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_debounceTileLayerUpdate = Debouncer(kDebounceTileLayerUpdate);
|
// _debounceTileLayerUpdate removed
|
||||||
OfflineAreaService();
|
OfflineAreaService();
|
||||||
_controller = widget.controller;
|
_controller = widget.controller;
|
||||||
_initLocation();
|
_initLocation();
|
||||||
@@ -130,13 +129,13 @@ class _MapViewState extends State<MapView> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final zoom = _controller.camera.zoom;
|
final zoom = _controller.camera.zoom;
|
||||||
if (zoom < 10) {
|
if (zoom < kCameraMinZoomLevel) {
|
||||||
// Show a snackbar-style bubble, if desired
|
// Show a snackbar-style bubble, if desired
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('Cameras not drawn below zoom level 10'),
|
content: Text('Cameras not drawn below zoom level $kCameraMinZoomLevel'),
|
||||||
duration: Duration(seconds: 2),
|
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
|
@override
|
||||||
void didUpdateWidget(covariant MapView oldWidget) {
|
void didUpdateWidget(covariant MapView oldWidget) {
|
||||||
super.didUpdateWidget(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() {
|
double _safeZoom() {
|
||||||
try {
|
try {
|
||||||
return _controller.camera.zoom;
|
return _controller.camera.zoom;
|
||||||
@@ -303,15 +269,7 @@ class _MapViewState extends State<MapView> {
|
|||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
tileProvider: TileProviderWithCache(
|
tileProvider: Provider.of<TileProviderWithCache>(context),
|
||||||
onTileCacheUpdated: () {
|
|
||||||
print('[MapView] onTileCacheUpdated fired (tile loaded)');
|
|
||||||
if (_debounceTileLayerUpdate != null) _debounceTileLayerUpdate!(() {
|
|
||||||
print('[MapView] Running debounced setState due to tile cache update');
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
urlTemplate: 'unused-{z}-{x}-{y}',
|
urlTemplate: 'unused-{z}-{x}-{y}',
|
||||||
tileSize: 256,
|
tileSize: 256,
|
||||||
tileBuilder: (ctx, tileWidget, tileImage) {
|
tileBuilder: (ctx, tileWidget, tileImage) {
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../services/map_data_provider.dart';
|
import '../services/map_data_provider.dart';
|
||||||
import '../app_state.dart';
|
import '../app_state.dart';
|
||||||
|
|
||||||
/// Singleton in-memory tile cache and async provider for custom tiles.
|
/// 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 final Map<String, Uint8List> _tileCache = {};
|
||||||
static Map<String, Uint8List> get tileCache => _tileCache;
|
static Map<String, Uint8List> get tileCache => _tileCache;
|
||||||
final VoidCallback? onTileCacheUpdated;
|
|
||||||
|
|
||||||
TileProviderWithCache({this.onTileCacheUpdated});
|
TileProviderWithCache();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ImageProvider getImage(TileCoordinates coords, TileLayer options, {MapSource source = MapSource.auto}) {
|
ImageProvider getImage(TileCoordinates coords, TileLayer options, {MapSource source = MapSource.auto}) {
|
||||||
final key = '${coords.z}/${coords.x}/${coords.y}';
|
final key = '${coords.z}/${coords.x}/${coords.y}';
|
||||||
if (_tileCache.containsKey(key)) {
|
if (_tileCache.containsKey(key)) {
|
||||||
return MemoryImage(_tileCache[key]!);
|
final bytes = _tileCache[key]!;
|
||||||
|
return MemoryImage(bytes);
|
||||||
} else {
|
} else {
|
||||||
_fetchAndCacheTile(coords, key, source: source);
|
_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');
|
return const AssetImage('assets/transparent_1x1.png');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,10 +40,7 @@ class TileProviderWithCache extends TileProvider {
|
|||||||
if (bytes.isNotEmpty) {
|
if (bytes.isNotEmpty) {
|
||||||
_tileCache[key] = Uint8List.fromList(bytes);
|
_tileCache[key] = Uint8List.fromList(bytes);
|
||||||
print('[TileProviderWithCache] Cached tile $key, bytes=${bytes.length}');
|
print('[TileProviderWithCache] Cached tile $key, bytes=${bytes.length}');
|
||||||
if (onTileCacheUpdated != null) {
|
notifyListeners(); // This updates any listening widgets
|
||||||
print('[TileProviderWithCache] Calling onTileCacheUpdated for $key');
|
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) => onTileCacheUpdated!());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// If bytes were empty, don't cache (will re-attempt next time)
|
// If bytes were empty, don't cache (will re-attempt next time)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user