mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-03-22 19:04:15 +00:00
- Add ServicePolicy framework with OSM-specific rate limiting and TTL - Add per-provider disk tile cache (ProviderTileCacheStore) with O(1) lookup, oldest-modified eviction, and ETag/304 revalidation - Rewrite DeflockTileProvider with two paths: common (NetworkTileProvider) and offline-first (disk cache -> local tiles -> network with caching) - Add zoom-aware offline routing so tiles outside offline area zoom ranges use the efficient common path instead of the overhead-heavy offline path - Fix HTTP client lifecycle: dispose() is now a no-op for flutter_map widget recycling; shutdown() handles permanent teardown - Add TileLayerManager with exponential backoff retry (2s->60s cap), provider switch detection, and backoff reset - Guard null provider/tileType in download dialog with localized error - Fix Nominatim cache key to use normalized viewbox values - Comprehensive test coverage (1800+ lines across 6 test files) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100 lines
3.6 KiB
Dart
100 lines
3.6 KiB
Dart
import 'dart:math';
|
|
import 'package:latlong2/latlong.dart';
|
|
import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
|
|
|
|
/// Utility for tile calculations and lat/lon conversions for OSM offline logic
|
|
|
|
/// Normalize bounds so south ≤ north, west ≤ east, and degenerate (near-zero)
|
|
/// spans are expanded by epsilon. Call this before storing bounds so that
|
|
/// `tileInBounds` and [computeTileList] see consistent corner ordering.
|
|
LatLngBounds normalizeBounds(LatLngBounds bounds) {
|
|
const double epsilon = 1e-7;
|
|
var latMin = min(bounds.southWest.latitude, bounds.northEast.latitude);
|
|
var latMax = max(bounds.southWest.latitude, bounds.northEast.latitude);
|
|
var lonMin = min(bounds.southWest.longitude, bounds.northEast.longitude);
|
|
var lonMax = max(bounds.southWest.longitude, bounds.northEast.longitude);
|
|
if ((latMax - latMin).abs() < epsilon) {
|
|
latMin -= epsilon;
|
|
latMax += epsilon;
|
|
}
|
|
if ((lonMax - lonMin).abs() < epsilon) {
|
|
lonMin -= epsilon;
|
|
lonMax += epsilon;
|
|
}
|
|
return LatLngBounds(LatLng(latMin, lonMin), LatLng(latMax, lonMax));
|
|
}
|
|
|
|
Set<List<int>> computeTileList(LatLngBounds bounds, int zMin, int zMax) {
|
|
Set<List<int>> tiles = {};
|
|
final normalized = normalizeBounds(bounds);
|
|
final double latMin = normalized.south;
|
|
final double latMax = normalized.north;
|
|
final double lonMin = normalized.west;
|
|
final double lonMax = normalized.east;
|
|
for (int z = zMin; z <= zMax; z++) {
|
|
final n = pow(2, z).toInt();
|
|
final minTileRaw = latLonToTileRaw(latMin, lonMin, z);
|
|
final maxTileRaw = latLonToTileRaw(latMax, lonMax, z);
|
|
int minX = min(minTileRaw[0].floor(), maxTileRaw[0].floor()) - 1;
|
|
int maxX = max(minTileRaw[0].ceil() - 1, maxTileRaw[0].ceil() - 1) + 1;
|
|
int minY = min(minTileRaw[1].floor(), maxTileRaw[1].floor()) - 1;
|
|
int maxY = max(minTileRaw[1].ceil() - 1, maxTileRaw[1].ceil() - 1) + 1;
|
|
minX = minX.clamp(0, n - 1);
|
|
maxX = maxX.clamp(0, n - 1);
|
|
minY = minY.clamp(0, n - 1);
|
|
maxY = maxY.clamp(0, n - 1);
|
|
for (int x = minX; x <= maxX; x++) {
|
|
for (int y = minY; y <= maxY; y++) {
|
|
tiles.add([z, x, y]);
|
|
}
|
|
}
|
|
}
|
|
return tiles;
|
|
}
|
|
|
|
List<double> latLonToTileRaw(double lat, double lon, int zoom) {
|
|
final n = pow(2.0, zoom);
|
|
final xtile = (lon + 180.0) / 360.0 * n;
|
|
final ytile = (1.0 -
|
|
log(tan(lat * pi / 180.0) + 1.0 / cos(lat * pi / 180.0)) / pi) / 2.0 * n;
|
|
return [xtile, ytile];
|
|
}
|
|
|
|
List<int> latLonToTile(double lat, double lon, int zoom) {
|
|
final n = pow(2.0, zoom);
|
|
final xtile = ((lon + 180.0) / 360.0 * n).floor();
|
|
final ytile = ((1.0 - log(tan(lat * pi / 180.0) + 1.0 / cos(lat * pi / 180.0)) / pi) / 2.0 * n).floor();
|
|
return [xtile, ytile];
|
|
}
|
|
|
|
/// Convert tile coordinates back to LatLng bounds
|
|
LatLngBounds tileToLatLngBounds(int x, int y, int z) {
|
|
final n = pow(2, z);
|
|
|
|
// Calculate bounds for this tile
|
|
final lonWest = x / n * 360.0 - 180.0;
|
|
final lonEast = (x + 1) / n * 360.0 - 180.0;
|
|
|
|
// For latitude, we need to invert the mercator projection
|
|
final latNorthRad = atan(sinh(pi * (1 - 2 * y / n)));
|
|
final latSouthRad = atan(sinh(pi * (1 - 2 * (y + 1) / n)));
|
|
|
|
final latNorth = latNorthRad * 180.0 / pi;
|
|
final latSouth = latSouthRad * 180.0 / pi;
|
|
|
|
return LatLngBounds(
|
|
LatLng(latSouth, lonWest), // SW corner
|
|
LatLng(latNorth, lonEast), // NE corner
|
|
);
|
|
}
|
|
|
|
/// Hyperbolic sine function: sinh(x) = (e^x - e^(-x)) / 2
|
|
double sinh(double x) {
|
|
return (exp(x) - exp(-x)) / 2;
|
|
}
|
|
|
|
LatLngBounds globalWorldBounds() {
|
|
// Use slightly shrunken bounds to avoid tile index overflow at extreme coordinates
|
|
return LatLngBounds(LatLng(-85.0, -179.9), LatLng(85.0, 179.9));
|
|
}
|