mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-08-15 15:40:32 +02:00
Consolidate / dedupe some code
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
import 'models/camera_profile.dart';
|
import 'models/camera_profile.dart';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../models/camera_profile.dart';
|
import '../models/camera_profile.dart';
|
||||||
import '../models/osm_camera_node.dart';
|
import '../models/osm_camera_node.dart';
|
||||||
@@ -125,7 +126,7 @@ class MapDataProvider {
|
|||||||
if (offline) {
|
if (offline) {
|
||||||
throw OfflineModeException("Cannot fetch remote tiles in offline mode.");
|
throw OfflineModeException("Cannot fetch remote tiles in offline mode.");
|
||||||
}
|
}
|
||||||
return fetchOSMTile(z: z, x: x, y: y);
|
return _fetchRemoteTileFromCurrentProvider(z, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly local
|
// Explicitly local
|
||||||
@@ -138,13 +139,29 @@ class MapDataProvider {
|
|||||||
return await fetchLocalTile(z: z, x: x, y: y);
|
return await fetchLocalTile(z: z, x: x, y: y);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (!offline) {
|
if (!offline) {
|
||||||
return fetchOSMTile(z: z, x: x, y: y);
|
return _fetchRemoteTileFromCurrentProvider(z, x, y);
|
||||||
} else {
|
} else {
|
||||||
throw OfflineModeException("Tile $z/$x/$y not found in offline areas and offline mode is enabled.");
|
throw OfflineModeException("Tile $z/$x/$y not found in offline areas and offline mode is enabled.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch remote tile using current provider from AppState
|
||||||
|
Future<List<int>> _fetchRemoteTileFromCurrentProvider(int z, int x, int y) async {
|
||||||
|
final appState = AppState.instance;
|
||||||
|
final selectedTileType = appState.selectedTileType;
|
||||||
|
final selectedProvider = appState.selectedTileProvider;
|
||||||
|
|
||||||
|
if (selectedTileType != null && selectedProvider != null) {
|
||||||
|
// Use current provider
|
||||||
|
final tileUrl = selectedTileType.getTileUrl(z, x, y, apiKey: selectedProvider.apiKey);
|
||||||
|
return fetchRemoteTile(z: z, x: x, y: y, url: tileUrl);
|
||||||
|
} else {
|
||||||
|
// Fallback to OSM if no provider selected
|
||||||
|
return fetchOSMTile(z: z, x: x, y: y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Clear any queued tile requests (call when map view changes significantly)
|
/// Clear any queued tile requests (call when map view changes significantly)
|
||||||
void clearTileQueue() {
|
void clearTileQueue() {
|
||||||
clearRemoteTileQueue();
|
clearRemoteTileQueue();
|
||||||
|
|||||||
@@ -41,23 +41,23 @@ Future<List<int>> fetchRemoteTile({
|
|||||||
while (true) {
|
while (true) {
|
||||||
await _tileFetchSemaphore.acquire();
|
await _tileFetchSemaphore.acquire();
|
||||||
try {
|
try {
|
||||||
print('[fetchRemoteTile] FETCH $z/$x/$y from $hostInfo');
|
// Only log on first attempt or errors
|
||||||
|
if (attempt == 1) {
|
||||||
|
debugPrint('[fetchRemoteTile] Fetching $z/$x/$y from $hostInfo');
|
||||||
|
}
|
||||||
attempt++;
|
attempt++;
|
||||||
final resp = await http.get(Uri.parse(url));
|
final resp = await http.get(Uri.parse(url));
|
||||||
print('[fetchRemoteTile] HTTP ${resp.statusCode} for $z/$x/$y from $hostInfo, length=${resp.bodyBytes.length}');
|
|
||||||
|
|
||||||
if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) {
|
if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) {
|
||||||
print('[fetchRemoteTile] SUCCESS $z/$x/$y from $hostInfo');
|
// Success - no logging for normal operation
|
||||||
NetworkStatus.instance.reportOsmTileSuccess(); // Still use OSM reporting for now
|
NetworkStatus.instance.reportOsmTileSuccess(); // Still use OSM reporting for now
|
||||||
return resp.bodyBytes;
|
return resp.bodyBytes;
|
||||||
} else {
|
} else {
|
||||||
print('[fetchRemoteTile] FAIL $z/$x/$y from $hostInfo: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}');
|
debugPrint('[fetchRemoteTile] FAIL $z/$x/$y from $hostInfo: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}');
|
||||||
NetworkStatus.instance.reportOsmTileIssue(); // Still use OSM reporting for now
|
NetworkStatus.instance.reportOsmTileIssue(); // Still use OSM reporting for now
|
||||||
throw HttpException('Failed to fetch tile $z/$x/$y from $hostInfo: status ${resp.statusCode}');
|
throw HttpException('Failed to fetch tile $z/$x/$y from $hostInfo: status ${resp.statusCode}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[fetchRemoteTile] Exception $z/$x/$y from $hostInfo: $e');
|
|
||||||
|
|
||||||
// Report network issues on connection errors
|
// Report network issues on connection errors
|
||||||
if (e.toString().contains('Connection refused') ||
|
if (e.toString().contains('Connection refused') ||
|
||||||
e.toString().contains('Connection timed out') ||
|
e.toString().contains('Connection timed out') ||
|
||||||
@@ -66,12 +66,14 @@ Future<List<int>> fetchRemoteTile({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (attempt >= maxAttempts) {
|
if (attempt >= maxAttempts) {
|
||||||
print("[fetchRemoteTile] Failed for $z/$x/$y from $hostInfo after $attempt attempts: $e");
|
debugPrint("[fetchRemoteTile] Failed for $z/$x/$y from $hostInfo after $attempt attempts: $e");
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
final delay = delays[attempt - 1].clamp(0, 60000);
|
final delay = delays[attempt - 1].clamp(0, 60000);
|
||||||
print("[fetchRemoteTile] Attempt $attempt for $z/$x/$y from $hostInfo failed: $e. Retrying in ${delay}ms.");
|
if (attempt == 1) {
|
||||||
|
debugPrint("[fetchRemoteTile] Attempt $attempt for $z/$x/$y from $hostInfo failed: $e. Retrying in ${delay}ms.");
|
||||||
|
}
|
||||||
await Future.delayed(Duration(milliseconds: delay));
|
await Future.delayed(Duration(milliseconds: delay));
|
||||||
} finally {
|
} finally {
|
||||||
_tileFetchSemaphore.release();
|
_tileFetchSemaphore.release();
|
||||||
|
|||||||
@@ -4,13 +4,10 @@ import 'dart:math' as math;
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
|
import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
import '../../app_state.dart';
|
import '../../app_state.dart';
|
||||||
import '../../models/osm_camera_node.dart';
|
import '../../models/osm_camera_node.dart';
|
||||||
import '../../models/tile_provider.dart';
|
|
||||||
import '../map_data_provider.dart';
|
import '../map_data_provider.dart';
|
||||||
import '../map_data_submodules/tiles_from_remote.dart';
|
|
||||||
import 'offline_area_models.dart';
|
import 'offline_area_models.dart';
|
||||||
import 'offline_tile_utils.dart';
|
import 'offline_tile_utils.dart';
|
||||||
import 'package:flock_map_app/dev_config.dart';
|
import 'package:flock_map_app/dev_config.dart';
|
||||||
@@ -30,32 +27,6 @@ class OfflineAreaDownloader {
|
|||||||
required Future<void> Function() saveAreasToDisk,
|
required Future<void> Function() saveAreasToDisk,
|
||||||
required Future<void> Function(OfflineArea) getAreaSizeBytes,
|
required Future<void> Function(OfflineArea) getAreaSizeBytes,
|
||||||
}) async {
|
}) async {
|
||||||
// Get tile provider info from the area metadata or current AppState
|
|
||||||
TileProvider? tileProvider;
|
|
||||||
TileType? tileType;
|
|
||||||
|
|
||||||
final appState = AppState.instance;
|
|
||||||
|
|
||||||
if (area.tileProviderId != null && area.tileTypeId != null) {
|
|
||||||
// Use the provider info stored with the area (for refreshing existing areas)
|
|
||||||
try {
|
|
||||||
tileProvider = appState.tileProviders.firstWhere(
|
|
||||||
(p) => p.id == area.tileProviderId,
|
|
||||||
);
|
|
||||||
tileType = tileProvider.tileTypes.firstWhere(
|
|
||||||
(t) => t.id == area.tileTypeId,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// Fallback if stored provider/type not found
|
|
||||||
tileProvider = appState.selectedTileProvider ?? appState.tileProviders.firstOrNull;
|
|
||||||
tileType = appState.selectedTileType ?? tileProvider?.tileTypes.firstOrNull;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// New area - use currently selected provider
|
|
||||||
tileProvider = appState.selectedTileProvider ?? appState.tileProviders.firstOrNull;
|
|
||||||
tileType = appState.selectedTileType ?? tileProvider?.tileTypes.firstOrNull;
|
|
||||||
}
|
|
||||||
// Calculate tiles to download
|
|
||||||
Set<List<int>> allTiles;
|
Set<List<int>> allTiles;
|
||||||
if (area.isPermanent) {
|
if (area.isPermanent) {
|
||||||
allTiles = computeTileList(globalWorldBounds(), kWorldMinZoom, kWorldMaxZoom);
|
allTiles = computeTileList(globalWorldBounds(), kWorldMinZoom, kWorldMaxZoom);
|
||||||
@@ -72,8 +43,6 @@ class OfflineAreaDownloader {
|
|||||||
onProgress: onProgress,
|
onProgress: onProgress,
|
||||||
saveAreasToDisk: saveAreasToDisk,
|
saveAreasToDisk: saveAreasToDisk,
|
||||||
getAreaSizeBytes: getAreaSizeBytes,
|
getAreaSizeBytes: getAreaSizeBytes,
|
||||||
tileProvider: tileProvider,
|
|
||||||
tileType: tileType,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Download cameras for non-permanent areas
|
// Download cameras for non-permanent areas
|
||||||
@@ -99,8 +68,6 @@ class OfflineAreaDownloader {
|
|||||||
void Function(double progress)? onProgress,
|
void Function(double progress)? onProgress,
|
||||||
required Future<void> Function() saveAreasToDisk,
|
required Future<void> Function() saveAreasToDisk,
|
||||||
required Future<void> Function(OfflineArea) getAreaSizeBytes,
|
required Future<void> Function(OfflineArea) getAreaSizeBytes,
|
||||||
TileProvider? tileProvider,
|
|
||||||
TileType? tileType,
|
|
||||||
}) async {
|
}) async {
|
||||||
int pass = 0;
|
int pass = 0;
|
||||||
Set<List<int>> tilesToFetch = allTiles;
|
Set<List<int>> tilesToFetch = allTiles;
|
||||||
@@ -113,7 +80,7 @@ class OfflineAreaDownloader {
|
|||||||
for (final tile in tilesToFetch) {
|
for (final tile in tilesToFetch) {
|
||||||
if (area.status == OfflineAreaStatus.cancelled) break;
|
if (area.status == OfflineAreaStatus.cancelled) break;
|
||||||
|
|
||||||
if (await _downloadSingleTile(tile, directory, area, tileProvider, tileType)) {
|
if (await _downloadSingleTile(tile, directory, area)) {
|
||||||
totalDone++;
|
totalDone++;
|
||||||
area.tilesDownloaded = totalDone;
|
area.tilesDownloaded = totalDone;
|
||||||
area.progress = area.tilesTotal == 0 ? 0.0 : (totalDone / area.tilesTotal);
|
area.progress = area.tilesTotal == 0 ? 0.0 : (totalDone / area.tilesTotal);
|
||||||
@@ -134,25 +101,20 @@ class OfflineAreaDownloader {
|
|||||||
return false; // Failed after max retries
|
return false; // Failed after max retries
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download a single tile
|
/// Download a single tile using the unified MapDataProvider path
|
||||||
static Future<bool> _downloadSingleTile(
|
static Future<bool> _downloadSingleTile(
|
||||||
List<int> tile,
|
List<int> tile,
|
||||||
String directory,
|
String directory,
|
||||||
OfflineArea area,
|
OfflineArea area,
|
||||||
TileProvider? tileProvider,
|
|
||||||
TileType? tileType,
|
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
List<int> bytes;
|
// Use the same unified path as live tiles: always go through MapDataProvider
|
||||||
|
final bytes = await MapDataProvider().getTile(
|
||||||
if (tileType != null && tileProvider != null) {
|
z: tile[0],
|
||||||
// Use the same path as live tiles: build URL and fetch directly
|
x: tile[1],
|
||||||
final tileUrl = tileType.getTileUrl(tile[0], tile[1], tile[2], apiKey: tileProvider.apiKey);
|
y: tile[2],
|
||||||
bytes = await fetchRemoteTile(z: tile[0], x: tile[1], y: tile[2], url: tileUrl);
|
source: MapSource.remote, // Force remote fetch for downloads
|
||||||
} else {
|
);
|
||||||
// Fallback to OSM for legacy areas or when no provider info
|
|
||||||
bytes = await fetchOSMTile(z: tile[0], x: tile[1], y: tile[2]);
|
|
||||||
}
|
|
||||||
if (bytes.isNotEmpty) {
|
if (bytes.isNotEmpty) {
|
||||||
await OfflineAreaDownloader.saveTileBytes(tile[0], tile[1], tile[2], directory, bytes);
|
await OfflineAreaDownloader.saveTileBytes(tile[0], tile[1], tile[2], directory, bytes);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -13,77 +13,50 @@ class SimpleTileHttpClient extends http.BaseClient {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||||
// Try to parse as a tile request from any provider
|
// Extract tile coordinates from the URL using our standard pattern
|
||||||
final tileInfo = _parseTileRequest(request.url);
|
final tileCoords = _extractTileCoords(request.url);
|
||||||
if (tileInfo != null) {
|
if (tileCoords != null) {
|
||||||
return _handleTileRequest(request, tileInfo);
|
final z = tileCoords['z']!; // We know these are not null from _extractTileCoords
|
||||||
|
final x = tileCoords['x']!;
|
||||||
|
final y = tileCoords['y']!;
|
||||||
|
return _handleTileRequest(z, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass through non-tile requests
|
// Pass through non-tile requests
|
||||||
return _inner.send(request);
|
return _inner.send(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse URL to extract tile coordinates if it looks like a tile request
|
/// Extract z/x/y coordinates from our standard tile URL pattern
|
||||||
Map<String, dynamic>? _parseTileRequest(Uri url) {
|
Map<String, int>? _extractTileCoords(Uri url) {
|
||||||
|
// We'll use a simple standard pattern: /{z}/{x}/{y}.png
|
||||||
|
// This will be the format we use in map_view.dart
|
||||||
final pathSegments = url.pathSegments;
|
final pathSegments = url.pathSegments;
|
||||||
|
|
||||||
// Common patterns for tile URLs:
|
if (pathSegments.length == 3) {
|
||||||
// OSM: /z/x/y.png
|
final z = int.tryParse(pathSegments[0]);
|
||||||
// Google: /vt/lyrs=y&x=x&y=y&z=z (query params)
|
final x = int.tryParse(pathSegments[1]);
|
||||||
// Mapbox: /styles/v1/mapbox/streets-v12/tiles/z/x/y
|
final yWithExt = pathSegments[2];
|
||||||
// ArcGIS: /tile/z/y/x.png
|
final y = int.tryParse(yWithExt.replaceAll(RegExp(r'\.[^.]*$'), '')); // Remove .png
|
||||||
|
|
||||||
// Try query parameters first (Google style)
|
|
||||||
final query = url.queryParameters;
|
|
||||||
if (query.containsKey('x') && query.containsKey('y') && query.containsKey('z')) {
|
|
||||||
final x = int.tryParse(query['x']!);
|
|
||||||
final y = int.tryParse(query['y']!);
|
|
||||||
final z = int.tryParse(query['z']!);
|
|
||||||
if (x != null && y != null && z != null) {
|
|
||||||
return {'z': z, 'x': x, 'y': y, 'originalUrl': url.toString()};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try path-based patterns
|
|
||||||
if (pathSegments.length >= 3) {
|
|
||||||
// Try z/x/y pattern (OSM style) - can be at different positions
|
|
||||||
for (int i = 0; i <= pathSegments.length - 3; i++) {
|
|
||||||
final z = int.tryParse(pathSegments[i]);
|
|
||||||
final x = int.tryParse(pathSegments[i + 1]);
|
|
||||||
final yWithExt = pathSegments[i + 2];
|
|
||||||
final y = int.tryParse(yWithExt.replaceAll(RegExp(r'\.[^.]*$'), '')); // Remove file extension
|
|
||||||
|
|
||||||
if (z != null && x != null && y != null) {
|
|
||||||
return {'z': z, 'x': x, 'y': y, 'originalUrl': url.toString()};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // Not a recognizable tile request
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<http.StreamedResponse> _handleTileRequest(http.BaseRequest request, Map<String, dynamic> tileInfo) async {
|
|
||||||
final z = tileInfo['z'] as int;
|
|
||||||
final x = tileInfo['x'] as int;
|
|
||||||
final y = tileInfo['y'] as int;
|
|
||||||
final originalUrl = tileInfo['originalUrl'] as String;
|
|
||||||
|
|
||||||
return _getTile(z, x, y, originalUrl, request.url.host);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<http.StreamedResponse> _getTile(int z, int x, int y, String originalUrl, String providerHost) async {
|
|
||||||
try {
|
|
||||||
// First try to get tile from offline storage
|
|
||||||
final localTileBytes = await _mapDataProvider.getTile(z: z, x: x, y: y, source: MapSource.local);
|
|
||||||
|
|
||||||
debugPrint('[SimpleTileService] Serving tile $z/$x/$y from offline storage');
|
if (z != null && x != null && y != null) {
|
||||||
|
return {'z': z, 'x': x, 'y': y};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.StreamedResponse> _handleTileRequest(int z, int x, int y) async {
|
||||||
|
try {
|
||||||
|
// Always go through MapDataProvider - it handles offline/online routing
|
||||||
|
final tileBytes = await _mapDataProvider.getTile(z: z, x: x, y: y, source: MapSource.auto);
|
||||||
|
|
||||||
// Clear waiting status - we got data
|
// Clear waiting status - we got data
|
||||||
NetworkStatus.instance.clearWaiting();
|
NetworkStatus.instance.clearWaiting();
|
||||||
|
|
||||||
// Serve offline tile with proper cache headers
|
// Serve tile with proper cache headers
|
||||||
return http.StreamedResponse(
|
return http.StreamedResponse(
|
||||||
Stream.value(localTileBytes),
|
Stream.value(tileBytes),
|
||||||
200,
|
200,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'image/png',
|
'Content-Type': 'image/png',
|
||||||
@@ -94,39 +67,15 @@ class SimpleTileHttpClient extends http.BaseClient {
|
|||||||
);
|
);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No offline tile available
|
debugPrint('[SimpleTileService] Could not get tile $z/$x/$y: $e');
|
||||||
debugPrint('[SimpleTileService] No offline tile for $z/$x/$y');
|
|
||||||
|
|
||||||
// Check if we're in offline mode
|
// Let MapDataProvider handle offline mode logic
|
||||||
if (AppState.instance.offlineMode) {
|
// Just return 404 and let flutter_map handle it gracefully
|
||||||
debugPrint('[SimpleTileService] Offline mode - not attempting $providerHost fetch for $z/$x/$y');
|
return http.StreamedResponse(
|
||||||
// Report that we couldn't serve this tile offline
|
Stream.value(<int>[]),
|
||||||
NetworkStatus.instance.reportOfflineMiss();
|
404,
|
||||||
return http.StreamedResponse(
|
reasonPhrase: 'Tile unavailable: $e',
|
||||||
Stream.value(<int>[]),
|
);
|
||||||
404,
|
|
||||||
reasonPhrase: 'Tile not available offline',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're online - try the original provider with proper error handling
|
|
||||||
debugPrint('[SimpleTileService] Online mode - trying $providerHost for $z/$x/$y');
|
|
||||||
try {
|
|
||||||
final response = await _inner.send(http.Request('GET', Uri.parse(originalUrl)));
|
|
||||||
// Clear waiting status on successful network tile
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
NetworkStatus.instance.clearWaiting();
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
} catch (networkError) {
|
|
||||||
debugPrint('[SimpleTileService] $providerHost request failed for $z/$x/$y: $networkError');
|
|
||||||
// Return 404 instead of throwing - let flutter_map handle gracefully
|
|
||||||
return http.StreamedResponse(
|
|
||||||
Stream.value(<int>[]),
|
|
||||||
404,
|
|
||||||
reasonPhrase: 'Network tile unavailable: $networkError',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-44
@@ -54,6 +54,9 @@ class MapViewState extends State<MapView> {
|
|||||||
|
|
||||||
// Track zoom to clear queue on zoom changes
|
// Track zoom to clear queue on zoom changes
|
||||||
double? _lastZoom;
|
double? _lastZoom;
|
||||||
|
|
||||||
|
// Track tile type changes to clear cache
|
||||||
|
String? _lastTileTypeId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -171,56 +174,16 @@ class MapViewState extends State<MapView> {
|
|||||||
return ids1.length == ids2.length && ids1.containsAll(ids2);
|
return ids1.length == ids2.length && ids1.containsAll(ids2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build tile layer based on selected tile provider
|
/// Build tile layer - uses standard URL that SimpleTileHttpClient can parse
|
||||||
Widget _buildTileLayer(AppState appState) {
|
Widget _buildTileLayer(AppState appState) {
|
||||||
final selectedTileType = appState.selectedTileType;
|
// Use a generic URL template that SimpleTileHttpClient recognizes
|
||||||
final selectedProvider = appState.selectedTileProvider;
|
// The actual provider URL will be built by MapDataProvider using current AppState
|
||||||
|
|
||||||
// Fallback to first available tile type if none selected
|
|
||||||
if (selectedTileType == null || selectedProvider == null) {
|
|
||||||
final allTypes = <TileType>[];
|
|
||||||
for (final provider in appState.tileProviders) {
|
|
||||||
allTypes.addAll(provider.availableTileTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
final fallback = allTypes.firstOrNull;
|
|
||||||
if (fallback != null) {
|
|
||||||
return TileLayer(
|
|
||||||
urlTemplate: fallback.urlTemplate,
|
|
||||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
|
||||||
tileProvider: NetworkTileProvider(
|
|
||||||
httpClient: _tileHttpClient,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ultimate fallback - hardcoded OSM
|
|
||||||
return TileLayer(
|
|
||||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
||||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
|
||||||
tileProvider: NetworkTileProvider(
|
|
||||||
httpClient: _tileHttpClient,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the URL template with API key if needed
|
|
||||||
String urlTemplate = selectedTileType.urlTemplate;
|
|
||||||
if (selectedTileType.requiresApiKey && selectedProvider.apiKey != null) {
|
|
||||||
urlTemplate = urlTemplate.replaceAll('{api_key}', selectedProvider.apiKey!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, use our custom HTTP client for all tile requests
|
|
||||||
// This will enable offline support for all providers
|
|
||||||
return TileLayer(
|
return TileLayer(
|
||||||
urlTemplate: urlTemplate,
|
urlTemplate: 'https://tiles.local/{z}/{x}/{y}.png',
|
||||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||||
tileProvider: NetworkTileProvider(
|
tileProvider: NetworkTileProvider(
|
||||||
httpClient: _tileHttpClient,
|
httpClient: _tileHttpClient,
|
||||||
),
|
),
|
||||||
additionalOptions: {
|
|
||||||
'attribution': selectedTileType.attribution,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +208,17 @@ class MapViewState extends State<MapView> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if tile type changed and clear cache if needed
|
||||||
|
final currentTileTypeId = appState.selectedTileType?.id;
|
||||||
|
if (_lastTileTypeId != null && _lastTileTypeId != currentTileTypeId) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// Clear our tile request queue
|
||||||
|
_tileHttpClient.clearTileQueue();
|
||||||
|
// Note: The ValueKey on FlutterMap will cause flutter_map to rebuild and clear its cache
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_lastTileTypeId = currentTileTypeId;
|
||||||
|
|
||||||
// Seed add‑mode target once, after first controller center is available.
|
// Seed add‑mode target once, after first controller center is available.
|
||||||
if (session != null && session.target == null) {
|
if (session != null && session.target == null) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user