mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
tiles not working but new map data provider exists in theory
This commit is contained in:
67
lib/services/map_data_provider.dart
Normal file
67
lib/services/map_data_provider.dart
Normal file
@@ -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<List<OsmCameraNode>> getCameras({
|
||||
required LatLngBounds bounds,
|
||||
required List<CameraProfile> 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<List<int>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
lib/services/map_data_submodules/cameras_from_overpass.dart
Normal file
58
lib/services/map_data_submodules/cameras_from_overpass.dart
Normal file
@@ -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<List<OsmCameraNode>> camerasFromOverpass({
|
||||
required LatLngBounds bounds,
|
||||
required List<CameraProfile> 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<String, dynamic>;
|
||||
final elements = data['elements'] as List<dynamic>;
|
||||
print('[camerasFromOverpass] Retrieved elements: ${elements.length}');
|
||||
return elements.whereType<Map<String, dynamic>>().map((e) {
|
||||
return OsmCameraNode(
|
||||
id: e['id'],
|
||||
coord: LatLng(e['lat'], e['lon']),
|
||||
tags: Map<String, String>.from(e['tags'] ?? {}),
|
||||
);
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
print('[camerasFromOverpass] Overpass exception: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
40
lib/services/map_data_submodules/tiles_from_osm.dart
Normal file
40
lib/services/map_data_submodules/tiles_from_osm.dart
Normal file
@@ -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<List<int>> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<DataProviderImage> {
|
||||
final TileCoordinates coords;
|
||||
final TileLayer options;
|
||||
DataProviderImage(this.coords, this.options);
|
||||
|
||||
@override
|
||||
Future<DataProviderImage> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<DataProviderImage>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(
|
||||
DataProviderImage key,
|
||||
Future<ui.Codec> Function(Uint8List, {int? cacheWidth, int? cacheHeight}) decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(DataProviderImage key, Future<ui.Codec> 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<int> _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<MapView> {
|
||||
late final MapController _controller;
|
||||
final OverpassService _overpass = OverpassService();
|
||||
final MapDataProvider _mapDataProvider = MapDataProvider();
|
||||
final Debouncer _debounce = Debouncer(const Duration(milliseconds: 500));
|
||||
|
||||
StreamSubscription<Position>? _positionSub;
|
||||
@@ -149,10 +206,11 @@ class _MapViewState extends State<MapView> {
|
||||
} 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<MapView> {
|
||||
),
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user