Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Fallback URLs (for #1203) #1348

Merged
merged 7 commits into from
Sep 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart';
import 'package:flutter_map_example/pages/epsg3413_crs.dart';
import 'package:flutter_map_example/pages/epsg4326_crs.dart';
import 'package:flutter_map_example/pages/esri.dart';
import 'package:flutter_map_example/pages/fallback_url_network_page.dart';
import 'package:flutter_map_example/pages/fallback_url_offline_page.dart';
import 'package:flutter_map_example/pages/home.dart';
import 'package:flutter_map_example/pages/interactive_test_page.dart';
import 'package:flutter_map_example/pages/latlng_to_screen_point.dart';
Expand Down Expand Up @@ -87,6 +89,10 @@ class MyApp extends StatelessWidget {
PointToLatLngPage.route: (context) => const PointToLatLngPage(),
LatLngScreenPointTestPage.route: (context) =>
const LatLngScreenPointTestPage(),
FallbackUrlNetworkPage.route: (context) =>
const FallbackUrlNetworkPage(),
FallbackUrlOfflinePage.route: (context) =>
const FallbackUrlOfflinePage(),
},
);
}
Expand Down
57 changes: 57 additions & 0 deletions example/lib/pages/fallback_url/fallback_url.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/widgets/drawer.dart';
import 'package:latlong2/latlong.dart';

class FallbackUrlPage extends StatelessWidget {
final String route;
final TileLayer tileLayer;
final String title;
final String description;
final double zoom;
final double? maxZoom;
final double? minZoom;
final LatLng center;

const FallbackUrlPage({
Key? key,
required this.route,
required this.tileLayer,
required this.title,
required this.description,
required this.center,
this.zoom = 13,
this.maxZoom,
this.minZoom,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
drawer: buildDrawer(context, route),
body: Padding(
padding: const EdgeInsets.all(8),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Text(description),
),
Flexible(
child: FlutterMap(
options: MapOptions(
center: center,
zoom: zoom,
maxZoom: maxZoom,
minZoom: minZoom,
),
children: [tileLayer],
),
),
],
),
),
);
}
}
28 changes: 28 additions & 0 deletions example/lib/pages/fallback_url_network_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/pages/fallback_url/fallback_url.dart';
import 'package:latlong2/latlong.dart';

class FallbackUrlNetworkPage extends StatelessWidget {
static const String route = '/fallback_url_network';

const FallbackUrlNetworkPage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return FallbackUrlPage(
route: route,
tileLayer: TileLayer(
urlTemplate: 'https://fake-tile-provider.org/{z}/{x}/{y}.png',
fallbackUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
subdomains: const ['a', 'b', 'c'],
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
),
title: 'Fallback URL NetworkTileProvider',
description:
'Map with a fake url should use the fallback, showing (51.5, -0.9).',
zoom: 5,
center: LatLng(51.5, -0.09),
);
}
}
29 changes: 29 additions & 0 deletions example/lib/pages/fallback_url_offline_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/pages/fallback_url/fallback_url.dart';
import 'package:latlong2/latlong.dart';

class FallbackUrlOfflinePage extends StatelessWidget {
static const String route = '/fallback_url_offline';

const FallbackUrlOfflinePage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return FallbackUrlPage(
route: route,
tileLayer: TileLayer(
tileProvider: AssetTileProvider(),
maxZoom: 14,
urlTemplate: 'assets/fake/tiles/{z}/{x}/{y}.png',
fallbackUrl: 'assets/map/anholt_osmbright/{z}/{x}/{y}.png',
),
title: 'Fallback URL AssetTileProvider',
description:
'Map with a fake asset path, should be using the fallback to show Anholt Island, Denmark.',
maxZoom: 14,
minZoom: 12,
center: LatLng(56.704173, 11.543808),
);
}
}
1 change: 0 additions & 1 deletion example/lib/pages/offline_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class OfflineMapPage extends StatelessWidget {
center: LatLng(56.704173, 11.543808),
minZoom: 12,
maxZoom: 14,
zoom: 13,
swPanBoundary: LatLng(56.6877, 11.5089),
nePanBoundary: LatLng(56.7378, 11.6644),
),
Expand Down
14 changes: 14 additions & 0 deletions example/lib/widgets/drawer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart';
import 'package:flutter_map_example/pages/epsg3413_crs.dart';
import 'package:flutter_map_example/pages/epsg4326_crs.dart';
import 'package:flutter_map_example/pages/esri.dart';
import 'package:flutter_map_example/pages/fallback_url_network_page.dart';
import 'package:flutter_map_example/pages/fallback_url_offline_page.dart';
import 'package:flutter_map_example/pages/home.dart';
import 'package:flutter_map_example/pages/interactive_test_page.dart';
import 'package:flutter_map_example/pages/latlng_to_screen_point.dart';
Expand Down Expand Up @@ -257,6 +259,18 @@ Drawer buildDrawer(BuildContext context, String currentRoute) {
PointToLatLngPage.route, currentRoute),
_buildMenuItem(context, const Text('LatLng to ScreenPoint'),
LatLngScreenPointTestPage.route, currentRoute),
_buildMenuItem(
context,
const Text('Fallback URL NetworkTileProvider'),
FallbackUrlNetworkPage.route,
currentRoute,
),
_buildMenuItem(
context,
const Text('Fallback URL AssetTileProvider'),
FallbackUrlOfflinePage.route,
currentRoute,
),
],
),
);
Expand Down
1 change: 1 addition & 0 deletions lib/flutter_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export 'package:flutter_map/src/layer/tile_layer/tile.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_builder.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_layer.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_provider/base_tile_provider.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_provider/asset_tile_provider.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_provider/file_tile_provider_io.dart'
if (dart.library.html) 'package:flutter_map/src/layer/tile_layer/tile_provider/file_tile_provider_web.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_provider/tile_provider_io.dart'
Expand Down
8 changes: 8 additions & 0 deletions lib/src/layer/tile_layer/tile_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class TileLayer extends StatefulWidget {
/// https://a.tile.openstreetmap.org/12/2177/1259.png
final String? urlTemplate;

/// Follows the same structure as [urlTemplate]. If specified, this URL is
/// used only if an error occurs when loading the [urlTemplate].
final String? fallbackUrl;

/// If `true`, inverses Y axis numbering for tiles (turn this on for
/// [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services).
final bool tms;
Expand Down Expand Up @@ -239,6 +243,7 @@ class TileLayer extends StatefulWidget {
TileLayer({
super.key,
this.urlTemplate,
this.fallbackUrl,
double tileSize = 256.0,
double minZoom = 0.0,
double maxZoom = 18.0,
Expand Down Expand Up @@ -340,6 +345,9 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
if (widget.reset != null) {
_resetSub = widget.reset?.listen((_) => _resetTiles());
}

//TODO fix
// _initThrottleUpdate();
}

@override
Expand Down
48 changes: 48 additions & 0 deletions lib/src/layer/tile_layer/tile_provider/asset_tile_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_map/src/layer/tile_layer/coords.dart';
import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart';
import 'package:flutter_map/src/layer/tile_layer/tile_provider/base_tile_provider.dart';

class AssetTileProvider extends TileProvider {
@override
AssetImage getImage(Coords<num> coords, TileLayer options) {
return AssetImage(
getTileUrl(coords, options),
bundle: _FlutterMapAssetBundle(
fallbackKey: getTileFallbackUrl(coords, options),
),
);
}
}

/// Used to load a fallback asset when the main asset is not found.
class _FlutterMapAssetBundle extends CachingAssetBundle {
final String? fallbackKey;

_FlutterMapAssetBundle({required this.fallbackKey});

Future<ByteData?> _loadAsset(String key) async {
final Uint8List encoded =
utf8.encoder.convert(Uri(path: Uri.encodeFull(key)).path);
final ByteData? asset = await ServicesBinding
.instance.defaultBinaryMessenger
.send('flutter/assets', encoded.buffer.asByteData());
return asset;
}

@override
Future<ByteData> load(String key) async {
final asset = await _loadAsset(key);
if (asset != null && asset.lengthInBytes > 0) return asset;

if (fallbackKey != null) {
final fallbackAsset = await _loadAsset(fallbackKey!);
if (fallbackAsset != null) return fallbackAsset;
}

throw FlutterError('_FlutterMapAssetBundle - Unable to load asset: $key');
}
}
28 changes: 20 additions & 8 deletions lib/src/layer/tile_layer/tile_provider/base_tile_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,7 @@ abstract class TileProvider {
/// Called when the [TileLayerWidget] is disposed
void dispose() {}

/// Generate a valid URL for a tile, based on it's coordinates and the current [TileLayerOptions]
String getTileUrl(Coords coords, TileLayer options) {
final urlTemplate = (options.wmsOptions != null)
? options.wmsOptions!
.getUrl(coords, options.tileSize.toInt(), options.retinaMode)
: options.urlTemplate;

String _getTileUrl(String urlTemplate, Coords coords, TileLayer options) {
final z = _getZoomForUrl(coords, options);

final data = <String, String>{
Expand All @@ -43,7 +37,25 @@ abstract class TileProvider {
}
final allOpts = Map<String, String>.from(data)
..addAll(options.additionalOptions);
return options.templateFunction(urlTemplate!, allOpts);
return options.templateFunction(urlTemplate, allOpts);
}

/// Generate a valid URL for a tile, based on it's coordinates and the current
/// [TileLayerOptions]
String getTileUrl(Coords coords, TileLayer options) {
final urlTemplate = (options.wmsOptions != null)
? options.wmsOptions!
.getUrl(coords, options.tileSize.toInt(), options.retinaMode)
: options.urlTemplate;

return _getTileUrl(urlTemplate!, coords, options);
}

/// Generates a valid URL for the [fallbackUrl].
String? getTileFallbackUrl(Coords coords, TileLayer options) {
final urlTemplate = options.fallbackUrl;
if (urlTemplate == null) return null;
return _getTileUrl(urlTemplate, coords, options);
}

double _getZoomForUrl(Coords coords, TileLayer options) {
Expand Down
42 changes: 28 additions & 14 deletions lib/src/layer/tile_layer/tile_provider/network_image_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,29 @@ import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:http/http.dart';
import 'package:http/http.dart' as http;
import 'package:http/retry.dart';

class FMNetworkImageProvider extends ImageProvider<FMNetworkImageProvider> {
/// The URL from which the image will be fetched.
final String url;

/// The http RetryClient that is used for the requests
final RetryClient retryClient;
/// The fallback URL from which the image will be fetched.
final String? fallbackUrl;

/// The http client that is used for the requests. Defaults to a [RetryClient]
/// with a [http.Client].
final http.Client httpClient;

/// Custom headers to add to the image fetch request
final Map<String, String> headers;

FMNetworkImageProvider(
this.url, {
RetryClient? retryClient,
required this.fallbackUrl,
http.Client? httpClient,
this.headers = const {},
}) : retryClient = retryClient ?? RetryClient(Client());
}) : httpClient = httpClient ?? RetryClient(http.Client());

@override
ImageStreamCompleter loadBuffer(
Expand All @@ -38,22 +43,31 @@ class FMNetworkImageProvider extends ImageProvider<FMNetworkImageProvider> {

Future<ImageInfo> _loadWithRetry(
FMNetworkImageProvider key,
DecoderBufferCallback decode,
) async {
DecoderBufferCallback decode, [
bool useFallback = false,
]) async {
assert(key == this);
assert(useFallback == false || fallbackUrl != null);

final uri = Uri.parse(url);
final response = await retryClient.get(uri, headers: headers);
try {
final uri = Uri.parse(useFallback ? fallbackUrl! : url);
final response = await httpClient.get(uri, headers: headers);

if (response.statusCode != 200) {
throw NetworkImageLoadException(
statusCode: response.statusCode, uri: uri);
}
if (response.statusCode != 200) {
throw NetworkImageLoadException(
statusCode: response.statusCode, uri: uri);
}

final codec =
await decode(await ImmutableBuffer.fromUint8List(response.bodyBytes));
final image = (await codec.getNextFrame()).image;

return ImageInfo(image: image);
return ImageInfo(image: image);
} catch (e) {
if (!useFallback && fallbackUrl != null) {
return _loadWithRetry(key, decode, true);
}
rethrow;
}
}
}
Loading