Preview tile fetching on startup, offline area refresh, more cameras->nodes

This commit is contained in:
stopflock
2025-09-29 21:28:40 -05:00
parent 1140e6300a
commit 2b26bf9188
12 changed files with 403 additions and 27 deletions
+109
View File
@@ -272,6 +272,115 @@ class OfflineAreaService {
_areas.remove(area);
await saveAreasToDisk();
}
/// Refresh/update an existing offline area - tiles, nodes, or both
Future<void> refreshArea({
required String id,
required bool refreshTiles,
required bool refreshNodes,
void Function(double progress)? onProgress,
void Function(OfflineAreaStatus status)? onComplete,
}) async {
final area = _areas.firstWhere((a) => a.id == id, orElse: () => throw 'Area not found');
if (area.status == OfflineAreaStatus.downloading) {
throw 'Area is already downloading';
}
// Set area to downloading state
area.status = OfflineAreaStatus.downloading;
area.progress = 0.0;
area.tilesDownloaded = 0;
await saveAreasToDisk();
try {
bool success = true;
if (refreshTiles && refreshNodes) {
// Refresh both - use the full download process
success = await OfflineAreaDownloader.downloadArea(
area: area,
bounds: area.bounds,
minZoom: area.minZoom,
maxZoom: area.maxZoom,
directory: area.directory,
onProgress: onProgress,
saveAreasToDisk: saveAreasToDisk,
getAreaSizeBytes: getAreaSizeBytes,
);
} else if (refreshTiles) {
// Refresh tiles only
success = await _refreshTilesOnly(area, onProgress);
} else if (refreshNodes) {
// Refresh nodes only
success = await _refreshNodesOnly(area, onProgress);
} else {
// Neither option selected - shouldn't happen but handle gracefully
success = true;
area.progress = 1.0;
}
await getAreaSizeBytes(area);
if (success) {
area.status = OfflineAreaStatus.complete;
area.progress = 1.0;
debugPrint('Area $id: refresh completed successfully.');
} else {
area.status = OfflineAreaStatus.error;
debugPrint('Area $id: refresh failed after maximum retry attempts.');
}
await saveAreasToDisk();
onComplete?.call(area.status);
} catch (e) {
area.status = OfflineAreaStatus.error;
await saveAreasToDisk();
onComplete?.call(area.status);
debugPrint('Area $id: refresh failed with exception: $e');
}
}
/// Refresh only the tiles for an area
Future<bool> _refreshTilesOnly(OfflineArea area, void Function(double progress)? onProgress) async {
final allTiles = computeTileList(area.bounds, area.minZoom, area.maxZoom);
area.tilesTotal = allTiles.length;
return await OfflineAreaDownloader.downloadTilesWithRetry(
area: area,
allTiles: allTiles,
directory: area.directory,
onProgress: onProgress,
saveAreasToDisk: saveAreasToDisk,
getAreaSizeBytes: getAreaSizeBytes,
);
}
/// Refresh only the nodes for an area
Future<bool> _refreshNodesOnly(OfflineArea area, void Function(double progress)? onProgress) async {
try {
// Use the same logic as in the downloader for consistency
final nodeZoom = (area.minZoom + 1).clamp(8, 16);
final expandedNodeBounds = OfflineAreaDownloader.calculateNodeBounds(area.bounds, nodeZoom);
final nodes = await MapDataProvider().getAllNodesForDownload(
bounds: expandedNodeBounds,
profiles: AppState.instance.profiles,
);
area.nodes = nodes;
await OfflineAreaDownloader.saveNodes(nodes, area.directory);
// Set progress to complete for nodes-only refresh
onProgress?.call(1.0);
area.progress = 1.0;
debugPrint('Area ${area.id}: Refreshed ${nodes.length} nodes');
return true;
} catch (e) {
debugPrint('Area ${area.id}: Failed to refresh nodes: $e');
return false;
}
}
/// Remove any legacy world areas from previous versions
Future<void> _cleanupLegacyWorldAreas() async {
@@ -30,7 +30,7 @@ class OfflineAreaDownloader {
area.tilesTotal = allTiles.length;
// Download tiles with retry logic
final success = await _downloadTilesWithRetry(
final success = await downloadTilesWithRetry(
area: area,
allTiles: allTiles,
directory: directory,
@@ -51,7 +51,7 @@ class OfflineAreaDownloader {
}
/// Download tiles with retry logic
static Future<bool> _downloadTilesWithRetry({
static Future<bool> downloadTilesWithRetry({
required OfflineArea area,
required Set<List<int>> allTiles,
required String directory,
@@ -138,7 +138,7 @@ class OfflineAreaDownloader {
// Modest expansion: use tiles at minZoom + 1 instead of minZoom
// This gives a reasonable buffer without capturing entire states
final nodeZoom = (minZoom + 1).clamp(8, 16); // Reasonable bounds for node fetching
final expandedNodeBounds = _calculateNodeBounds(bounds, nodeZoom);
final expandedNodeBounds = calculateNodeBounds(bounds, nodeZoom);
final nodes = await MapDataProvider().getAllNodesForDownload(
bounds: expandedNodeBounds,
@@ -150,7 +150,7 @@ class OfflineAreaDownloader {
}
/// Calculate expanded bounds that cover the entire tile area at minimum zoom
static LatLngBounds _calculateNodeBounds(LatLngBounds visibleBounds, int minZoom) {
static LatLngBounds calculateNodeBounds(LatLngBounds visibleBounds, int minZoom) {
final tiles = computeTileList(visibleBounds, minZoom, minZoom);
if (tiles.isEmpty) return visibleBounds;
+80
View File
@@ -0,0 +1,80 @@
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../models/tile_provider.dart';
import '../state/settings_state.dart';
/// Service for fetching missing tile preview images
class TilePreviewService {
static const int _previewZoom = 10;
static const int _previewX = 512;
static const int _previewY = 384;
static const Duration _timeout = Duration(seconds: 10);
/// Attempt to fetch missing preview tiles for tile types that don't already have preview data
/// Fails silently - no error handling or user notification on failure
static Future<void> fetchMissingPreviews(SettingsState settingsState) async {
try {
bool anyUpdates = false;
for (final provider in settingsState.tileProviders) {
final updatedTileTypes = <TileType>[];
bool providerNeedsUpdate = false;
for (final tileType in provider.tileTypes) {
// Only fetch if preview tile is missing
if (tileType.previewTile == null) {
// Skip if tile type requires API key but provider doesn't have one
if (tileType.requiresApiKey && (provider.apiKey == null || provider.apiKey!.isEmpty)) {
updatedTileTypes.add(tileType);
continue;
}
final previewData = await _fetchPreviewForTileType(tileType, provider.apiKey);
if (previewData != null) {
// Create updated tile type with preview data
final updatedTileType = tileType.copyWith(previewTile: previewData);
updatedTileTypes.add(updatedTileType);
providerNeedsUpdate = true;
} else {
updatedTileTypes.add(tileType);
}
} else {
updatedTileTypes.add(tileType);
}
}
if (providerNeedsUpdate) {
final updatedProvider = provider.copyWith(tileTypes: updatedTileTypes);
await settingsState.addOrUpdateTileProvider(updatedProvider);
anyUpdates = true;
}
}
if (anyUpdates) {
debugPrint('TilePreviewService: Updated providers with new preview tiles');
}
} catch (e) {
// Fail silently as requested
debugPrint('TilePreviewService: Error during preview fetching: $e');
}
}
static Future<Uint8List?> _fetchPreviewForTileType(TileType tileType, String? apiKey) async {
try {
final url = tileType.getTileUrl(_previewZoom, _previewX, _previewY, apiKey: apiKey);
final response = await http.get(Uri.parse(url)).timeout(_timeout);
if (response.statusCode == 200 && response.bodyBytes.isNotEmpty) {
debugPrint('TilePreviewService: Fetched preview for ${tileType.name}');
return response.bodyBytes;
}
} catch (e) {
// Fail silently - just log for debugging
debugPrint('TilePreviewService: Failed to fetch preview for ${tileType.name}: $e');
}
return null;
}
}