From 3dca7d575125e125505b2379aafe9d27bb1526b2 Mon Sep 17 00:00:00 2001 From: stopflock Date: Sat, 9 Aug 2025 13:50:10 -0500 Subject: [PATCH] tiles not working but new map data provider exists in theory --- lib/services/map_data/cams_from_local.dart | 0 lib/services/map_data/cams_from_osm.dart | 0 lib/services/map_data/tiles_from_osm.dart | 0 lib/services/map_data_provider.dart | 67 +++++++++++++++ .../cameras_from_local.dart} | 0 .../cameras_from_overpass.dart | 58 +++++++++++++ .../tiles_from_local.dart | 0 .../map_data_submodules/tiles_from_osm.dart | 40 +++++++++ lib/widgets/map_view.dart | 83 +++++++++++++++---- 9 files changed, 232 insertions(+), 16 deletions(-) delete mode 100644 lib/services/map_data/cams_from_local.dart delete mode 100644 lib/services/map_data/cams_from_osm.dart delete mode 100644 lib/services/map_data/tiles_from_osm.dart create mode 100644 lib/services/map_data_provider.dart rename lib/services/{map_data.dart => map_data_submodules/cameras_from_local.dart} (100%) create mode 100644 lib/services/map_data_submodules/cameras_from_overpass.dart rename lib/services/{map_data => map_data_submodules}/tiles_from_local.dart (100%) create mode 100644 lib/services/map_data_submodules/tiles_from_osm.dart diff --git a/lib/services/map_data/cams_from_local.dart b/lib/services/map_data/cams_from_local.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/services/map_data/cams_from_osm.dart b/lib/services/map_data/cams_from_osm.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/services/map_data/tiles_from_osm.dart b/lib/services/map_data/tiles_from_osm.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/services/map_data_provider.dart b/lib/services/map_data_provider.dart new file mode 100644 index 0000000..e5d0be7 --- /dev/null +++ b/lib/services/map_data_provider.dart @@ -0,0 +1,67 @@ +import 'package:latlong2/latlong.dart'; +import 'package:flutter_map/flutter_map.dart'; + +import '../models/camera_profile.dart'; +import '../models/osm_camera_node.dart'; +import '../app_state.dart'; +import 'map_data_submodules/cameras_from_overpass.dart'; +import 'map_data_submodules/tiles_from_osm.dart'; + +enum MapSource { local, remote, auto } // For future use + +class OfflineModeException implements Exception { + final String message; + OfflineModeException(this.message); + @override + String toString() => 'OfflineModeException: $message'; +} + +class MapDataProvider { + static final MapDataProvider _instance = MapDataProvider._(); + factory MapDataProvider() => _instance; + MapDataProvider._(); + + bool _offlineMode = false; + bool get isOfflineMode => _offlineMode; + void setOfflineMode(bool enabled) { + _offlineMode = enabled; + } + + /// Fetch cameras from OSM/Overpass or local storage, depending on source/offline mode. + Future> getCameras({ + required LatLngBounds bounds, + required List profiles, + UploadMode uploadMode = UploadMode.production, + MapSource source = MapSource.auto, + }) async { + // Resolve source: + if (_offlineMode && source != MapSource.local) { + throw OfflineModeException("Cannot fetch remote cameras in offline mode."); + } + if (source == MapSource.local) { + // TODO: implement local camera loading + throw UnimplementedError('Local camera loading not yet implemented.'); + } else { + // Use Overpass remote fetch, from submodule: + return camerasFromOverpass(bounds: bounds, profiles: profiles, uploadMode: uploadMode); + } + } + /// Fetch tile image bytes from OSM or local (future). Only fetches, does not save! + Future> getTile({ + required int z, + required int x, + required int y, + MapSource source = MapSource.auto, + }) async { + if (_offlineMode && source != MapSource.local) { + throw OfflineModeException("Cannot fetch remote tiles in offline mode."); + } + if (source == MapSource.local) { + // TODO: implement local tile loading + throw UnimplementedError('Local tile loading not yet implemented.'); + } else { + // Use OSM remote fetch from submodule: + return fetchOSMTile(z: z, x: x, y: y); + } + } +} \ No newline at end of file diff --git a/lib/services/map_data.dart b/lib/services/map_data_submodules/cameras_from_local.dart similarity index 100% rename from lib/services/map_data.dart rename to lib/services/map_data_submodules/cameras_from_local.dart diff --git a/lib/services/map_data_submodules/cameras_from_overpass.dart b/lib/services/map_data_submodules/cameras_from_overpass.dart new file mode 100644 index 0000000..998f30b --- /dev/null +++ b/lib/services/map_data_submodules/cameras_from_overpass.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:latlong2/latlong.dart'; +import 'package:flutter_map/flutter_map.dart'; + +import '../../models/camera_profile.dart'; +import '../../models/osm_camera_node.dart'; +import '../../app_state.dart'; + +/// Fetches cameras from the Overpass OSM API for the given bounds and profiles. +Future> camerasFromOverpass({ + required LatLngBounds bounds, + required List profiles, + UploadMode uploadMode = UploadMode.production, +}) async { + if (profiles.isEmpty) return []; + + final nodeClauses = profiles.map((profile) { + final tagFilters = profile.tags.entries + .map((e) => '["${e.key}"="${e.value}"]') + .join('\n '); + return '''node\n $tagFilters\n (${bounds.southWest.latitude},${bounds.southWest.longitude},\n ${bounds.northEast.latitude},${bounds.northEast.longitude});'''; + }).join('\n '); + + const String prodEndpoint = 'https://overpass-api.de/api/interpreter'; + + final query = ''' + [out:json][timeout:25]; + ( + $nodeClauses + ); + out body 250; + '''; + + try { + print('[camerasFromOverpass] Querying Overpass...'); + print('[camerasFromOverpass] Query:\n$query'); + final resp = await http.post(Uri.parse(prodEndpoint), body: {'data': query.trim()}); + print('[camerasFromOverpass] Status: ${resp.statusCode}, Length: ${resp.body.length}'); + if (resp.statusCode != 200) { + print('[camerasFromOverpass] Overpass failed: ${resp.body}'); + return []; + } + final data = jsonDecode(resp.body) as Map; + final elements = data['elements'] as List; + print('[camerasFromOverpass] 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('[camerasFromOverpass] Overpass exception: $e'); + return []; + } +} diff --git a/lib/services/map_data/tiles_from_local.dart b/lib/services/map_data_submodules/tiles_from_local.dart similarity index 100% rename from lib/services/map_data/tiles_from_local.dart rename to lib/services/map_data_submodules/tiles_from_local.dart diff --git a/lib/services/map_data_submodules/tiles_from_osm.dart b/lib/services/map_data_submodules/tiles_from_osm.dart new file mode 100644 index 0000000..5a055c7 --- /dev/null +++ b/lib/services/map_data_submodules/tiles_from_osm.dart @@ -0,0 +1,40 @@ +import 'dart:math'; +import 'dart:io'; +import 'package:http/http.dart' as http; + +/// Fetches a tile from OSM, with in-memory retries/backoff. +/// Returns tile image bytes, or throws on persistent failure. +Future> fetchOSMTile({ + required int z, + required int x, + required int y, +}) async { + final url = 'https://tile.openstreetmap.org/$z/$x/$y.png'; + const int maxAttempts = 3; + int attempt = 0; + final random = Random(); + final delays = [ + 0, + 3000 + random.nextInt(1000) - 500, + 10000 + random.nextInt(4000) - 2000 + ]; + while (true) { + try { + attempt++; + final resp = await http.get(Uri.parse(url)); + if (resp.statusCode == 200) { + return resp.bodyBytes; + } else { + throw HttpException('Failed to fetch tile $z/$x/$y: status ${resp.statusCode}'); + } + } catch (e) { + if (attempt >= maxAttempts) { + print("[fetchOSMTile] Failed for $z/$x/$y after $attempt attempts: $e"); + rethrow; + } + final delay = delays[attempt - 1].clamp(0, 60000); + print("[fetchOSMTile] Attempt $attempt for $z/$x/$y failed: $e. Retrying in ${delay}ms."); + await Future.delayed(Duration(milliseconds: delay)); + } + } +} diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 5451f8f..ff465e1 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -1,21 +1,78 @@ import 'dart:async'; import 'dart:math' as math; import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui' as ui; import 'package:http/io_client.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; import 'package:geolocator/geolocator.dart'; import 'package:provider/provider.dart'; import '../app_state.dart'; -import '../services/overpass_service.dart'; +import '../services/map_data_provider.dart'; import '../services/offline_area_service.dart'; import '../models/osm_camera_node.dart'; import 'debouncer.dart'; import 'camera_tag_sheet.dart'; +class DataProviderTileProvider extends TileProvider { + @override + ImageProvider getImage(TileCoordinates coords, TileLayer options) { + return DataProviderImage(coords, options); + } +} + +class DataProviderImage extends ImageProvider { + final TileCoordinates coords; + final TileLayer options; + DataProviderImage(this.coords, this.options); + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter load( + DataProviderImage key, + Future Function(Uint8List, {int? cacheWidth, int? cacheHeight}) decode) { + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, decode), + scale: 1.0, + ); + } + + Future _loadAsync(DataProviderImage key, Future Function(Uint8List, {int? cacheWidth, int? cacheHeight}) decode) async { + final z = key.coords.z; + final x = key.coords.x; + final y = key.coords.y; + + try { + final bytes = await MapDataProvider().getTile(z: z, x: x, y: y); + if (bytes.isEmpty) throw Exception("Empty image bytes for $z/$x/$y"); + return await decode(Uint8List.fromList(bytes)); + } catch (e) { + // Optionally: provide an error tile + print('[MapView] Failed to load OSM tile for $z/$x/$y: $e'); + // Return a blank pixel or a fallback error tile of your design + return await decode(Uint8List.fromList(_transparentPng)); + } + } + + // A tiny 1x1 transparent PNG + static const List _transparentPng = [ + 137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82, + 0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137, + 0,0,0,10,73,68,65,84,8,153,99,0,1,0,0,5, + 0,1,13,10,42,100,0,0,0,0,73,69,78,68,174,66, + 96,130]; +} + // --- Smart marker widget for camera with single/double tap distinction class _CameraMapMarker extends StatefulWidget { final OsmCameraNode node; @@ -79,7 +136,7 @@ class MapView extends StatefulWidget { class _MapViewState extends State { late final MapController _controller; - final OverpassService _overpass = OverpassService(); + final MapDataProvider _mapDataProvider = MapDataProvider(); final Debouncer _debounce = Debouncer(const Duration(milliseconds: 500)); StreamSubscription? _positionSub; @@ -149,10 +206,11 @@ class _MapViewState extends State { } catch (_) { return; // controller not ready yet } - final cams = await _overpass.fetchCameras( - bounds, - appState.enabledProfiles, + 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); } @@ -232,17 +290,10 @@ class _MapViewState extends State { ), children: [ TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - tileProvider: NetworkTileProvider( - headers: { - 'User-Agent': - 'FlockMap/0.4 (+https://github.com/yourrepo)', - }, - httpClient: IOClient( - HttpClient()..maxConnectionsPerHost = 4, - ), - ), - userAgentPackageName: 'com.example.flock_map_app', + tileProvider: DataProviderTileProvider(), + urlTemplate: '', // Not used by custom provider + tileSize: 256, + // Any other TileLayer customization as needed ), PolygonLayer(polygons: overlays), MarkerLayer(markers: markers),