Skip to content
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
246 changes: 185 additions & 61 deletions lib/src/layer/tile_layer.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
Expand All @@ -17,6 +18,10 @@ import 'layer.dart';
/// A tile is an image binded to a specific geographical position.
class TileLayerOptions extends LayerOptions {
/// Defines the structure to create the URLs for the tiles.
/// `{s}` means one of the available subdomains (can be omitted)
/// `{z}` zoom level
/// `{x}` and `{y}` — tile coordinates
/// `{r}` can be used to add "@2x" to the URL to load retina tiles (can be omitted)
///
/// Example:
///
Expand Down Expand Up @@ -47,20 +52,20 @@ class TileLayerOptions extends LayerOptions {
/// In most tile providers goes from 0 to 19.
final double maxZoom;

// Minimum zoom number the tile source has available. If it is specified,
// the tiles on all zoom levels lower than minNativeZoom will be loaded
// from minNativeZoom level and auto-scaled.
/// Minimum zoom number the tile source has available. If it is specified,
/// the tiles on all zoom levels lower than minNativeZoom will be loaded
/// from minNativeZoom level and auto-scaled.
final double minNativeZoom;

// Maximum zoom number the tile source has available. If it is specified,
// the tiles on all zoom levels higher than maxNativeZoom will be loaded
// from maxNativeZoom level and auto-scaled.
/// Maximum zoom number the tile source has available. If it is specified,
/// the tiles on all zoom levels higher than maxNativeZoom will be loaded
/// from maxNativeZoom level and auto-scaled.
final double maxNativeZoom;

// If set to true, the zoom number used in tile URLs will be reversed (`maxZoom - zoom` instead of `zoom`)
/// If set to true, the zoom number used in tile URLs will be reversed (`maxZoom - zoom` instead of `zoom`)
final bool zoomReverse;

// The zoom number used in tile URLs will be offset with this value.
/// The zoom number used in tile URLs will be offset with this value.
final double zoomOffset;

/// List of subdomains for the URL.
Expand All @@ -80,10 +85,10 @@ class TileLayerOptions extends LayerOptions {
/// https://c.tile.openstreetmap.org/{z}/{x}/{y}.png
final List<String> subdomains;

///Color shown behind the tiles.
/// Color shown behind the tiles.
final Color backgroundColor;

///Opacity of the rendered tile
/// Opacity of the rendered tile
final double opacity;

/// Provider to load the tiles. The default is CachedNetworkTileProvider,
Expand Down Expand Up @@ -131,7 +136,7 @@ class TileLayerOptions extends LayerOptions {
///
/// TileLayerOptions(
/// urlTemplate: "https://api.tiles.mapbox.com/v4/"
/// "{id}/{z}/{x}/{y}@2x.png?access_token={accessToken}",
/// "{id}/{z}/{x}/{y}{r}.png?access_token={accessToken}",
/// additionalOptions: {
/// 'accessToken': '<PUT_ACCESS_TOKEN_HERE>',
/// 'id': 'mapbox.streets',
Expand All @@ -141,52 +146,100 @@ class TileLayerOptions extends LayerOptions {
///
final Map<String, String> additionalOptions;

// Tiles will not update more than once every `updateInterval` milliseconds
// (default 200) when panning.
// It can be 0 (but it will calculating for loading tiles every frame when panning / zooming, flutter is fast)
// This can save some fps and even bandwidth
// (ie. when fast panning / animating between long distances in short time)
/// Tiles will not update more than once every `updateInterval`
/// (default 200 milliseconds) when panning.
/// It can be null (but it will calculating for loading tiles every frame when panning / zooming, flutter is fast)
/// This can save some fps and even bandwidth
/// (ie. when fast panning / animating between long distances in short time)
final Duration updateInterval;

// Tiles fade in duration in milliseconds (default 100),
// it can 0 to avoid fade in
/// Tiles fade in duration in milliseconds (default 100),
/// it can be null to avoid fade in
final Duration tileFadeInDuration;

TileLayerOptions(
{this.urlTemplate,
this.tileSize = 256.0,
this.minZoom = 0.0,
this.maxZoom = 18.0,
this.minNativeZoom,
this.maxNativeZoom,
this.zoomReverse = false,
this.zoomOffset = 0.0,
this.additionalOptions = const <String, String>{},
this.subdomains = const <String>[],
this.keepBuffer = 2,
this.backgroundColor = const Color(0xFFE0E0E0),
this.placeholderImage,
this.errorImage,
this.tileProvider = const CachedNetworkTileProvider(),
this.tms = false,
// ignore: avoid_init_to_null
this.wmsOptions = null,
this.opacity = 1.0,
// Tiles will not update more than once every `updateInterval` milliseconds
// (default 200) when panning.
// It can be 0 (but it will calculating for loading tiles every frame when panning / zooming, flutter is fast)
// This can save some fps and even bandwidth
// (ie. when fast panning / animating between long distances in short time)
int updateInterval = 200,
// Tiles fade in duration in milliseconds (default 100),
// it can 0 to avoid fade in
int tileFadeInDuration = 100,
rebuild})
: updateInterval =
/// Opacity start value when Tile starts fade in (0.0 - 1.0)
/// Takes effect if `tileFadeInDuration` is not null
final double tileFadeInStart;

/// Opacity start value when an exists Tile starts fade in with different Url (0.0 - 1.0)
/// Takes effect when `tileFadeInDuration` is not null and if `overrideTilesWhenUrlChanges` if true
final double tileFadeInStartWhenOverride;

/// `false`: current Tiles will be first dropped and then reload via new url (default)
/// `true`: current Tiles will be visible until new ones aren't loaded (new Tiles are loaded independently)
/// @see https://github.com/johnpryan/flutter_map/issues/583
final bool overrideTilesWhenUrlChanges;

/// If `true`, it will request four tiles of half the specified size and a
/// bigger zoom level in place of one to utilize the high resolution.
///
/// If `true` then MapOptions's `maxZoom` should be `maxZoom - 1` since retinaMode
/// just simulates retina display by playing with `zoomOffset`.
/// If geoserver supports retina `@2` tiles then it it advised to use them
/// instead of simulating it (use {r} in the [urlTemplate])
///
/// It is advised to use retinaMode if display supports it, write code like this:
/// TileLayerOptions(
/// retinaMode: true && MediaQuery.of(context).devicePixelRatio > 1.0,
/// ),
final bool retinaMode;

TileLayerOptions({
this.urlTemplate,
double tileSize = 256.0,
double minZoom = 0.0,
double maxZoom = 18.0,
this.minNativeZoom,
this.maxNativeZoom,
this.zoomReverse = false,
double zoomOffset = 0.0,
this.additionalOptions = const <String, String>{},
this.subdomains = const <String>[],
this.keepBuffer = 2,
this.backgroundColor = const Color(0xFFE0E0E0),
this.placeholderImage,
this.errorImage,
this.tileProvider = const CachedNetworkTileProvider(),
this.tms = false,
// ignore: avoid_init_to_null
this.wmsOptions = null,
this.opacity = 1.0,
// Tiles will not update more than once every `updateInterval` milliseconds
// (default 200) when panning.
// It can be 0 (but it will calculating for loading tiles every frame when panning / zooming, flutter is fast)
// This can save some fps and even bandwidth
// (ie. when fast panning / animating between long distances in short time)
int updateInterval = 200,
// Tiles fade in duration in milliseconds (default 100),
// it can 0 to avoid fade in
int tileFadeInDuration = 100,
this.tileFadeInStart = 0.0,
this.tileFadeInStartWhenOverride = 0.0,
this.overrideTilesWhenUrlChanges = false,
this.retinaMode = false,
rebuild,
}) : updateInterval =
updateInterval <= 0 ? null : Duration(milliseconds: updateInterval),
tileFadeInDuration = tileFadeInDuration <= 0
? null
: Duration(milliseconds: tileFadeInDuration),
assert(tileFadeInStart >= 0.0 && tileFadeInStart <= 1.0),
assert(tileFadeInStartWhenOverride >= 0.0 &&
tileFadeInStartWhenOverride <= 1.0),
maxZoom =
wmsOptions == null && retinaMode && maxZoom > 0.0 && !zoomReverse
? maxZoom - 1.0
: maxZoom,
minZoom =
wmsOptions == null && retinaMode && maxZoom > 0.0 && zoomReverse
? math.max(minZoom + 1.0, 0.0)
: minZoom,
zoomOffset = wmsOptions == null && retinaMode && maxZoom > 0.0
? (zoomReverse ? zoomOffset - 1.0 : zoomOffset + 1.0)
: zoomOffset,
tileSize = wmsOptions == null && retinaMode && maxZoom > 0.0
? (tileSize / 2.0).floorToDouble()
: tileSize,
super(rebuild: rebuild);
}

Expand Down Expand Up @@ -253,7 +306,7 @@ class WMSTileLayerOptions {
return buffer.toString();
}

String getUrl(Coords coords, int tileSize) {
String getUrl(Coords coords, int tileSize, bool retinaMode) {
final tileSizePoint = CustomPoint(tileSize, tileSize);
final nvPoint = coords.scaleBy(tileSizePoint);
final sePoint = nvPoint + tileSizePoint;
Expand All @@ -267,8 +320,8 @@ class WMSTileLayerOptions {
: [bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y];

final buffer = StringBuffer(_encodedBaseUrl);
buffer.write('&width=$tileSize');
buffer.write('&height=$tileSize');
buffer.write('&width=${retinaMode ? tileSize * 2 : tileSize}');
buffer.write('&height=${retinaMode ? tileSize * 2 : tileSize}');
buffer.write('&bbox=${bbox.join(',')}');
return buffer.toString();
}
Expand Down Expand Up @@ -317,6 +370,53 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
_update(null);
_moveSub = widget.stream.listen((_) => _handleMove());

_initThrottleUpdate();
}

@override
void didUpdateWidget(TileLayer oldWidget) {
super.didUpdateWidget(oldWidget);
var reloadTiles = false;

if (oldWidget.options.tileSize != options.tileSize) {
_tileSize = CustomPoint(options.tileSize, options.tileSize);
reloadTiles = true;
}

if (oldWidget.options.retinaMode != options.retinaMode) {
reloadTiles = true;
}

if (oldWidget.options.updateInterval != options.updateInterval) {
_throttleUpdate?.close();
_initThrottleUpdate();
}

if (!reloadTiles) {
final oldUrl = oldWidget.options.wmsOptions?._encodedBaseUrl ??
oldWidget.options.urlTemplate;
final newUrl = options.wmsOptions?._encodedBaseUrl ?? options.urlTemplate;
if (oldUrl != newUrl) {
if (options.overrideTilesWhenUrlChanges) {
for (var tile in _tiles.values) {
tile.imageProvider = options.tileProvider
.getImage(_wrapCoords(tile.coords), options);
tile.loadTileImage();
}
} else {
reloadTiles = true;
}
}
}

if (reloadTiles) {
_removeAllTiles();
_resetView();
_update(null);
}
}

void _initThrottleUpdate() {
if (options.updateInterval == null) {
_throttleUpdate = null;
} else {
Expand All @@ -331,12 +431,12 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {

@override
void dispose() {
super.dispose();

_removeAllTiles();
_moveSub?.cancel();
options.tileProvider.dispose();
_throttleUpdate?.close();

super.dispose();
}

@override
Expand Down Expand Up @@ -822,6 +922,8 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
print(error);

tile.loadError = true;
} else {
tile.loadError = false;
}

var key = _tileCoordsToKey(coords);
Expand All @@ -830,12 +932,20 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
return;
}

var fadeInStart = tile.loaded == null
? options.tileFadeInStart
: options.tileFadeInStartWhenOverride;
tile.loaded = DateTime.now();
if (options.tileFadeInDuration == null ||
fadeInStart == 1.0 ||
(tile.loadError && null == options.errorImage)) {
tile.active = true;
} else {
tile.startFadeInAnimation(options.tileFadeInDuration, this);
tile.startFadeInAnimation(
options.tileFadeInDuration,
this,
from: fadeInStart,
);
}

setState(() {});
Expand Down Expand Up @@ -896,7 +1006,7 @@ class Tile implements Comparable<Tile> {
final String coordsKey;
final Coords<double> coords;
final CustomPoint<num> tilePos;
final ImageProvider imageProvider;
ImageProvider imageProvider;
final Level level;

bool current;
Expand Down Expand Up @@ -929,10 +1039,20 @@ class Tile implements Comparable<Tile> {
this.retain = false,
this.loadError = false,
}) {
loadTileImage();
}

void loadTileImage() {
try {
final oldImageStream = _imageStream;
_imageStream = imageProvider.resolve(ImageConfiguration());
_listener = ImageStreamListener(_tileOnLoad, onError: _tileOnError);
_imageStream.addListener(_listener);

if (_imageStream.key != oldImageStream?.key) {
oldImageStream?.removeListener(_listener);

_listener = ImageStreamListener(_tileOnLoad, onError: _tileOnError);
_imageStream.addListener(_listener);
}
} catch (e, s) {
// make sure all exception is handled - #444 / #536
_tileOnError(e, s);
Expand All @@ -952,11 +1072,14 @@ class Tile implements Comparable<Tile> {
_imageStream?.removeListener(_listener);
}

void startFadeInAnimation(Duration duration, TickerProvider vsync) {
void startFadeInAnimation(Duration duration, TickerProvider vsync,
{double from}) {
animationController?.removeStatusListener(_onAnimateEnd);

animationController = AnimationController(duration: duration, vsync: vsync)
..addStatusListener(_onAnimateEnd);

animationController.forward();
animationController.forward(from: from);
}

void _onAnimateEnd(AnimationStatus status) {
Expand All @@ -974,7 +1097,8 @@ class Tile implements Comparable<Tile> {

void _tileOnError(dynamic exception, StackTrace stackTrace) {
if (null != tileReady) {
tileReady(coords, exception, this);
tileReady(
coords, exception ?? 'Unknown exception during loadTileImage', this);
}
}

Expand Down
Loading