nodes, not cameras

This commit is contained in:
stopflock
2025-08-29 16:44:34 -05:00
parent b5c210d009
commit 3ddebd2664
15 changed files with 73 additions and 73 deletions

View File

@@ -17,7 +17,7 @@ import 'state/upload_queue_state.dart';
// Re-export types
export 'state/settings_state.dart' show UploadMode, FollowMeMode;
export 'state/session_state.dart' show AddCameraSession, EditCameraSession;
export 'state/session_state.dart' show AddNodeSession, EditNodeSession;
// ------------------ AppState ------------------
class AppState extends ChangeNotifier {
@@ -69,8 +69,8 @@ class AppState extends ChangeNotifier {
List<OperatorProfile> get operatorProfiles => _operatorProfileState.profiles;
// Session state
AddCameraSession? get session => _sessionState.session;
EditCameraSession? get editSession => _sessionState.editSession;
AddNodeSession? get session => _sessionState.session;
EditNodeSession? get editSession => _sessionState.editSession;
// Settings state
bool get offlineMode => _settingsState.offlineMode;

View File

@@ -28,8 +28,8 @@ const double kAddPinYOffset = 0.0;
const String kClientName = 'FlockMap';
const String kClientVersion = '0.9.7';
// Marker/camera interaction
const int kCameraMinZoomLevel = 10; // Minimum zoom to show cameras or warning
// Marker/node interaction
const int kCameraMinZoomLevel = 10; // Minimum zoom to show nodes or warning
const Duration kMarkerTapTimeout = Duration(milliseconds: 250);
const Duration kDebounceCameraRefresh = Duration(milliseconds: 500);
@@ -62,8 +62,8 @@ const int kAbsoluteMaxZoom = 19;
const double kCameraIconDiameter = 20.0;
const double kCameraRingThickness = 4.0;
const double kCameraDotOpacity = 0.4; // Opacity for the grey dot interior
const Color kCameraRingColorReal = Color(0xC43F55F3); // Real cameras from OSM - blue
const Color kCameraRingColorMock = Color(0xC4FFFFFF); // Add camera mock point - white
const Color kCameraRingColorPending = Color(0xC49C27B0); // Submitted/pending cameras - purple
const Color kCameraRingColorEditing = Color(0xC4FF9800); // Camera being edited - orange
const Color kCameraRingColorPendingEdit = Color(0xC4757575); // Original camera with pending edit - grey
const Color kCameraRingColorReal = Color(0xC43F55F3); // Real nodes from OSM - blue
const Color kCameraRingColorMock = Color(0xC4FFFFFF); // Add node mock point - white
const Color kCameraRingColorPending = Color(0xC49C27B0); // Submitted/pending nodes - purple
const Color kCameraRingColorEditing = Color(0xC4FF9800); // Node being edited - orange
const Color kCameraRingColorPendingEdit = Color(0xC4757575); // Original node with pending edit - grey

View File

@@ -7,8 +7,8 @@ import '../app_state.dart';
import '../dev_config.dart';
import '../widgets/map_view.dart';
import '../widgets/add_camera_sheet.dart';
import '../widgets/edit_camera_sheet.dart';
import '../widgets/add_node_sheet.dart';
import '../widgets/edit_node_sheet.dart';
import '../widgets/camera_provider_with_cache.dart';
import '../widgets/download_area_dialog.dart';
@@ -70,7 +70,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
}
}
void _openAddCameraSheet() {
void _openAddNodeSheet() {
final appState = context.read<AppState>();
// Disable follow-me when adding a camera so the map doesn't jump around
appState.setFollowMeMode(FollowMeMode.off);
@@ -79,11 +79,11 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
final session = appState.session!; // guaranteed nonnull now
_scaffoldKey.currentState!.showBottomSheet(
(ctx) => AddCameraSheet(session: session),
(ctx) => AddNodeSheet(session: session),
);
}
void _openEditCameraSheet() {
void _openEditNodeSheet() {
final appState = context.read<AppState>();
// Disable follow-me when editing a camera so the map doesn't jump around
appState.setFollowMeMode(FollowMeMode.off);
@@ -91,7 +91,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
final session = appState.editSession!; // should be non-null when this is called
_scaffoldKey.currentState!.showBottomSheet(
(ctx) => EditCameraSheet(session: session),
(ctx) => EditNodeSheet(session: session),
);
}
@@ -102,7 +102,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
// Auto-open edit sheet when edit session starts
if (appState.editSession != null && !_editSheetShown) {
_editSheetShown = true;
WidgetsBinding.instance.addPostFrameCallback((_) => _openEditCameraSheet());
WidgetsBinding.instance.addPostFrameCallback((_) => _openEditNodeSheet());
} else if (appState.editSession == null) {
_editSheetShown = false;
}
@@ -169,8 +169,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
Expanded(
child: ElevatedButton.icon(
icon: Icon(Icons.add_location_alt),
label: Text('Tag Camera'),
onPressed: _openAddCameraSheet,
label: Text('Tag Node'),
onPressed: _openAddNodeSheet,
style: ElevatedButton.styleFrom(
minimumSize: Size(0, 48),
textStyle: TextStyle(fontSize: 16),

View File

@@ -7,7 +7,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';
import 'settings_screen_sections/max_nodes_section.dart';
import 'settings_screen_sections/tile_provider_section.dart';
class SettingsScreen extends StatelessWidget {
@@ -30,7 +30,7 @@ class SettingsScreen extends StatelessWidget {
Divider(),
OperatorProfileListSection(),
Divider(),
MaxCamerasSection(),
MaxNodesSection(),
Divider(),
TileProviderSection(),
Divider(),

View File

@@ -2,21 +2,21 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../app_state.dart';
class MaxCamerasSection extends StatefulWidget {
const MaxCamerasSection({super.key});
class MaxNodesSection extends StatefulWidget {
const MaxNodesSection({super.key});
@override
State<MaxCamerasSection> createState() => _MaxCamerasSectionState();
State<MaxNodesSection> createState() => _MaxNodesSectionState();
}
class _MaxCamerasSectionState extends State<MaxCamerasSection> {
class _MaxNodesSectionState extends State<MaxNodesSection> {
late TextEditingController _controller;
@override
void initState() {
super.initState();
final maxCameras = context.read<AppState>().maxCameras;
_controller = TextEditingController(text: maxCameras.toString());
final maxNodes = context.read<AppState>().maxCameras;
_controller = TextEditingController(text: maxNodes.toString());
}
@override
@@ -35,11 +35,11 @@ class _MaxCamerasSectionState extends State<MaxCamerasSection> {
children: [
ListTile(
leading: const Icon(Icons.filter_alt),
title: const Text('Max cameras fetched/drawn'),
title: const Text('Max nodes fetched/drawn'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Set an upper limit for the number of cameras on the map (default: 250).'),
const Text('Set an upper limit for the number of nodes on the map (default: 250).'),
if (showWarning)
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),

View File

@@ -40,7 +40,7 @@ class OperatorProfileListSection extends StatelessWidget {
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'No operator profiles defined. Create one to apply operator tags to camera submissions.',
'No operator profiles defined. Create one to apply operator tags to node submissions.',
style: TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
),

View File

@@ -31,7 +31,7 @@ class MapDataProvider {
AppState.instance.setOfflineMode(enabled);
}
/// Fetch cameras from OSM/Overpass or local storage.
/// Fetch surveillance nodes from OSM/Overpass or local storage.
/// Remote is default. If source is MapSource.auto, remote is tried first unless offline.
Future<List<OsmCameraNode>> getCameras({
required LatLngBounds bounds,
@@ -44,7 +44,7 @@ class MapDataProvider {
// Explicit remote request: error if offline, else always remote
if (source == MapSource.remote) {
if (offline) {
throw OfflineModeException("Cannot fetch remote cameras in offline mode.");
throw OfflineModeException("Cannot fetch remote nodes in offline mode.");
}
return camerasFromOverpass(
bounds: bounds,
@@ -79,7 +79,7 @@ class MapDataProvider {
pageSize: AppState.instance.maxCameras,
);
} catch (e) {
debugPrint('[MapDataProvider] Remote camera fetch failed, error: $e. Falling back to local.');
debugPrint('[MapDataProvider] Remote node fetch failed, error: $e. Falling back to local.');
return fetchLocalCameras(
bounds: bounds,
profiles: profiles,
@@ -89,7 +89,7 @@ class MapDataProvider {
}
}
/// Bulk/paged camera fetch for offline downloads (handling paging, dedup, and Overpass retries)
/// Bulk/paged node fetch for offline downloads (handling paging, dedup, and Overpass retries)
/// Only use for offline area download, not for map browsing! Ignores maxCameras config.
Future<List<OsmCameraNode>> getAllCamerasForDownload({
required LatLngBounds bounds,
@@ -100,7 +100,7 @@ class MapDataProvider {
}) async {
final offline = AppState.instance.offlineMode;
if (offline) {
throw OfflineModeException("Cannot fetch remote cameras for offline area download in offline mode.");
throw OfflineModeException("Cannot fetch remote nodes for offline area download in offline mode.");
}
return camerasFromOverpass(
bounds: bounds,

View File

@@ -14,7 +14,7 @@ class Uploader {
Future<bool> upload(PendingUpload p) async {
try {
print('Uploader: Starting upload for camera at ${p.coord.latitude}, ${p.coord.longitude}');
print('Uploader: Starting upload for node at ${p.coord.latitude}, ${p.coord.longitude}');
// 1. open changeset
final action = p.isEdit ? 'Update' : 'Add';
@@ -22,7 +22,7 @@ class Uploader {
<osm>
<changeset>
<tag k="created_by" v="$kClientName $kClientVersion"/>
<tag k="comment" v="$action ${p.profile.name} surveillance camera"/>
<tag k="comment" v="$action ${p.profile.name} surveillance node"/>
</changeset>
</osm>''';
print('Uploader: Creating changeset...');

View File

@@ -5,25 +5,25 @@ import '../models/camera_profile.dart';
import '../models/operator_profile.dart';
import '../models/osm_camera_node.dart';
// ------------------ AddCameraSession ------------------
class AddCameraSession {
AddCameraSession({required this.profile, this.directionDegrees = 0});
// ------------------ AddNodeSession ------------------
class AddNodeSession {
AddNodeSession({required this.profile, this.directionDegrees = 0});
CameraProfile profile;
OperatorProfile? operatorProfile;
double directionDegrees;
LatLng? target;
}
// ------------------ EditCameraSession ------------------
class EditCameraSession {
EditCameraSession({
// ------------------ EditNodeSession ------------------
class EditNodeSession {
EditNodeSession({
required this.originalNode,
required this.profile,
required this.directionDegrees,
required this.target,
});
final OsmCameraNode originalNode; // The original camera being edited
final OsmCameraNode originalNode; // The original node being edited
CameraProfile profile;
OperatorProfile? operatorProfile;
double directionDegrees;
@@ -31,19 +31,19 @@ class EditCameraSession {
}
class SessionState extends ChangeNotifier {
AddCameraSession? _session;
EditCameraSession? _editSession;
AddNodeSession? _session;
EditNodeSession? _editSession;
// Getters
AddCameraSession? get session => _session;
EditCameraSession? get editSession => _editSession;
AddNodeSession? get session => _session;
EditNodeSession? get editSession => _editSession;
void startAddSession(List<CameraProfile> enabledProfiles) {
final submittableProfiles = enabledProfiles.where((p) => p.isSubmittable).toList();
final defaultProfile = submittableProfiles.isNotEmpty
? submittableProfiles.first
: enabledProfiles.first; // Fallback to any enabled profile
_session = AddCameraSession(profile: defaultProfile);
_session = AddNodeSession(profile: defaultProfile);
_editSession = null; // Clear any edit session
notifyListeners();
}
@@ -64,7 +64,7 @@ class SessionState extends ChangeNotifier {
}
}
_editSession = EditCameraSession(
_editSession = EditNodeSession(
originalNode: node,
profile: matchingProfile,
directionDegrees: node.directionDeg ?? 0,
@@ -150,7 +150,7 @@ class SessionState extends ChangeNotifier {
notifyListeners();
}
AddCameraSession? commitSession() {
AddNodeSession? commitSession() {
if (_session?.target == null) return null;
final session = _session!;
@@ -159,7 +159,7 @@ class SessionState extends ChangeNotifier {
return session;
}
EditCameraSession? commitEditSession() {
EditNodeSession? commitEditSession() {
if (_editSession == null) return null;
final session = _editSession!;

View File

@@ -25,7 +25,7 @@ class UploadQueueState extends ChangeNotifier {
}
// Add a completed session to the upload queue
void addFromSession(AddCameraSession session, {required UploadMode uploadMode}) {
void addFromSession(AddNodeSession session, {required UploadMode uploadMode}) {
final upload = PendingUpload(
coord: session.target!,
direction: session.directionDegrees,
@@ -58,7 +58,7 @@ class UploadQueueState extends ChangeNotifier {
}
// Add a completed edit session to the upload queue
void addFromEditSession(EditCameraSession session, {required UploadMode uploadMode}) {
void addFromEditSession(EditNodeSession session, {required UploadMode uploadMode}) {
final upload = PendingUpload(
coord: session.target,
direction: session.directionDegrees,

View File

@@ -6,10 +6,10 @@ import '../models/camera_profile.dart';
import '../models/operator_profile.dart';
import 'refine_tags_sheet.dart';
class AddCameraSheet extends StatelessWidget {
const AddCameraSheet({super.key, required this.session});
class AddNodeSheet extends StatelessWidget {
const AddNodeSheet({super.key, required this.session});
final AddCameraSession session;
final AddNodeSession session;
@override
Widget build(BuildContext context) {
@@ -19,7 +19,7 @@ class AddCameraSheet extends StatelessWidget {
appState.commitSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Camera queued for upload')),
const SnackBar(content: Text('Node queued for upload')),
);
}
@@ -111,7 +111,7 @@ class AddCameraSheet extends StatelessWidget {
SizedBox(width: 6),
Expanded(
child: Text(
'Enable a submittable profile in Settings to submit new cameras.',
'Enable a submittable profile in Settings to submit new nodes.',
style: TextStyle(color: Colors.red, fontSize: 13),
),
),
@@ -127,7 +127,7 @@ class AddCameraSheet extends StatelessWidget {
SizedBox(width: 6),
Expanded(
child: Text(
'This profile is for map viewing only. Please select a submittable profile to submit new cameras.',
'This profile is for map viewing only. Please select a submittable profile to submit new nodes.',
style: TextStyle(color: Colors.orange, fontSize: 13),
),
),

View File

@@ -7,10 +7,10 @@ import '../models/operator_profile.dart';
import '../state/settings_state.dart';
import 'refine_tags_sheet.dart';
class EditCameraSheet extends StatelessWidget {
const EditCameraSheet({super.key, required this.session});
class EditNodeSheet extends StatelessWidget {
const EditNodeSheet({super.key, required this.session});
final EditCameraSession session;
final EditNodeSession session;
@override
Widget build(BuildContext context) {
@@ -20,7 +20,7 @@ class EditCameraSheet extends StatelessWidget {
appState.commitEditSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Camera edit queued for upload')),
const SnackBar(content: Text('Node edit queued for upload')),
);
}
@@ -65,7 +65,7 @@ class EditCameraSheet extends StatelessWidget {
),
const SizedBox(height: 8),
Text(
'Edit Camera #${session.originalNode.id}',
'Edit Node #${session.originalNode.id}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
@@ -118,7 +118,7 @@ class EditCameraSheet extends StatelessWidget {
SizedBox(width: 6),
Expanded(
child: Text(
'Cannot submit edits on production nodes to sandbox. Switch to Production mode in Settings to edit cameras.',
'Cannot submit edits on production nodes to sandbox. Switch to Production mode in Settings to edit nodes.',
style: TextStyle(color: Colors.blue, fontSize: 13),
),
),
@@ -134,7 +134,7 @@ class EditCameraSheet extends StatelessWidget {
SizedBox(width: 6),
Expanded(
child: Text(
'Enable a submittable profile in Settings to edit cameras.',
'Enable a submittable profile in Settings to edit nodes.',
style: TextStyle(color: Colors.red, fontSize: 13),
),
),
@@ -150,7 +150,7 @@ class EditCameraSheet extends StatelessWidget {
SizedBox(width: 6),
Expanded(
child: Text(
'This profile is for map viewing only. Please select a submittable profile to edit cameras.',
'This profile is for map viewing only. Please select a submittable profile to edit nodes.',
style: TextStyle(color: Colors.orange, fontSize: 13),
),
),

View File

@@ -12,8 +12,8 @@ class DirectionConesBuilder {
static List<Polygon> buildDirectionCones({
required List<OsmCameraNode> cameras,
required double zoom,
AddCameraSession? session,
EditCameraSession? editSession,
AddNodeSession? session,
EditNodeSession? editSession,
}) {
final overlays = <Polygon>[];

View File

@@ -10,8 +10,8 @@ import 'layer_selector_button.dart';
class MapOverlays extends StatelessWidget {
final MapController mapController;
final UploadMode uploadMode;
final AddCameraSession? session;
final EditCameraSession? editSession;
final AddNodeSession? session;
final EditNodeSession? editSession;
final String? attribution; // Attribution for current tile provider
const MapOverlays({

View File

@@ -66,7 +66,7 @@ class _RefineTagsSheetState extends State<RefineTagsSheet> {
),
SizedBox(height: 4),
Text(
'Create operator profiles in Settings to apply additional tags to your camera submissions.',
'Create operator profiles in Settings to apply additional tags to your node submissions.',
style: TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
),