app launches again, sorta working, map broken, settings page has configurable max cams.

This commit is contained in:
stopflock
2025-08-10 13:15:48 -05:00
parent 641344271f
commit 96f3a9f108
10 changed files with 152 additions and 20 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 B

View File

@@ -50,6 +50,19 @@ class AppState extends ChangeNotifier {
final List<CameraProfile> _profiles = [];
final Set<CameraProfile> _enabled = {};
static const String _enabledPrefsKey = 'enabled_profiles';
static const String _maxCamerasPrefsKey = 'max_cameras';
// Maximum number of cameras fetched/drawn
int _maxCameras = 250;
int get maxCameras => _maxCameras;
set maxCameras(int n) {
if (n < 10) n = 10; // minimum
_maxCameras = n;
SharedPreferences.getInstance().then((prefs) {
prefs.setInt(_maxCamerasPrefsKey, n);
});
notifyListeners();
}
// Upload mode: production, sandbox, or simulate (in-memory, no uploads)
UploadMode _uploadMode = UploadMode.production;
@@ -130,6 +143,10 @@ class AppState extends ChangeNotifier {
await prefs.remove(_legacyTestModePrefsKey);
await prefs.setInt(_uploadModePrefsKey, _uploadMode.index);
}
// Max cameras
if (prefs.containsKey(_maxCamerasPrefsKey)) {
_maxCameras = prefs.getInt(_maxCamerasPrefsKey) ?? 250;
}
// Offline mode loading
if (prefs.containsKey(_offlineModePrefsKey)) {
_offlineMode = prefs.getBool(_offlineModePrefsKey) ?? false;

View File

@@ -6,6 +6,7 @@ import 'settings_screen_sections/queue_section.dart';
import 'settings_screen_sections/offline_areas_section.dart';
import 'settings_screen_sections/offline_mode_section.dart';
import 'settings_screen_sections/about_section.dart';
import 'settings_screen_sections/max_cameras_section.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@@ -17,14 +18,16 @@ class SettingsScreen extends StatelessWidget {
body: ListView(
padding: const EdgeInsets.all(16),
children: const [
AuthSection(),
Divider(),
UploadModeSection(),
Divider(),
AuthSection(),
Divider(),
QueueSection(),
Divider(),
ProfileListSection(),
Divider(),
MaxCamerasSection(),
Divider(),
OfflineModeSection(),
Divider(),
OfflineAreasSection(),

View File

@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../app_state.dart';
class MaxCamerasSection extends StatefulWidget {
const MaxCamerasSection({super.key});
@override
State<MaxCamerasSection> createState() => _MaxCamerasSectionState();
}
class _MaxCamerasSectionState extends State<MaxCamerasSection> {
late TextEditingController _controller;
@override
void initState() {
super.initState();
final maxCameras = context.read<AppState>().maxCameras;
_controller = TextEditingController(text: maxCameras.toString());
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
final current = appState.maxCameras;
final showWarning = current > 250;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: const Icon(Icons.filter_alt),
title: const Text('Max cameras fetched/drawn'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Set an upper limit for the number of cameras on the map (default: 250).'),
if (showWarning)
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: const [
Icon(Icons.warning, color: Colors.orange, size: 18),
SizedBox(width: 6),
Expanded(child: Text(
'You probably don\'t want to do that unless you are absolutely sure you have a good reason for it.',
style: TextStyle(color: Colors.orange),
)),
],
),
),
],
),
trailing: SizedBox(
width: 80,
child: TextFormField(
controller: _controller,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8),
border: OutlineInputBorder(),
),
onFieldSubmitted: (value) {
final n = int.tryParse(value) ?? 10;
appState.maxCameras = n;
_controller.text = appState.maxCameras.toString();
},
),
),
),
],
);
}
}

View File

@@ -47,7 +47,12 @@ class MapDataProvider {
throw UnimplementedError('Local camera loading not yet implemented.');
} else {
// Use Overpass remote fetch, from submodule:
return camerasFromOverpass(bounds: bounds, profiles: profiles, uploadMode: uploadMode);
return camerasFromOverpass(
bounds: bounds,
profiles: profiles,
uploadMode: uploadMode,
maxCameras: AppState.instance.maxCameras,
);
}
}
/// Fetch tile image bytes from OSM or local (future). Only fetches, does not save!

View File

@@ -12,6 +12,7 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
required LatLngBounds bounds,
required List<CameraProfile> profiles,
UploadMode uploadMode = UploadMode.production,
int? maxCameras,
}) async {
if (profiles.isEmpty) return [];
@@ -24,12 +25,13 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
const String prodEndpoint = 'https://overpass-api.de/api/interpreter';
final limit = maxCameras ?? AppState.instance.maxCameras;
final query = '''
[out:json][timeout:25];
(
$nodeClauses
);
out body 250;
out body $limit;
''';
try {

View File

@@ -158,13 +158,33 @@ class _MapViewState extends State<MapView> {
} catch (_) {
return; // controller not ready yet
}
final cams = await _mapDataProvider.getCameras(
bounds: bounds,
profiles: appState.enabledProfiles,
uploadMode: appState.uploadMode,
// MapSource.auto (default) will prefer Overpass for now
);
if (mounted) setState(() => _cameras = cams);
// If too zoomed out, do NOT fetch cameras; show info
final zoom = _controller.camera.zoom;
if (zoom < 10) {
if (mounted) setState(() => _cameras = []);
// 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 {
final cams = await _mapDataProvider.getCameras(
bounds: bounds,
profiles: appState.enabledProfiles,
uploadMode: appState.uploadMode,
// MapSource.auto (default) will prefer Overpass for now
);
if (mounted) setState(() => _cameras = cams);
} on OfflineModeException catch (_) {
// Swallow the error in offline mode
if (mounted) setState(() => _cameras = []);
}
}
double _safeZoom() {
@@ -199,14 +219,15 @@ class _MapViewState extends State<MapView> {
// Camera markers first, then GPS dot, so blue dot is always on top
final markers = <Marker>[
..._cameras.map(
(n) => Marker(
..._cameras
.where((n) => n.coord.latitude != 0 || n.coord.longitude != 0)
.where((n) => n.coord.latitude.abs() <= 90 && n.coord.longitude.abs() <= 180)
.map((n) => Marker(
point: n.coord,
width: 24,
height: 24,
child: _CameraMapMarker(node: n, mapController: _controller),
),
),
)),
if (_currentLatLng != null)
Marker(
point: _currentLatLng!,
@@ -221,6 +242,8 @@ class _MapViewState extends State<MapView> {
_buildCone(session.target!, session.directionDegrees, zoom),
..._cameras
.where((n) => n.hasDirection && n.directionDeg != null)
.where((n) => n.coord.latitude != 0 || n.coord.longitude != 0)
.where((n) => n.coord.latitude.abs() <= 90 && n.coord.longitude.abs() <= 180)
.map((n) => _buildCone(n.coord, n.directionDeg!, zoom)),
];
@@ -233,6 +256,7 @@ class _MapViewState extends State<MapView> {
initialZoom: 15,
maxZoom: 19,
onPositionChanged: (pos, gesture) {
setState(() {}); // Instant UI update for zoom, etc.
if (gesture) widget.onUserGesture();
if (session != null) {
appState.updateSession(target: pos.center);
@@ -264,7 +288,7 @@ class _MapViewState extends State<MapView> {
return Image.memory(bytes, gaplessPlayback: true, fit: BoxFit.cover);
}
}
return Image.asset('assets/transparent_1x1.png', gaplessPlayback: true, fit: BoxFit.cover);
return tileWidget;
} catch (e) {
print('tileBuilder error: $e for tileImage: ${tileImage.toString()}');
return tileWidget;

View File

@@ -11,6 +11,7 @@ class TileProviderWithCache extends TileProvider {
static final Map<String, Uint8List> _tileCache = {};
static Map<String, Uint8List> get tileCache => _tileCache;
final VoidCallback? onTileCacheUpdated;
TileProviderWithCache({this.onTileCacheUpdated});
@override
@@ -20,7 +21,7 @@ class TileProviderWithCache extends TileProvider {
return MemoryImage(_tileCache[key]!);
} else {
_fetchAndCacheTile(coords, key);
// Return a transparent PNG until the tile is available.
// Use asset (robust, cross-platform) for non-existing tiles.
return const AssetImage('assets/transparent_1x1.png');
}
}
@@ -39,9 +40,10 @@ class TileProviderWithCache extends TileProvider {
SchedulerBinding.instance.addPostFrameCallback((_) => onTileCacheUpdated!());
}
}
// If bytes were empty, don't cache anything (will re-attempt next time)
} catch (e) {
print('[TileProviderWithCache] Error fetching tile $key: $e');
// Optionally: fall back to a different asset, or record failures
// Do NOT cache a failed/placeholder/empty tile!
}
}
}

View File

@@ -36,9 +36,8 @@ flutter:
assets:
- assets/info.txt
- assets/transparent_1x1.png
- assets/black_1x1.png
- assets/app_icon.png
- assets/transparent_1x1.png
flutter_native_splash:
color: "#202020"