mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-07-17 00:21:50 +00:00
feat: Add Mapy.cz Outdoor (OSM + LIDAR) base map layer
- New MapLayerType.mapyOutdoor with Referer header support (tile server rejects requests without mapy.com Referer) - Headered layers get a dedicated NetworkTileProvider sharing the offline tile cache - Offline region downloads pass layer headers through TileDownloadService Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ba27843166
commit
365bd76cbc
@@ -12,6 +12,8 @@ enum MapLayerType {
|
||||
// Kept for stored preference compatibility after MBTiles removal.
|
||||
vectorMbtiles,
|
||||
wmsBase,
|
||||
// Appended last: stored layer preference is the enum index.
|
||||
mapyOutdoor,
|
||||
}
|
||||
|
||||
class MapLayer {
|
||||
@@ -21,6 +23,9 @@ class MapLayer {
|
||||
final String attribution;
|
||||
final double maxZoom;
|
||||
|
||||
/// Extra HTTP headers required by the tile server (e.g. Referer).
|
||||
final Map<String, String>? headers;
|
||||
|
||||
// WMS specific properties
|
||||
final bool isWms;
|
||||
final String? wmsBaseUrl;
|
||||
@@ -36,6 +41,7 @@ class MapLayer {
|
||||
required this.urlTemplate,
|
||||
required this.attribution,
|
||||
required this.maxZoom,
|
||||
this.headers,
|
||||
this.isWms = false,
|
||||
this.wmsBaseUrl,
|
||||
this.wmsLayers,
|
||||
@@ -64,6 +70,9 @@ class MapLayer {
|
||||
case MapLayerType.vectorMbtiles:
|
||||
// Legacy value kept only for preference migration compatibility.
|
||||
return name;
|
||||
case MapLayerType.mapyOutdoor:
|
||||
// Brand name, not translated.
|
||||
return name;
|
||||
case MapLayerType.wmsBase:
|
||||
// For WMS layers, use the name (will be localized separately)
|
||||
return name;
|
||||
@@ -86,6 +95,17 @@ class MapLayer {
|
||||
maxZoom: 17.49, // OpenTopoMap maximum (just below level 18)
|
||||
);
|
||||
|
||||
/// Mapy.cz outdoor map: OSM data with LIDAR-based shaded relief.
|
||||
/// The tile server rejects requests without a mapy.com Referer.
|
||||
static const mapyOutdoor = MapLayer(
|
||||
type: MapLayerType.mapyOutdoor,
|
||||
name: 'Mapy.cz Outdoor (OSM + LIDAR)',
|
||||
urlTemplate: 'https://mapserver.mapy.cz/turist-en/retina/{z}-{x}-{y}',
|
||||
attribution: '© Seznam.cz, a.s., © OpenStreetMap contributors',
|
||||
maxZoom: 19,
|
||||
headers: {'Referer': 'https://mapy.com/'},
|
||||
);
|
||||
|
||||
static const esriWorldImagery = MapLayer(
|
||||
type: MapLayerType.esriWorldImagery,
|
||||
name: 'ESRI Satellite',
|
||||
@@ -160,6 +180,7 @@ class MapLayer {
|
||||
static const List<MapLayer> allLayers = [
|
||||
openStreetMap,
|
||||
openTopoMap,
|
||||
mapyOutdoor,
|
||||
esriWorldImagery,
|
||||
googleHybrid,
|
||||
googleRoadmap,
|
||||
|
||||
@@ -266,6 +266,7 @@ class OfflineTilesProvider extends ChangeNotifier {
|
||||
maxZoom: _maxZoom,
|
||||
urlTemplate: _selectedLayer.urlTemplate,
|
||||
displayName: _selectedLayer.name,
|
||||
headers: _selectedLayer.headers,
|
||||
);
|
||||
|
||||
await for (final event in stream) {
|
||||
|
||||
@@ -63,14 +63,31 @@ class MapTab extends StatefulWidget {
|
||||
|
||||
class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
final MapController _mapController = MapController();
|
||||
static final MapCachingProvider _tileCachingProvider =
|
||||
OfflineMapCachingProvider(
|
||||
BuiltInMapCachingProvider.getOrCreateInstance(
|
||||
maxCacheSize: 10_000_000_000,
|
||||
overrideFreshAge: const Duration(days: 365),
|
||||
),
|
||||
);
|
||||
static final TileProvider _tileProvider = NetworkTileProvider(
|
||||
cachingProvider: OfflineMapCachingProvider(
|
||||
BuiltInMapCachingProvider.getOrCreateInstance(
|
||||
maxCacheSize: 10_000_000_000,
|
||||
overrideFreshAge: const Duration(days: 365),
|
||||
),
|
||||
),
|
||||
cachingProvider: _tileCachingProvider,
|
||||
);
|
||||
// Providers for layers that need extra HTTP headers (e.g. Mapy.cz Referer),
|
||||
// cached per URL template so they are not recreated on every build.
|
||||
static final Map<String, TileProvider> _headeredTileProviders = {};
|
||||
|
||||
static TileProvider _tileProviderFor(MapLayer layer) {
|
||||
final headers = layer.headers;
|
||||
if (headers == null) return _tileProvider;
|
||||
return _headeredTileProviders.putIfAbsent(
|
||||
layer.urlTemplate,
|
||||
() => NetworkTileProvider(
|
||||
headers: headers,
|
||||
cachingProvider: _tileCachingProvider,
|
||||
),
|
||||
);
|
||||
}
|
||||
// DO NOT create a new LocationTrackingService instance here
|
||||
// Use the singleton from AppProvider instead via _locationService getter
|
||||
final MapMarkerService _markerService = MapMarkerService();
|
||||
@@ -2974,7 +2991,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
else if (!_currentLayer.isWms)
|
||||
flutter_map.TileLayer(
|
||||
urlTemplate: _currentLayer.urlTemplate,
|
||||
tileProvider: _tileProvider,
|
||||
tileProvider: _tileProviderFor(_currentLayer),
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: _currentLayer.maxZoom,
|
||||
),
|
||||
|
||||
@@ -114,6 +114,9 @@ class _OfflineMapScreenState extends State<OfflineMapScreen> {
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: provider.selectedLayer.urlTemplate,
|
||||
tileProvider: NetworkTileProvider(
|
||||
headers: provider.selectedLayer.headers ?? const {},
|
||||
),
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: provider.selectedLayer.maxZoom,
|
||||
),
|
||||
|
||||
@@ -84,6 +84,7 @@ class TileDownloadService {
|
||||
required int maxZoom,
|
||||
required String urlTemplate,
|
||||
String? displayName,
|
||||
Map<String, String>? headers,
|
||||
int maxConcurrency = 6,
|
||||
int rateLimit = 30,
|
||||
}) {
|
||||
@@ -96,6 +97,7 @@ class TileDownloadService {
|
||||
maxZoom: maxZoom,
|
||||
urlTemplate: urlTemplate,
|
||||
displayName: displayName,
|
||||
headers: headers,
|
||||
maxConcurrency: maxConcurrency,
|
||||
rateLimit: rateLimit,
|
||||
);
|
||||
@@ -110,6 +112,7 @@ class TileDownloadService {
|
||||
required int maxZoom,
|
||||
required String urlTemplate,
|
||||
String? displayName,
|
||||
Map<String, String>? headers,
|
||||
required int maxConcurrency,
|
||||
required int rateLimit,
|
||||
}) async {
|
||||
@@ -184,7 +187,7 @@ class TileDownloadService {
|
||||
break;
|
||||
}
|
||||
|
||||
final future = _downloadSingleTile(tile, urlTemplate, styleHash)
|
||||
final future = _downloadSingleTile(tile, urlTemplate, styleHash, headers)
|
||||
.then((event) {
|
||||
if (!controller.isClosed) {
|
||||
controller.add(event);
|
||||
@@ -220,6 +223,7 @@ class TileDownloadService {
|
||||
TileCoord tile,
|
||||
String urlTemplate,
|
||||
String styleHash,
|
||||
Map<String, String>? extraHeaders,
|
||||
) async {
|
||||
final bounds = TileMathService.tileBounds(tile.x, tile.y, tile.z);
|
||||
|
||||
@@ -241,7 +245,7 @@ class TileDownloadService {
|
||||
try {
|
||||
final response = await _httpClient.get(
|
||||
Uri.parse(url),
|
||||
headers: {'User-Agent': 'MeshCoreSAR/1.0'},
|
||||
headers: {'User-Agent': 'MeshCoreSAR/1.0', ...?extraHeaders},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
|
||||
Reference in New Issue
Block a user