From dca6cb717921af3ff86c5bf95ac626b0693e8633 Mon Sep 17 00:00:00 2001 From: stopflock Date: Wed, 6 Aug 2025 12:24:32 -0500 Subject: [PATCH] bunch of stuff, good luck. still need to fix user-by-mode thing --- README.md | 30 +++++++++++++--- lib/keys.dart | 6 ++++ lib/keys.dart.example | 3 ++ lib/models/camera_profile.dart | 4 ++- lib/screens/profile_editor.dart | 4 +++ lib/screens/settings_screen.dart | 15 +++++++- lib/services/auth_service.dart | 9 +++-- lib/services/overpass_service.dart | 55 +++++++++++++++++++----------- lib/widgets/add_camera_sheet.dart | 37 +++++++++++++++++++- lib/widgets/map_view.dart | 42 ++++++++++++++++++++++- 10 files changed, 174 insertions(+), 31 deletions(-) create mode 100644 lib/keys.dart create mode 100644 lib/keys.dart.example diff --git a/README.md b/README.md index e5bdd15..8bdcb9c 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,32 @@ A minimal Flutter scaffold for mapping and tagging Flock‑style ALPR cameras in OpenStreetMap. -# NOTE: -Forks should register for their own oauth2 client id from OSM: https://www.openstreetmap.org/oauth2/applications -These are hardcoded in lib/services/auth_service.dart for each app. -If you discover a bug that causes bad behavior w/rt OSM API, you might want to register a new one for the patched version to distinguish them. You can also then delete the old version from OSM to prevent new people from using the old version. +# OAuth Setup + +Before you can upload to OpenStreetMap (production **or sandbox**), you must register your own OAuth2 application on each OSM API you wish to support: +- [Production OSM register page](https://www.openstreetmap.org/oauth2/applications) +- [Sandbox OSM register page](https://master.apis.dev.openstreetmap.org/oauth2/applications) + +Copy your generated client IDs into a new file: + +```dart +// lib/keys.dart +const String kOsmProdClientId = 'YOUR_PROD_CLIENT_ID_HERE'; +const String kOsmSandboxClientId = 'YOUR_SANDBOX_CLIENT_ID_HERE'; +``` + +For open source: use `lib/keys.dart.example` as a template and do **not** commit your real secrets. + +If you discover a bug that causes bad behavior w/rt OSM API, register a new OAuth client to distinguish patched versions and, if needed, delete the old app to prevent misuse. + +# Upload Modes + +In Settings, you can now choose your "Upload Destination": +- **Production**: Live OSM database (visible to all users). +- **Sandbox**: OSM's dedicated test database; safe for development/testing. [More info](https://wiki.openstreetmap.org/wiki/Sandbox). +- **Simulate**: Does not contact any server. Actions are fully offline for testing UI/flows. + +--- ## TODO for Beta/RC Release diff --git a/lib/keys.dart b/lib/keys.dart new file mode 100644 index 0000000..95154d7 --- /dev/null +++ b/lib/keys.dart @@ -0,0 +1,6 @@ +// OpenStreetMap OAuth client IDs for this app. +// +// NEVER commit real secrets to public repos. For open source, use keys.dart.example instead. + +const String kOsmProdClientId = 'Js6Fn3NR3HEGaD0ZIiHBQlV9LrVcHmsOsDmApHtSyuY'; // example - replace with real +const String kOsmSandboxClientId = 'x26twxRKTZwf1a4Ha1a-wkXncBzqnJv8JwtacJope9Q'; // example - replace with real diff --git a/lib/keys.dart.example b/lib/keys.dart.example new file mode 100644 index 0000000..068bf19 --- /dev/null +++ b/lib/keys.dart.example @@ -0,0 +1,3 @@ +// Example OSM OAuth key config +const String kOsmProdClientId = 'YOUR_PROD_CLIENT_ID_HERE'; +const String kOsmSandboxClientId = 'YOUR_SANDBOX_CLIENT_ID_HERE'; diff --git a/lib/models/camera_profile.dart b/lib/models/camera_profile.dart index f71c479..0804856 100644 --- a/lib/models/camera_profile.dart +++ b/lib/models/camera_profile.dart @@ -20,7 +20,9 @@ class CameraProfile { name: 'Generic Flock', tags: const { 'man_made': 'surveillance', - 'surveillance:type': 'ALPR', + 'surveillance': 'public', + 'surveillance:zone': 'traffic', + 'surveillance:type': 'ALPR', // left for backward compatibility — you may want to revisit per OSM best practice 'camera:type': 'fixed', 'manufacturer': 'Flock Safety', 'manufacturer:wikidata': 'Q108485435', diff --git a/lib/screens/profile_editor.dart b/lib/screens/profile_editor.dart index f0012e9..037f992 100644 --- a/lib/screens/profile_editor.dart +++ b/lib/screens/profile_editor.dart @@ -20,9 +20,13 @@ class _ProfileEditorState extends State { static const _defaultTags = [ MapEntry('man_made', 'surveillance'), + MapEntry('surveillance', 'public'), + MapEntry('surveillance:zone', 'traffic'), MapEntry('surveillance:type', 'ALPR'), MapEntry('camera:type', 'fixed'), + MapEntry('camera:mount', ''), MapEntry('manufacturer', ''), + MapEntry('manufacturer:wikidata', ''), ]; @override diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 43ffe20..f62159f 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -178,7 +178,20 @@ class SettingsScreen extends StatelessWidget { case UploadMode.production: return const Text('Upload to the live OSM database (visible to all users)', style: TextStyle(fontSize: 12, color: Colors.black87)); case UploadMode.sandbox: - return const Text('Upload to the OSM Sandbox (safe for testing, data resets regularly)', style: TextStyle(fontSize: 12, color: Colors.orange)); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Uploads go to the OSM Sandbox (safe for testing, resets regularly).', + style: TextStyle(fontSize: 12, color: Colors.orange), + ), + SizedBox(height: 2), + Text( + 'NOTE: Due to OpenStreetMap limitations, cameras submitted to the sandbox will NOT appear on the map in this app.', + style: TextStyle(fontSize: 11, color: Colors.redAccent), + ), + ], + ); case UploadMode.simulate: default: return const Text('Simulate uploads (does not contact OSM servers)', style: TextStyle(fontSize: 12, color: Colors.deepPurple)); diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 67e9bd1..e681c60 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -9,8 +9,10 @@ import 'package:http/http.dart' as http; /// Handles PKCE OAuth login with OpenStreetMap. import '../app_state.dart'; +import '../keys.dart'; + class AuthService { - static const String _clientId = 'Js6Fn3NR3HEGaD0ZIiHBQlV9LrVcHmsOsDmApHtSyuY'; + // Both client IDs from keys.dart static const _redirect = 'flockmap://auth'; late OAuth2Helper _helper; @@ -27,6 +29,7 @@ class AuthService { final authBase = isSandbox ? 'https://master.apis.dev.openstreetmap.org' // sandbox auth : 'https://www.openstreetmap.org'; + final clientId = isSandbox ? kOsmSandboxClientId : kOsmProdClientId; final client = OAuth2Client( authorizeUrl: '$authBase/oauth2/authorize', tokenUrl: '$authBase/oauth2/token', @@ -35,11 +38,11 @@ class AuthService { ); _helper = OAuth2Helper( client, - clientId: _clientId, + clientId: clientId, scopes: ['read_prefs', 'write_api'], enablePKCE: true, ); - print('AuthService: Initialized for $mode with $authBase'); + print('AuthService: Initialized for $mode with $authBase and clientId $clientId'); } Future isLoggedIn() async => diff --git a/lib/services/overpass_service.dart b/lib/services/overpass_service.dart index 4028ab2..10db57e 100644 --- a/lib/services/overpass_service.dart +++ b/lib/services/overpass_service.dart @@ -6,12 +6,17 @@ import 'package:latlong2/latlong.dart'; import '../models/camera_profile.dart'; import '../models/osm_camera_node.dart'; -class OverpassService { - static const _endpoint = 'https://overpass-api.de/api/interpreter'; +import '../app_state.dart'; +class OverpassService { + static const _prodEndpoint = 'https://overpass-api.de/api/interpreter'; + static const _sandboxEndpoint = 'https://overpass-api.dev.openstreetmap.org/api/interpreter'; + + // You can pass UploadMode, or use production by default Future> fetchCameras( LatLngBounds bbox, List profiles, + {UploadMode uploadMode = UploadMode.production} ) async { if (profiles.isEmpty) return []; @@ -31,25 +36,35 @@ class OverpassService { out body 250; '''; - try { - final resp = - await http.post(Uri.parse(_endpoint), body: {'data': query.trim()}); - if (resp.statusCode != 200) return []; - - final data = jsonDecode(resp.body) as Map; - final elements = data['elements'] as List; - - return elements.whereType>().map((e) { - return OsmCameraNode( - id: e['id'], - coord: LatLng(e['lat'], e['lon']), - tags: Map.from(e['tags'] ?? {}), - ); - }).toList(); - } catch (_) { - // Network error – return empty list silently - return []; + Future> fetchFromUri(String endpoint, String query) async { + try { + print('[Overpass] Querying $endpoint'); + print('[Overpass] Query:\n$query'); + final resp = await http.post(Uri.parse(endpoint), body: {'data': query.trim()}); + print('[Overpass] Status: \\${resp.statusCode}, Length: \\${resp.body.length}'); + if (resp.statusCode != 200) { + print('[Overpass] Failed: \\${resp.body}'); + return []; + } + final data = jsonDecode(resp.body) as Map; + final elements = data['elements'] as List; + print('[Overpass] Retrieved elements: \\${elements.length}'); + return elements.whereType>().map((e) { + return OsmCameraNode( + id: e['id'], + coord: LatLng(e['lat'], e['lon']), + tags: Map.from(e['tags'] ?? {}), + ); + }).toList(); + } catch (e) { + print('[Overpass] Exception: \\${e}'); + // Network error – return empty list silently + return []; + } } + + // Fetch from production Overpass for all modes. + return await fetchFromUri(_prodEndpoint, query); } } diff --git a/lib/widgets/add_camera_sheet.dart b/lib/widgets/add_camera_sheet.dart index 3d910bb..2a7f4d9 100644 --- a/lib/widgets/add_camera_sheet.dart +++ b/lib/widgets/add_camera_sheet.dart @@ -26,6 +26,9 @@ class AddCameraSheet extends StatelessWidget { Navigator.pop(context); } + final customProfiles = appState.enabledProfiles.where((p) => !p.builtin).toList(); + final allowSubmit = customProfiles.isNotEmpty && !session.profile.builtin; + return Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), @@ -64,6 +67,38 @@ class AddCameraSheet extends StatelessWidget { onChanged: (v) => appState.updateSession(directionDeg: v), ), ), + if (customProfiles.isEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: const [ + Icon(Icons.info_outline, color: Colors.red, size: 20), + SizedBox(width: 6), + Expanded( + child: Text( + 'Enable or create a custom profile in Settings to submit new cameras.', + style: TextStyle(color: Colors.red, fontSize: 13), + ), + ), + ], + ), + ) + else if (session.profile.builtin) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: const [ + Icon(Icons.info_outline, color: Colors.orange, size: 20), + SizedBox(width: 6), + Expanded( + child: Text( + 'The built-in profile is for map viewing only. Please select a custom profile to submit new cameras.', + style: TextStyle(color: Colors.orange, fontSize: 13), + ), + ), + ], + ), + ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), @@ -78,7 +113,7 @@ class AddCameraSheet extends StatelessWidget { const SizedBox(width: 12), Expanded( child: ElevatedButton( - onPressed: _commit, + onPressed: allowSubmit ? _commit : null, child: const Text('Submit'), ), ), diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 8f4e4af..44c6f6f 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -82,7 +82,11 @@ class _MapViewState extends State { } catch (_) { return; // controller not ready yet } - final cams = await _overpass.fetchCameras(bounds, appState.enabledProfiles); + final cams = await _overpass.fetchCameras( + bounds, + appState.enabledProfiles, + uploadMode: appState.uploadMode, + ); if (mounted) setState(() => _cameras = cams); } @@ -99,6 +103,12 @@ class _MapViewState extends State { final appState = context.watch(); final session = appState.session; + // Always watch for changes on uploadMode/profiles and refresh if needed + // (debounced, to avoid flooding when quickly toggling) + WidgetsBinding.instance.addPostFrameCallback((_) { + _debounce(() => _refreshCameras(appState)); + }); + // Seed add‑mode target once, after first controller center is available. if (session != null && session.target == null) { try { @@ -181,6 +191,36 @@ class _MapViewState extends State { ], ), + // MODE INDICATOR badge (top-right) + if (appState.uploadMode == UploadMode.sandbox || appState.uploadMode == UploadMode.simulate) + Positioned( + top: 18, + right: 14, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: appState.uploadMode == UploadMode.sandbox + ? Colors.orange.withOpacity(0.90) + : Colors.deepPurple.withOpacity(0.80), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow(color: Colors.black26, blurRadius: 5, offset: Offset(0,2)), + ], + ), + child: Text( + appState.uploadMode == UploadMode.sandbox + ? 'SANDBOX MODE' + : 'SIMULATE', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 13, + letterSpacing: 1.1, + ), + ), + ), + ), + // Attribution overlay Positioned( bottom: 20,