diff --git a/do_builds.sh b/do_builds.sh index 11b3b07..8e3f548 100755 --- a/do_builds.sh +++ b/do_builds.sh @@ -36,7 +36,7 @@ if [ "$BUILD_IOS" = true ]; then ./app2ipa.sh build/ios/iphoneos/Runner.app || exit 1 echo "Moving iOS files..." - mv Runner.ipa "../flockmap_v${appver}.ipa" || exit 1 + mv Runner.ipa "../deflock_v${appver}.ipa" || exit 1 echo fi @@ -45,7 +45,7 @@ if [ "$BUILD_ANDROID" = true ]; then flutter build apk || exit 1 echo "Moving Android files..." - cp build/app/outputs/flutter-apk/app-release.apk "../flockmap_v${appver}.apk" || exit 1 + cp build/app/outputs/flutter-apk/app-release.apk "../deflock_v${appver}.apk" || exit 1 echo fi diff --git a/lib/dev_config.dart b/lib/dev_config.dart index 16fd4df..f94e24d 100644 --- a/lib/dev_config.dart +++ b/lib/dev_config.dart @@ -34,7 +34,7 @@ const String kClientName = 'DeFlock'; const String kClientVersion = '0.9.11'; // Development/testing features - set to false for production builds -const bool kEnableDevelopmentModes = false; // Set to false to hide sandbox/simulate modes and force production mode +const bool kEnableDevelopmentModes = true; // Set to false to hide sandbox/simulate modes and force production mode // Marker/node interaction const int kCameraMinZoomLevel = 10; // Minimum zoom to show nodes or warning diff --git a/lib/services/map_data_provider.dart b/lib/services/map_data_provider.dart index d7ea4f8..9c5ad29 100644 --- a/lib/services/map_data_provider.dart +++ b/lib/services/map_data_provider.dart @@ -6,6 +6,7 @@ import '../models/node_profile.dart'; import '../models/osm_camera_node.dart'; import '../app_state.dart'; import 'map_data_submodules/nodes_from_overpass.dart'; +import 'map_data_submodules/nodes_from_osm_api.dart'; import 'map_data_submodules/tiles_from_remote.dart'; import 'map_data_submodules/nodes_from_local.dart'; import 'map_data_submodules/tiles_from_local.dart'; @@ -48,7 +49,7 @@ class MapDataProvider { if (offline) { throw OfflineModeException("Cannot fetch remote nodes in offline mode."); } - return fetchOverpassNodes( + return _fetchRemoteNodes( bounds: bounds, profiles: profiles, uploadMode: uploadMode, @@ -64,15 +65,31 @@ class MapDataProvider { ); } - // AUTO: In offline mode, only fetch local. In online mode, fetch both remote and local, then merge. + // AUTO: In offline mode, behavior depends on upload mode if (offline) { - return fetchLocalNodes( + if (uploadMode == UploadMode.sandbox) { + // Offline + Sandbox = no nodes (local cache is production data) + debugPrint('[MapDataProvider] Offline + Sandbox mode: returning no nodes (local cache is production data)'); + return []; + } else { + // Offline + Production = use local cache + return fetchLocalNodes( + bounds: bounds, + profiles: profiles, + maxNodes: AppState.instance.maxCameras, + ); + } + } else if (uploadMode == UploadMode.sandbox) { + // Sandbox mode: Only fetch from sandbox API, ignore local production nodes + debugPrint('[MapDataProvider] Sandbox mode: fetching only from sandbox API, ignoring local cache'); + return _fetchRemoteNodes( bounds: bounds, profiles: profiles, - maxNodes: AppState.instance.maxCameras, + uploadMode: uploadMode, + maxResults: AppState.instance.maxCameras, ); } else { - // Online mode: fetch both remote and local, then merge with deduplication + // Production mode: fetch both remote and local, then merge with deduplication final List>> futures = []; // Always try to get local nodes (fast, cached) @@ -83,7 +100,7 @@ class MapDataProvider { )); // Always try to get remote nodes (slower, fresh data) - futures.add(fetchOverpassNodes( + futures.add(_fetchRemoteNodes( bounds: bounds, profiles: profiles, uploadMode: uploadMode, @@ -134,7 +151,7 @@ class MapDataProvider { if (offline) { throw OfflineModeException("Cannot fetch remote nodes for offline area download in offline mode."); } - return fetchOverpassNodes( + return _fetchRemoteNodes( bounds: bounds, profiles: profiles, uploadMode: uploadMode, @@ -195,4 +212,58 @@ class MapDataProvider { void clearTileQueue() { clearRemoteTileQueue(); } + + /// Fetch remote nodes with Overpass first, OSM API fallback + Future> _fetchRemoteNodes({ + required LatLngBounds bounds, + required List profiles, + UploadMode uploadMode = UploadMode.production, + required int maxResults, + }) async { + // For sandbox mode, skip Overpass and go directly to OSM API + // (Overpass doesn't have sandbox data) + if (uploadMode == UploadMode.sandbox) { + debugPrint('[MapDataProvider] Sandbox mode detected, using OSM API directly'); + return fetchOsmApiNodes( + bounds: bounds, + profiles: profiles, + uploadMode: uploadMode, + maxResults: maxResults, + ); + } + + // For production mode, try Overpass first, then fallback to OSM API + try { + final nodes = await fetchOverpassNodes( + bounds: bounds, + profiles: profiles, + uploadMode: uploadMode, + maxResults: maxResults, + ); + + // If Overpass returns nodes, we're good + if (nodes.isNotEmpty) { + return nodes; + } + + // If Overpass returns empty (could be no data or could be an issue), + // try OSM API as well to be thorough + debugPrint('[MapDataProvider] Overpass returned no nodes, trying OSM API fallback'); + return fetchOsmApiNodes( + bounds: bounds, + profiles: profiles, + uploadMode: uploadMode, + maxResults: maxResults, + ); + + } catch (e) { + debugPrint('[MapDataProvider] Overpass failed ($e), trying OSM API fallback'); + return fetchOsmApiNodes( + bounds: bounds, + profiles: profiles, + uploadMode: uploadMode, + maxResults: maxResults, + ); + } + } } \ No newline at end of file diff --git a/lib/services/map_data_submodules/nodes_from_osm_api.dart b/lib/services/map_data_submodules/nodes_from_osm_api.dart new file mode 100644 index 0000000..01a8363 --- /dev/null +++ b/lib/services/map_data_submodules/nodes_from_osm_api.dart @@ -0,0 +1,129 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:flutter/foundation.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:xml/xml.dart'; + +import '../../models/node_profile.dart'; +import '../../models/osm_camera_node.dart'; +import '../../app_state.dart'; +import '../network_status.dart'; + +/// Fetches surveillance nodes from the direct OSM API using bbox query. +/// This is a fallback for when Overpass is not available (e.g., sandbox mode). +Future> fetchOsmApiNodes({ + required LatLngBounds bounds, + required List profiles, + UploadMode uploadMode = UploadMode.production, + required int maxResults, +}) async { + if (profiles.isEmpty) return []; + + // Choose API endpoint based on upload mode + final String apiHost = uploadMode == UploadMode.sandbox + ? 'api06.dev.openstreetmap.org' + : 'api.openstreetmap.org'; + + // Build the map query URL - fetches all data in bounding box + final left = bounds.southWest.longitude; + final bottom = bounds.southWest.latitude; + final right = bounds.northEast.longitude; + final top = bounds.northEast.latitude; + + final url = 'https://$apiHost/api/0.6/map?bbox=$left,$bottom,$right,$top'; + + try { + debugPrint('[fetchOsmApiNodes] Querying OSM API for nodes in bbox...'); + debugPrint('[fetchOsmApiNodes] URL: $url'); + + final response = await http.get(Uri.parse(url)); + + if (response.statusCode != 200) { + debugPrint('[fetchOsmApiNodes] OSM API error: ${response.statusCode} - ${response.body}'); + NetworkStatus.instance.reportOverpassIssue(); // Reuse same status tracking + return []; + } + + // Parse XML response + final document = XmlDocument.parse(response.body); + final nodes = []; + + // Find all node elements + for (final nodeElement in document.findAllElements('node')) { + final id = int.tryParse(nodeElement.getAttribute('id') ?? ''); + final latStr = nodeElement.getAttribute('lat'); + final lonStr = nodeElement.getAttribute('lon'); + + if (id == null || latStr == null || lonStr == null) continue; + + final lat = double.tryParse(latStr); + final lon = double.tryParse(lonStr); + if (lat == null || lon == null) continue; + + // Parse tags + final tags = {}; + for (final tagElement in nodeElement.findElements('tag')) { + final key = tagElement.getAttribute('k'); + final value = tagElement.getAttribute('v'); + if (key != null && value != null) { + tags[key] = value; + } + } + + // Check if this node matches any of our profiles + if (_nodeMatchesProfiles(tags, profiles)) { + nodes.add(OsmCameraNode( + id: id, + coord: LatLng(lat, lon), + tags: tags, + )); + } + + // Respect maxResults limit if set + if (maxResults > 0 && nodes.length >= maxResults) { + break; + } + } + + if (nodes.isNotEmpty) { + debugPrint('[fetchOsmApiNodes] Retrieved ${nodes.length} matching surveillance nodes'); + } + + NetworkStatus.instance.reportOverpassSuccess(); // Reuse same status tracking + return nodes; + + } catch (e) { + debugPrint('[fetchOsmApiNodes] Exception: $e'); + + // Report network issues for connection errors + if (e.toString().contains('Connection refused') || + e.toString().contains('Connection timed out') || + e.toString().contains('Connection reset')) { + NetworkStatus.instance.reportOverpassIssue(); + } + + return []; + } +} + +/// Check if a node's tags match any of the given profiles +bool _nodeMatchesProfiles(Map nodeTags, List profiles) { + for (final profile in profiles) { + if (_nodeMatchesProfile(nodeTags, profile)) { + return true; + } + } + return false; +} + +/// Check if a node's tags match a specific profile +bool _nodeMatchesProfile(Map nodeTags, NodeProfile profile) { + // All profile tags must be present in the node for it to match + for (final entry in profile.tags.entries) { + if (nodeTags[entry.key] != entry.value) { + return false; + } + } + return true; +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index cc5d217..b522d63 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -793,7 +793,7 @@ packages: source: hosted version: "1.1.0" xml: - dependency: transitive + dependency: "direct main" description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 diff --git a/pubspec.yaml b/pubspec.yaml index 6a4e7c7..d8f8ed9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: geolocator: ^10.1.0 http: ^1.2.1 flutter_svg: ^2.0.10 + xml: ^6.4.2 # Auth, storage, prefs oauth2_client: ^4.2.0