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

Refactor tile removal #1596

Merged
merged 10 commits into from
Jul 24, 2023
84 changes: 69 additions & 15 deletions example/lib/pages/tile_loading_error_handle.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/widgets/drawer.dart';
Expand All @@ -13,55 +16,62 @@ class TileLoadingErrorHandle extends StatefulWidget {
}

class _TileLoadingErrorHandleState extends State<TileLoadingErrorHandle> {
static const _showSnackBarDuration = Duration(seconds: 1);
bool _simulateTileLoadErrors = false;
DateTime? _lastShowedTileLoadError;

@override
Widget build(BuildContext context) {
var needLoadingError = true;

return Scaffold(
appBar: AppBar(title: const Text('Tile Loading Error Handle')),
drawer: buildDrawer(context, TileLoadingErrorHandle.route),
body: Padding(
padding: const EdgeInsets.all(8),
child: Column(
children: [
SwitchListTile(
title: const Text('Simulate tile loading errors'),
value: _simulateTileLoadErrors,
onChanged: (newValue) => setState(() {
_simulateTileLoadErrors = newValue;
}),
),
const Padding(
padding: EdgeInsets.only(top: 8, bottom: 8),
child: Text('Turn on Airplane mode and try to move or zoom map'),
child: Text(
'Enable tile load error simulation or disable internet and try to move or zoom map.'),
),
Flexible(
child: Builder(builder: (BuildContext context) {
return FlutterMap(
options: MapOptions(
initialCenter: const LatLng(51.5, -0.09),
options: const MapOptions(
initialCenter: LatLng(51.5, -0.09),
initialZoom: 5,
onPositionChanged: (MapPosition mapPosition, bool _) {
needLoadingError = true;
},
),
children: [
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',

// For example purposes. It is recommended to use
// TileProvider with a caching and retry strategy, like
// NetworkTileProvider or CachedNetworkTileProvider
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
evictErrorTileStrategy: EvictErrorTileStrategy.none,
errorTileCallback: (tile, error, stackTrace) {
if (needLoadingError) {
if (_showErrorSnackBar) {
_lastShowedTileLoadError = DateTime.now();
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
duration: const Duration(seconds: 1),
duration: _showSnackBarDuration,
content: Text(
error.toString(),
style: const TextStyle(color: Colors.black),
),
backgroundColor: Colors.deepOrange,
));
});
needLoadingError = false;
}
},
tileProvider: _simulateTileLoadErrors
? _SimulateErrorsTileProvider()
: null,
),
],
);
Expand All @@ -72,4 +82,48 @@ class _TileLoadingErrorHandleState extends State<TileLoadingErrorHandle> {
),
);
}

bool get _showErrorSnackBar =>
_lastShowedTileLoadError == null ||
DateTime.now().difference(_lastShowedTileLoadError!) -
const Duration(milliseconds: 50) >
_showSnackBarDuration;
}

class _SimulateErrorsTileProvider extends TileProvider {
_SimulateErrorsTileProvider() : super();

@override
ImageProvider<Object> getImage(
TileCoordinates coordinates,
TileLayer options,
) =>
_SimulateErrorImageProvider();
}

class _SimulateErrorImageProvider
extends ImageProvider<_SimulateErrorImageProvider> {
_SimulateErrorImageProvider();

@override
ImageStreamCompleter load(
_SimulateErrorImageProvider key,
Future<ui.Codec> Function(
Uint8List, {
bool allowUpscaling,
int? cacheHeight,
int? cacheWidth,
}) decode,
) =>
_SimulateErrorImageStreamCompleter();

@override
Future<_SimulateErrorImageProvider> obtainKey(ImageConfiguration _) =>
Future.error('Simulated tile loading error');
}

class _SimulateErrorImageStreamCompleter extends ImageStreamCompleter {
_SimulateErrorImageStreamCompleter() {
throw 'Simulated tile loading error';
}
}
18 changes: 10 additions & 8 deletions lib/src/layer/tile_layer/tile_coordinates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,7 @@ class TileCoordinates extends Point<int> {
@override
String toString() => 'TileCoordinate($x, $y, $z)';

@override
bool operator ==(Object other) {
if (other is! TileCoordinates) return false;

return x == other.x && y == other.y && z == other.z;
}

// Overriden because Point's distanceTo does not allow comparing with a point
// Overridden because Point's distanceTo does not allow comparing with a point
// of a different type.
@override
double distanceTo(Point<num> other) {
Expand All @@ -26,6 +19,15 @@ class TileCoordinates extends Point<int> {
return sqrt(dx * dx + dy * dy);
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is TileCoordinates &&
other.x == x &&
other.y == y &&
other.z == z;
}

@override
int get hashCode => Object.hash(x.hashCode, y.hashCode, z.hashCode);
}
100 changes: 51 additions & 49 deletions lib/src/layer/tile_layer/tile_image.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart';
import 'package:flutter_map/src/layer/tile_layer/tile_display.dart';
import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart';

class TileImage extends ChangeNotifier {
bool _disposed = false;

// Controls fade-in opacity.
AnimationController? _animationController;

// Whether the tile is displayable. See [readyToDisplay].
bool _readyToDisplay = false;

/// Used by animationController. Still required if animation is disabled in
/// case the tile display is changed at a later point.
final TickerProvider vsync;
Expand All @@ -14,8 +19,6 @@ class TileImage extends ChangeNotifier {
/// indicate the position of the tile at that zoom level.
final TileCoordinates coordinates;

AnimationController? _animationController;

/// Callback fired when loading finishes with or withut an error. This
/// callback is not triggered after this TileImage is disposed.
final void Function(TileCoordinates coordinates) onLoadComplete;
Expand All @@ -27,29 +30,14 @@ class TileImage extends ChangeNotifier {
onLoadError;

/// Options for how the tile image is displayed.
TileDisplay _display;
TileDisplay _tileDisplay;

/// An optional image to show when a loading error occurs.
final ImageProvider? errorImage;

ImageProvider imageProvider;

/// Current tiles are tiles which are in the current tile zoom AND:
/// * Are visible OR,
/// * Were previously visible and are still within the visible bounds
/// expanded by the [TileLayer.keepBuffer].
bool current = true;

/// Used during pruning to determine which tiles should be kept.
bool retain = false;

/// Whether the tile is displayable. This means that either:
/// * Loading errored but there is a tile error image.
/// * Loading succeeded and the fade animation has finished.
/// * Loading succeeded and there is no fade animation.
bool _active = false;

// True if an error occurred during loading.
/// True if an error occurred during loading.
bool loadError = false;

/// When loading started.
Expand All @@ -70,7 +58,7 @@ class TileImage extends ChangeNotifier {
required this.onLoadError,
required TileDisplay tileDisplay,
required this.errorImage,
}) : _display = tileDisplay,
}) : _tileDisplay = tileDisplay,
_animationController = tileDisplay.when(
instantaneous: (_) => null,
fadeIn: (fadeIn) => AnimationController(
Expand All @@ -79,25 +67,33 @@ class TileImage extends ChangeNotifier {
),
);

double get opacity => _display.when(
instantaneous: (instantaneous) => _active ? instantaneous.opacity : 0.0,
double get opacity => _tileDisplay.when(
instantaneous: (instantaneous) =>
_readyToDisplay ? instantaneous.opacity : 0.0,
fadeIn: (fadeIn) => _animationController!.value,
)!;

AnimationController? get animation => _animationController;

String get coordinatesKey => coordinates.key;

bool get active => _active;
/// Whether the tile is displayable. This means that either:
/// * Loading errored but an error image is configured.
/// * Loading succeeded and the fade animation has finished.
/// * Loading succeeded and there is no fade animation.
///
/// Note that [opacity] can be less than 1 when this is true if instantaneous
/// tile display is used with a maximum opacity less than 1.
bool get readyToDisplay => _readyToDisplay;

// Used to sort TileImages by their distance from the current zoom.
double zIndex(double maxZoom, int currentZoom) =>
maxZoom - (currentZoom - coordinates.z).abs();

// Change the tile display options.
set tileDisplay(TileDisplay newTileDisplay) {
final oldTileDisplay = _display;
_display = newTileDisplay;
final oldTileDisplay = _tileDisplay;
_tileDisplay = newTileDisplay;

// Handle disabling/enabling of animation controller if necessary
oldTileDisplay.when(
Expand All @@ -108,7 +104,7 @@ class TileImage extends ChangeNotifier {
_animationController = AnimationController(
duration: fadeIn.duration,
vsync: vsync,
value: _active ? 1.0 : 0.0,
value: _readyToDisplay ? 1.0 : 0.0,
);
},
);
Expand Down Expand Up @@ -156,7 +152,7 @@ class TileImage extends ChangeNotifier {
this.imageInfo = imageInfo;

if (!_disposed) {
_activate();
_display();
onLoadComplete(coordinates);
}
}
Expand All @@ -165,42 +161,49 @@ class TileImage extends ChangeNotifier {
loadError = true;

if (!_disposed) {
_activate();
if (errorImage != null) _display();
onLoadError(this, exception, stackTrace);
onLoadComplete(coordinates);
}
}

void _activate() {
// Initiates fading in and marks this TileImage as readyToDisplay when fading
// finishes. If fading is disabled or a loading error occurred this TileImage
// becomes readyToDisplay immediately.
void _display() {
final previouslyLoaded = loadFinishedAt != null;
loadFinishedAt = DateTime.now();

_display.when(
if (loadError) {
assert(
errorImage != null,
'A TileImage should not be displayed if loading errors and there is no '
'errorImage to show.',
);
_readyToDisplay = true;
if (!_disposed) notifyListeners();
return;
}

_tileDisplay.when(
instantaneous: (_) {
_active = true;
_readyToDisplay = true;
if (!_disposed) notifyListeners();
},
fadeIn: (fadeIn) {
if (loadError && errorImage != null) {
_active = true;
if (!_disposed) notifyListeners();
return;
}

final fadeStartOpacity =
previouslyLoaded ? fadeIn.reloadStartOpacity : fadeIn.startOpacity;

if (fadeStartOpacity == 1.0) {
_active = true;
_readyToDisplay = true;
if (!_disposed) notifyListeners();
return;
} else {
_animationController!.reset();
_animationController!.forward(from: fadeStartOpacity).then((_) {
_readyToDisplay = true;
if (!_disposed) notifyListeners();
});
}

_animationController!.reset();
_animationController!.forward(from: fadeStartOpacity).then((_) {
_active = true;
if (!_disposed) notifyListeners();
});
},
);
}
Expand All @@ -223,8 +226,7 @@ class TileImage extends ChangeNotifier {
}
}

// Mark the image as inactive.
_active = false;
_readyToDisplay = false;
_animationController?.stop(canceled: false);
_animationController?.value = 0.0;
notifyListeners();
Expand All @@ -244,6 +246,6 @@ class TileImage extends ChangeNotifier {

@override
String toString() {
return 'TileImage($coordinates, active: $_active)';
return 'TileImage($coordinates, readyToDisplay: $_readyToDisplay)';
}
}
Loading
Loading