Remove world map area

This commit is contained in:
stopflock
2025-09-28 19:00:07 -05:00
parent c9f1ecf7d0
commit 4ad33d17e0
6 changed files with 44 additions and 223 deletions
+1 -2
View File
@@ -2,8 +2,6 @@
import 'package:flutter/material.dart';
/// Developer/build-time configuration for global/non-user-tunable constants.
const int kWorldMinZoom = 1;
const int kWorldMaxZoom = 5;
// Example: Default tile storage estimate (KB per tile), for size estimates
const double kTileEstimateKb = 25.0;
@@ -66,6 +64,7 @@ const int kMaxUserDownloadZoomSpan = 7;
// Download area limits and constants
const int kMaxReasonableTileCount = 20000;
const int kAbsoluteMaxTileCount = 50000;
const int kAbsoluteMaxZoom = 19;
// Camera icon configuration
@@ -49,7 +49,7 @@ class _OfflineAreasSectionState extends State<OfflineAreasSection> {
: '--';
String subtitle = '${locService.t('offlineAreas.provider')}: ${area.tileProviderDisplay}\n' +
locService.t('offlineAreas.zoomLevels', params: [area.minZoom.toString(), area.maxZoom.toString()]) + '\n' +
'Max zoom: Z${area.maxZoom}' + '\n' +
'${locService.t('offlineAreas.latitude')}: ${area.bounds.southWest.latitude.toStringAsFixed(3)}, ${area.bounds.southWest.longitude.toStringAsFixed(3)}\n' +
'${locService.t('offlineAreas.latitude')}: ${area.bounds.northEast.latitude.toStringAsFixed(3)}, ${area.bounds.northEast.longitude.toStringAsFixed(3)}';
@@ -59,9 +59,7 @@ class _OfflineAreasSectionState extends State<OfflineAreasSection> {
subtitle += '\n${locService.t('offlineAreas.tiles')}: ${area.tilesTotal}';
}
subtitle += '\n${locService.t('offlineAreas.size')}: $diskStr';
if (!area.isPermanent) {
subtitle += '\n${locService.t('offlineAreas.cameras')}: ${area.nodes.length}';
}
subtitle += '\n${locService.t('offlineAreas.cameras')}: ${area.nodes.length}';
return Card(
child: ListTile(
leading: Icon(area.status == OfflineAreaStatus.complete
@@ -76,10 +74,9 @@ class _OfflineAreasSectionState extends State<OfflineAreasSection> {
? area.name
: locService.t('offlineAreas.areaIdFallback', params: [area.id.substring(0, 6)])),
),
if (!area.isPermanent)
IconButton(
icon: const Icon(Icons.edit, size: 20),
tooltip: locService.t('offlineAreas.renameArea'),
IconButton(
icon: const Icon(Icons.edit, size: 20),
tooltip: locService.t('offlineAreas.renameArea'),
onPressed: () async {
String? newName = await showDialog<String>(
context: context,
@@ -116,29 +113,7 @@ class _OfflineAreasSectionState extends State<OfflineAreasSection> {
}
},
),
if (area.isPermanent && area.status != OfflineAreaStatus.downloading)
IconButton(
icon: const Icon(Icons.refresh, color: Colors.blue),
tooltip: locService.t('offlineAreas.refreshWorldTiles'),
onPressed: () async {
await service.downloadArea(
id: area.id,
bounds: area.bounds,
minZoom: area.minZoom,
maxZoom: area.maxZoom,
directory: area.directory,
name: area.name,
onProgress: (progress) {},
onComplete: (status) {},
tileProviderId: area.tileProviderId,
tileProviderName: area.tileProviderName,
tileTypeId: area.tileTypeId,
tileTypeName: area.tileTypeName,
);
setState(() {});
},
)
else if (!area.isPermanent && area.status != OfflineAreaStatus.downloading)
if (area.status != OfflineAreaStatus.downloading)
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
tooltip: locService.t('offlineAreas.deleteOfflineArea'),
+22 -6
View File
@@ -7,7 +7,7 @@ import 'package:path_provider/path_provider.dart';
import 'offline_areas/offline_area_models.dart';
import 'offline_areas/offline_tile_utils.dart';
import 'offline_areas/offline_area_downloader.dart';
import 'offline_areas/world_area_manager.dart';
import '../models/osm_camera_node.dart';
import '../app_state.dart';
import 'map_data_provider.dart';
@@ -59,8 +59,7 @@ class OfflineAreaService {
if (_initialized) return;
await _loadAreasFromDisk();
await WorldAreaManager.ensureWorldArea(_areas, getOfflineAreaDir, downloadArea);
await saveAreasToDisk(); // Save any world area updates
await _cleanupLegacyWorldAreas();
_initialized = true;
}
@@ -262,9 +261,6 @@ class OfflineAreaService {
}
_areas.remove(area);
await saveAreasToDisk();
if (area.isPermanent) {
await WorldAreaManager.ensureWorldArea(_areas, getOfflineAreaDir, downloadArea);
}
}
void deleteArea(String id) async {
@@ -277,5 +273,25 @@ class OfflineAreaService {
await saveAreasToDisk();
}
/// Remove any legacy world areas from previous versions
Future<void> _cleanupLegacyWorldAreas() async {
final worldAreas = _areas.where((area) => area.isPermanent || area.id == 'world').toList();
if (worldAreas.isNotEmpty) {
debugPrint('OfflineAreaService: Cleaning up ${worldAreas.length} legacy world area(s)');
for (final area in worldAreas) {
final dir = Directory(area.directory);
if (await dir.exists()) {
await dir.delete(recursive: true);
debugPrint('OfflineAreaService: Deleted world area directory: ${area.directory}');
}
_areas.remove(area);
}
await saveAreasToDisk();
debugPrint('OfflineAreaService: Legacy world area cleanup complete');
}
}
}
@@ -10,7 +10,6 @@ import '../../models/osm_camera_node.dart';
import '../map_data_provider.dart';
import 'offline_area_models.dart';
import 'offline_tile_utils.dart';
import 'package:deflockapp/dev_config.dart';
/// Handles the actual downloading process for offline areas
class OfflineAreaDownloader {
@@ -27,12 +26,7 @@ class OfflineAreaDownloader {
required Future<void> Function() saveAreasToDisk,
required Future<void> Function(OfflineArea) getAreaSizeBytes,
}) async {
Set<List<int>> allTiles;
if (area.isPermanent) {
allTiles = computeTileList(globalWorldBounds(), kWorldMinZoom, kWorldMaxZoom);
} else {
allTiles = computeTileList(bounds, minZoom, maxZoom);
}
Set<List<int>> allTiles = computeTileList(bounds, minZoom, maxZoom);
area.tilesTotal = allTiles.length;
// Download tiles with retry logic
@@ -45,17 +39,13 @@ class OfflineAreaDownloader {
getAreaSizeBytes: getAreaSizeBytes,
);
// Download nodes for non-permanent areas
if (!area.isPermanent) {
await _downloadNodes(
area: area,
bounds: bounds,
minZoom: minZoom,
directory: directory,
);
} else {
area.nodes = [];
}
// Download nodes for all areas
await _downloadNodes(
area: area,
bounds: bounds,
minZoom: minZoom,
directory: directory,
);
return success;
}
@@ -1,153 +0,0 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
import 'package:path_provider/path_provider.dart';
import 'offline_area_models.dart';
import 'offline_tile_utils.dart';
import 'package:deflockapp/dev_config.dart';
/// Manages the world area (permanent offline area for base map)
class WorldAreaManager {
static const String _worldAreaId = 'world';
static const String _worldAreaName = 'World Base Map';
/// Ensure world area exists and check if download is needed
static Future<OfflineArea> ensureWorldArea(
List<OfflineArea> areas,
Future<Directory> Function() getOfflineAreaDir,
Future<void> Function({
required String id,
required LatLngBounds bounds,
required int minZoom,
required int maxZoom,
required String directory,
String? name,
String? tileProviderId,
String? tileProviderName,
String? tileTypeId,
String? tileTypeName,
}) downloadArea,
) async {
// Find existing world area
OfflineArea? world;
for (final area in areas) {
if (area.isPermanent) {
world = area;
break;
}
}
// Create world area if it doesn't exist, or update existing area without provider info
if (world == null) {
final appDocDir = await getOfflineAreaDir();
final dir = "${appDocDir.path}/$_worldAreaId";
world = OfflineArea(
id: _worldAreaId,
name: _worldAreaName,
bounds: globalWorldBounds(),
minZoom: kWorldMinZoom,
maxZoom: kWorldMaxZoom,
directory: dir,
status: OfflineAreaStatus.downloading,
isPermanent: true,
// World area always uses OpenStreetMap
tileProviderId: 'openstreetmap',
tileProviderName: 'OpenStreetMap',
tileTypeId: 'osm_street',
tileTypeName: 'Street Map',
);
areas.insert(0, world);
} else if (world.tileProviderId == null || world.tileTypeId == null) {
// Update existing world area that lacks provider metadata
final updatedWorld = OfflineArea(
id: world.id,
name: world.name,
bounds: world.bounds,
minZoom: world.minZoom,
maxZoom: world.maxZoom,
directory: world.directory,
status: world.status,
progress: world.progress,
tilesDownloaded: world.tilesDownloaded,
tilesTotal: world.tilesTotal,
nodes: world.nodes,
sizeBytes: world.sizeBytes,
isPermanent: world.isPermanent,
// Add missing provider metadata
tileProviderId: 'openstreetmap',
tileProviderName: 'OpenStreetMap',
tileTypeId: 'osm_street',
tileTypeName: 'Street Map',
);
final index = areas.indexOf(world);
areas[index] = updatedWorld;
world = updatedWorld;
}
// Check world area status and start download if needed
await _checkAndStartWorldDownload(world, downloadArea);
return world;
}
/// Check world area download status and start if needed
static Future<void> _checkAndStartWorldDownload(
OfflineArea world,
Future<void> Function({
required String id,
required LatLngBounds bounds,
required int minZoom,
required int maxZoom,
required String directory,
String? name,
String? tileProviderId,
String? tileProviderName,
String? tileTypeId,
String? tileTypeName,
}) downloadArea,
) async {
if (world.status == OfflineAreaStatus.complete) return;
// Count existing tiles
final expectedTiles = computeTileList(
globalWorldBounds(),
kWorldMinZoom,
kWorldMaxZoom,
);
int filesFound = 0;
for (final tile in expectedTiles) {
final file = File('${world.directory}/tiles/${tile[0]}/${tile[1]}/${tile[2]}.png');
if (file.existsSync()) {
filesFound++;
}
}
// Update world area stats
world.tilesTotal = expectedTiles.length;
world.tilesDownloaded = filesFound;
world.progress = (world.tilesTotal == 0) ? 0.0 : (filesFound / world.tilesTotal);
if (filesFound == world.tilesTotal) {
world.status = OfflineAreaStatus.complete;
debugPrint('WorldAreaManager: World area download already complete.');
} else {
world.status = OfflineAreaStatus.downloading;
debugPrint('WorldAreaManager: Starting world area download. ${world.tilesDownloaded}/${world.tilesTotal} tiles found.');
// Start download (fire and forget) - use OSM for world areas
downloadArea(
id: world.id,
bounds: world.bounds,
minZoom: world.minZoom,
maxZoom: world.maxZoom,
directory: world.directory,
name: world.name,
tileProviderId: 'openstreetmap',
tileProviderName: 'OpenStreetMap',
tileTypeId: 'osm_street',
tileTypeName: 'Street Map',
);
}
}
}
+7 -13
View File
@@ -73,12 +73,12 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
});
}
/// Calculate the maximum zoom level that keeps tile count under the limit
/// Calculate the maximum zoom level that keeps tile count under the absolute limit
int _calculateMaxZoomForTileLimit(LatLngBounds bounds, int minZoom) {
for (int zoom = minZoom; zoom <= kAbsoluteMaxZoom; zoom++) {
final tileCount = computeTileList(bounds, minZoom, zoom).length;
if (tileCount > kMaxReasonableTileCount) {
// Return the previous zoom level that was still under the limit
if (tileCount > kAbsoluteMaxTileCount) {
// Return the previous zoom level that was still under the absolute limit
return math.max(minZoom, zoom - 1);
}
}
@@ -155,14 +155,6 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
),
],
),
if (_minZoom != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(locService.t('download.minZoom')),
Text('Z$_minZoom'),
],
),
if (_maxPossibleZoom != null && _tileCount != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
@@ -178,7 +170,9 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
locService.t('download.maxRecommendedZoom', params: [_maxPossibleZoom.toString()]),
_tileCount! > kMaxReasonableTileCount
? 'Above recommended limit (Z${_maxPossibleZoom})'
: locService.t('download.maxRecommendedZoom', params: [_maxPossibleZoom.toString()]),
style: TextStyle(
fontSize: 12,
color: _tileCount! > kMaxReasonableTileCount
@@ -190,7 +184,7 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
const SizedBox(height: 2),
Text(
_tileCount! > kMaxReasonableTileCount
? locService.t('download.exceedsTileLimit', params: [kMaxReasonableTileCount.toString()])
? 'Current selection exceeds ${kMaxReasonableTileCount} recommended tile limit but is within ${kAbsoluteMaxTileCount} absolute limit'
: locService.t('download.withinTileLimit', params: [kMaxReasonableTileCount.toString()]),
style: TextStyle(
fontSize: 11,