mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +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:
@@ -12,6 +12,8 @@ enum MapLayerType {
|
|||||||
// Kept for stored preference compatibility after MBTiles removal.
|
// Kept for stored preference compatibility after MBTiles removal.
|
||||||
vectorMbtiles,
|
vectorMbtiles,
|
||||||
wmsBase,
|
wmsBase,
|
||||||
|
// Appended last: stored layer preference is the enum index.
|
||||||
|
mapyOutdoor,
|
||||||
}
|
}
|
||||||
|
|
||||||
class MapLayer {
|
class MapLayer {
|
||||||
@@ -21,6 +23,9 @@ class MapLayer {
|
|||||||
final String attribution;
|
final String attribution;
|
||||||
final double maxZoom;
|
final double maxZoom;
|
||||||
|
|
||||||
|
/// Extra HTTP headers required by the tile server (e.g. Referer).
|
||||||
|
final Map<String, String>? headers;
|
||||||
|
|
||||||
// WMS specific properties
|
// WMS specific properties
|
||||||
final bool isWms;
|
final bool isWms;
|
||||||
final String? wmsBaseUrl;
|
final String? wmsBaseUrl;
|
||||||
@@ -36,6 +41,7 @@ class MapLayer {
|
|||||||
required this.urlTemplate,
|
required this.urlTemplate,
|
||||||
required this.attribution,
|
required this.attribution,
|
||||||
required this.maxZoom,
|
required this.maxZoom,
|
||||||
|
this.headers,
|
||||||
this.isWms = false,
|
this.isWms = false,
|
||||||
this.wmsBaseUrl,
|
this.wmsBaseUrl,
|
||||||
this.wmsLayers,
|
this.wmsLayers,
|
||||||
@@ -64,6 +70,9 @@ class MapLayer {
|
|||||||
case MapLayerType.vectorMbtiles:
|
case MapLayerType.vectorMbtiles:
|
||||||
// Legacy value kept only for preference migration compatibility.
|
// Legacy value kept only for preference migration compatibility.
|
||||||
return name;
|
return name;
|
||||||
|
case MapLayerType.mapyOutdoor:
|
||||||
|
// Brand name, not translated.
|
||||||
|
return name;
|
||||||
case MapLayerType.wmsBase:
|
case MapLayerType.wmsBase:
|
||||||
// For WMS layers, use the name (will be localized separately)
|
// For WMS layers, use the name (will be localized separately)
|
||||||
return name;
|
return name;
|
||||||
@@ -86,6 +95,17 @@ class MapLayer {
|
|||||||
maxZoom: 17.49, // OpenTopoMap maximum (just below level 18)
|
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(
|
static const esriWorldImagery = MapLayer(
|
||||||
type: MapLayerType.esriWorldImagery,
|
type: MapLayerType.esriWorldImagery,
|
||||||
name: 'ESRI Satellite',
|
name: 'ESRI Satellite',
|
||||||
@@ -160,6 +180,7 @@ class MapLayer {
|
|||||||
static const List<MapLayer> allLayers = [
|
static const List<MapLayer> allLayers = [
|
||||||
openStreetMap,
|
openStreetMap,
|
||||||
openTopoMap,
|
openTopoMap,
|
||||||
|
mapyOutdoor,
|
||||||
esriWorldImagery,
|
esriWorldImagery,
|
||||||
googleHybrid,
|
googleHybrid,
|
||||||
googleRoadmap,
|
googleRoadmap,
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ class OfflineTilesProvider extends ChangeNotifier {
|
|||||||
maxZoom: _maxZoom,
|
maxZoom: _maxZoom,
|
||||||
urlTemplate: _selectedLayer.urlTemplate,
|
urlTemplate: _selectedLayer.urlTemplate,
|
||||||
displayName: _selectedLayer.name,
|
displayName: _selectedLayer.name,
|
||||||
|
headers: _selectedLayer.headers,
|
||||||
);
|
);
|
||||||
|
|
||||||
await for (final event in stream) {
|
await for (final event in stream) {
|
||||||
|
|||||||
@@ -63,14 +63,31 @@ class MapTab extends StatefulWidget {
|
|||||||
|
|
||||||
class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||||
final MapController _mapController = MapController();
|
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(
|
static final TileProvider _tileProvider = NetworkTileProvider(
|
||||||
cachingProvider: OfflineMapCachingProvider(
|
cachingProvider: _tileCachingProvider,
|
||||||
BuiltInMapCachingProvider.getOrCreateInstance(
|
|
||||||
maxCacheSize: 10_000_000_000,
|
|
||||||
overrideFreshAge: const Duration(days: 365),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
// 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
|
// DO NOT create a new LocationTrackingService instance here
|
||||||
// Use the singleton from AppProvider instead via _locationService getter
|
// Use the singleton from AppProvider instead via _locationService getter
|
||||||
final MapMarkerService _markerService = MapMarkerService();
|
final MapMarkerService _markerService = MapMarkerService();
|
||||||
@@ -2974,7 +2991,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
else if (!_currentLayer.isWms)
|
else if (!_currentLayer.isWms)
|
||||||
flutter_map.TileLayer(
|
flutter_map.TileLayer(
|
||||||
urlTemplate: _currentLayer.urlTemplate,
|
urlTemplate: _currentLayer.urlTemplate,
|
||||||
tileProvider: _tileProvider,
|
tileProvider: _tileProviderFor(_currentLayer),
|
||||||
userAgentPackageName: 'com.meshcore.sar',
|
userAgentPackageName: 'com.meshcore.sar',
|
||||||
maxZoom: _currentLayer.maxZoom,
|
maxZoom: _currentLayer.maxZoom,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ class _OfflineMapScreenState extends State<OfflineMapScreen> {
|
|||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate: provider.selectedLayer.urlTemplate,
|
urlTemplate: provider.selectedLayer.urlTemplate,
|
||||||
|
tileProvider: NetworkTileProvider(
|
||||||
|
headers: provider.selectedLayer.headers ?? const {},
|
||||||
|
),
|
||||||
userAgentPackageName: 'com.meshcore.sar',
|
userAgentPackageName: 'com.meshcore.sar',
|
||||||
maxZoom: provider.selectedLayer.maxZoom,
|
maxZoom: provider.selectedLayer.maxZoom,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class TileDownloadService {
|
|||||||
required int maxZoom,
|
required int maxZoom,
|
||||||
required String urlTemplate,
|
required String urlTemplate,
|
||||||
String? displayName,
|
String? displayName,
|
||||||
|
Map<String, String>? headers,
|
||||||
int maxConcurrency = 6,
|
int maxConcurrency = 6,
|
||||||
int rateLimit = 30,
|
int rateLimit = 30,
|
||||||
}) {
|
}) {
|
||||||
@@ -96,6 +97,7 @@ class TileDownloadService {
|
|||||||
maxZoom: maxZoom,
|
maxZoom: maxZoom,
|
||||||
urlTemplate: urlTemplate,
|
urlTemplate: urlTemplate,
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
|
headers: headers,
|
||||||
maxConcurrency: maxConcurrency,
|
maxConcurrency: maxConcurrency,
|
||||||
rateLimit: rateLimit,
|
rateLimit: rateLimit,
|
||||||
);
|
);
|
||||||
@@ -110,6 +112,7 @@ class TileDownloadService {
|
|||||||
required int maxZoom,
|
required int maxZoom,
|
||||||
required String urlTemplate,
|
required String urlTemplate,
|
||||||
String? displayName,
|
String? displayName,
|
||||||
|
Map<String, String>? headers,
|
||||||
required int maxConcurrency,
|
required int maxConcurrency,
|
||||||
required int rateLimit,
|
required int rateLimit,
|
||||||
}) async {
|
}) async {
|
||||||
@@ -184,7 +187,7 @@ class TileDownloadService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
final future = _downloadSingleTile(tile, urlTemplate, styleHash)
|
final future = _downloadSingleTile(tile, urlTemplate, styleHash, headers)
|
||||||
.then((event) {
|
.then((event) {
|
||||||
if (!controller.isClosed) {
|
if (!controller.isClosed) {
|
||||||
controller.add(event);
|
controller.add(event);
|
||||||
@@ -220,6 +223,7 @@ class TileDownloadService {
|
|||||||
TileCoord tile,
|
TileCoord tile,
|
||||||
String urlTemplate,
|
String urlTemplate,
|
||||||
String styleHash,
|
String styleHash,
|
||||||
|
Map<String, String>? extraHeaders,
|
||||||
) async {
|
) async {
|
||||||
final bounds = TileMathService.tileBounds(tile.x, tile.y, tile.z);
|
final bounds = TileMathService.tileBounds(tile.x, tile.y, tile.z);
|
||||||
|
|
||||||
@@ -241,7 +245,7 @@ class TileDownloadService {
|
|||||||
try {
|
try {
|
||||||
final response = await _httpClient.get(
|
final response = await _httpClient.get(
|
||||||
Uri.parse(url),
|
Uri.parse(url),
|
||||||
headers: {'User-Agent': 'MeshCoreSAR/1.0'},
|
headers: {'User-Agent': 'MeshCoreSAR/1.0', ...?extraHeaders},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
|
|||||||
Reference in New Issue
Block a user