fix: Prefer cached map tiles offline ref:

This commit is contained in:
Janez T
2026-04-08 09:46:31 +02:00
parent 4bd7f36b98
commit b863cda139
3 changed files with 137 additions and 34 deletions

View File

@@ -1,19 +1,15 @@
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter_map/flutter_map.dart';
import 'offline_tile_cache_service.dart';
import 'tile_sharing_service.dart';
/// A [MapCachingProvider] that checks the offline AVIF tile cache (and
/// optionally peers) before falling through to the built-in cache.
/// A [MapCachingProvider] that checks the offline AVIF tile cache before
/// falling through to the built-in cache/network path.
///
/// This allows preloaded tiles to be served during normal map browsing.
class OfflineMapCachingProvider implements MapCachingProvider {
final MapCachingProvider _delegate;
final OfflineTileCacheService _cache = OfflineTileCacheService.instance;
final TileSharingService _sharing = TileSharingService.instance;
OfflineMapCachingProvider(this._delegate);
@@ -40,28 +36,6 @@ class OfflineMapCachingProvider implements MapCachingProvider {
),
);
}
// Try peers
if (_sharing.discoveredPeers.isNotEmpty) {
final avifBytes = await _sharing.fetchFromAnyPeer(
styleHash, coords.z, coords.x, coords.y);
if (avifBytes != null) {
// Cache locally for next time
await _cache.putRawTile(
styleHash, coords.z, coords.x, coords.y, avifBytes);
final decoded = await OfflineTileCacheService.getTileAsPngStatic(avifBytes);
if (decoded != null) {
return (
bytes: decoded,
metadata: CachedMapTileMetadata(
staleAt: DateTime.now().add(const Duration(days: 365)),
lastModified: null,
etag: null,
),
);
}
}
}
}
// Fall through to delegate (built-in cache)
@@ -78,9 +52,54 @@ class OfflineMapCachingProvider implements MapCachingProvider {
return _delegate.putTile(url: url, metadata: metadata, bytes: bytes);
}
@visibleForTesting
static ({int z, int x, int y})? parseTileUrlForTesting(String url) {
final coords = _parseTileUrl(url);
if (coords == null) {
return null;
}
return (z: coords.z, x: coords.x, y: coords.y);
}
@visibleForTesting
static String extractUrlTemplateForTesting(String url) {
return _extractUrlTemplate(url);
}
/// Parse z/x/y from a tile URL.
static _TileCoords? _parseTileUrl(String url) {
// Match common patterns: /{z}/{x}/{y}.png, /tile/{z}/{y}/{x}, etc.
final queryStyleMatch = RegExp(
r'(?:\?|&|/)(?:[^#]*&)?x=(\d+)&y=(\d+)&z=(\d+)(?:&|$)',
).firstMatch(url);
if (queryStyleMatch != null) {
return _TileCoords(
z: int.parse(queryStyleMatch.group(3)!),
x: int.parse(queryStyleMatch.group(1)!),
y: int.parse(queryStyleMatch.group(2)!),
);
}
final uri = Uri.tryParse(url);
if (uri != null) {
final z = int.tryParse(uri.queryParameters['z'] ?? '');
final x = int.tryParse(uri.queryParameters['x'] ?? '');
final y = int.tryParse(uri.queryParameters['y'] ?? '');
if (z != null && x != null && y != null) {
return _TileCoords(z: z, x: x, y: y);
}
}
// Match known path formats.
final yxTilePattern = RegExp(r'/tile/(\d+)/(\d+)/(\d+)$');
final yxTileMatch = yxTilePattern.firstMatch(url);
if (yxTileMatch != null) {
return _TileCoords(
z: int.parse(yxTileMatch.group(1)!),
x: int.parse(yxTileMatch.group(3)!),
y: int.parse(yxTileMatch.group(2)!),
);
}
final patterns = [
RegExp(r'/(\d+)/(\d+)/(\d+)\.(?:png|jpg|jpeg|webp)'),
RegExp(r'/(\d+)/(\d+)/(\d+)$'),
@@ -101,7 +120,53 @@ class OfflineMapCachingProvider implements MapCachingProvider {
/// Extract a URL template from a concrete URL by replacing coordinates.
static String _extractUrlTemplate(String url) {
// Replace the last three numeric path segments with placeholders
final queryStylePattern = RegExp(r'([?&])x=\d+&y=\d+&z=\d+');
if (queryStylePattern.hasMatch(url)) {
return url.replaceAllMapped(
queryStylePattern,
(match) => '${match.group(1)}x={x}&y={y}&z={z}',
);
}
final pathStyleQueryPattern = RegExp(r'(&)x=\d+&y=\d+&z=\d+');
if (pathStyleQueryPattern.hasMatch(url)) {
return url.replaceAllMapped(
pathStyleQueryPattern,
(match) => '${match.group(1)}x={x}&y={y}&z={z}',
);
}
final uri = Uri.tryParse(url);
if (uri != null) {
final query = Map<String, String>.from(uri.queryParameters);
var replacedQuery = false;
if (query.containsKey('z')) {
query['z'] = '{z}';
replacedQuery = true;
}
if (query.containsKey('x')) {
query['x'] = '{x}';
replacedQuery = true;
}
if (query.containsKey('y')) {
query['y'] = '{y}';
replacedQuery = true;
}
if (replacedQuery) {
return uri.replace(queryParameters: query).toString();
}
}
final yxTilePattern = RegExp(r'/tile/(\d+)/(\d+)/(\d+)$');
final yxTileMatch = yxTilePattern.firstMatch(url);
if (yxTileMatch != null) {
return url.replaceRange(
yxTileMatch.start,
yxTileMatch.end,
'/tile/{z}/{y}/{x}',
);
}
return url.replaceAllMapped(
RegExp(r'/(\d+)/(\d+)/(\d+)(\.(?:png|jpg|jpeg|webp))?$'),
(m) => '/{z}/{x}/{y}${m.group(4) ?? ''}',