Skip to content

Commit 1c23823

Browse files
committed
Basic modern raster tile layer working
1 parent 85912ba commit 1c23823

File tree

8 files changed

+127
-45
lines changed

8 files changed

+127
-45
lines changed

example/lib/pages/home.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ class _HomePageState extends State<HomePage> {
3838
initialZoom: 5,
3939
),
4040
children: [
41-
openStreetMapTileLayer,
41+
RasterTileLayer.simple(
42+
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
43+
uaIdentifier: 'dev.fleaflet.flutter_map.demo',
44+
),
4245
RichAttributionWidget(
4346
popupInitialDisplayDuration: const Duration(seconds: 5),
4447
animationConfig: const ScaleRAWA(),

lib/flutter_map.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart';
2929
export 'package:flutter_map/src/layer/attribution_layer/simple.dart';
3030
export 'package:flutter_map/src/layer/circle_layer/circle_layer.dart';
3131
export 'package:flutter_map/src/layer/marker_layer/marker_layer.dart';
32+
export 'package:flutter_map/src/layer/modern_tile_layer/source_generators/source_generator.dart';
33+
export 'package:flutter_map/src/layer/modern_tile_layer/source_generators/wms.dart';
34+
export 'package:flutter_map/src/layer/modern_tile_layer/source_generators/xyz.dart';
35+
export 'package:flutter_map/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart';
3236
export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart';
3337
export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/caching_provider.dart';
3438
export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart';

lib/src/layer/modern_tile_layer/base_tile_data.dart

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
import 'dart:async';
22

33
import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart';
4+
import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart';
45
import 'package:meta/meta.dart';
56

6-
/// Data associated with a particular tile coordinate which 'loads'
7-
/// asynchronously.
8-
///
9-
/// These are generated by the [BaseTileLayer.tileLoader] and consumed by the
7+
/// Data associated with a particular tile coordinate, which allows
8+
/// bi-directional communication between the [TileLoader] and
109
/// [BaseTileLayer.renderer].
1110
///
12-
/// Association with a tile coordinate is made in the tile layer.
13-
///
14-
/// It is up to the implementation as to what 'loads' means. However, the
15-
/// [BaseTileLayer] will use [whenLoaded], [isLoaded], and [dispose] to manage
16-
/// (such as pruning) the tile for the renderer.
11+
/// The tile layer's internal logic consumes this interface's fields, and can
12+
/// also manipulate the renderer.
1713
abstract interface class BaseTileData {
18-
/// Completes when the underlying resource is 'loaded'
19-
Future<void> get whenLoaded;
20-
21-
/// Whether the underlying resource is 'loaded'
22-
bool get isLoaded;
14+
/// Should be completed when the tile is fully optically visible: the tile
15+
/// layer will start a prune of eligible tiles, and this tile will become
16+
/// eligible for pruning.
17+
///
18+
/// If the tile never becomes fully optically visible, this shouldn't usually
19+
/// be completed.
20+
Future<void> get triggerPrune;
2321

24-
/// Called when a tile is removed from the map of visible tiles
22+
/// Called when a tile is removed from the map of visible tiles passed to the
23+
/// renderer.
2524
///
2625
/// This should usually be used to abort loading of the underlying resource
2726
/// if it has not yet loaded, or release the resources held by it if already
2827
/// loaded.
2928
///
30-
/// This should not usually be called externally.
29+
/// If called, then it is assumed that the tile layer's internal logic no
30+
/// longer cares about any other field in the object.
3131
@internal
3232
void dispose();
3333
}
@@ -54,7 +54,7 @@ class WrapperTileData<D extends Object?> implements BaseTileData {
5454
///
5555
/// This never completes if the data completes to an error.
5656
@override
57-
Future<D> get whenLoaded => _loadedTracker.future;
57+
Future<D> get triggerPrune => _loadedTracker.future;
5858

5959
/// Whether [data] represents the loaded data
6060
@override

lib/src/layer/modern_tile_layer/base_tile_layer.dart

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:collection';
23

34
import 'package:flutter/widgets.dart';
@@ -66,8 +67,9 @@ class _BaseTileLayerState<D extends BaseTileData>
6667
final key = (coordinates: coordinates, layerKey: layerKey);
6768
tiles.putIfAbsent(
6869
key,
69-
() => widget.tileLoader(coordinates, widget.options)
70-
..whenLoaded.then((_) => _pruneOnLoadedTile(key)),
70+
() => _TileDataWithPrunableIndicator(
71+
widget.tileLoader(coordinates, widget.options),
72+
)..triggerPrune.then((_) => _pruneOnLoadedTile(key)),
7173
// TODO: Consider how to handle errors
7274
);
7375
}
@@ -90,7 +92,7 @@ class _BaseTileLayerState<D extends BaseTileData>
9092
visibleTileCoordinates.contains(tile.key.coordinates) &&
9193
tile.key.layerKey == layerKey,
9294
)
93-
.every((tile) => tile.value.isLoaded);
95+
.every((tile) => tile.value.isPrunable);
9496
if (allLoaded) {
9597
tiles.removeWhere(
9698
(key, _) => !visibleTileCoordinates.contains(key.coordinates),
@@ -101,7 +103,7 @@ class _BaseTileLayerState<D extends BaseTileData>
101103
context,
102104
layerKey,
103105
widget.options,
104-
Map.unmodifiable(tiles),
106+
Map.unmodifiable(tiles.map((k, v) => MapEntry(k, v._data))),
105107
);
106108
}
107109

@@ -112,7 +114,7 @@ class _BaseTileLayerState<D extends BaseTileData>
112114
// Remove all identical tiles of other (old) keys. aka replace my ancestor
113115
tiles.removeWhere(
114116
(otherKey, otherData) =>
115-
otherData.isLoaded &&
117+
otherData.isPrunable &&
116118
otherKey.coordinates == key.coordinates &&
117119
otherKey.layerKey != key.layerKey,
118120
);
@@ -123,7 +125,7 @@ class _BaseTileLayerState<D extends BaseTileData>
123125
// or not
124126
tiles.removeWhere(
125127
(otherKey, otherData) =>
126-
otherData.isLoaded && otherKey.coordinates == childCoordinates,
128+
otherData.isPrunable && otherKey.coordinates == childCoordinates,
127129
);
128130
}
129131

@@ -144,7 +146,7 @@ class _BaseTileLayerState<D extends BaseTileData>
144146
siblingCoordinates.contains(other.key.coordinates) &&
145147
other.key.layerKey == key.layerKey,
146148
)
147-
.every((other) => other.value.isLoaded);
149+
.every((other) => other.value.isPrunable);
148150

149151
if (allLoaded) {
150152
// Prune parent if me and my siblings are all loaded
@@ -235,17 +237,34 @@ extension _ParentChildTraversal on TileCoordinates {
235237
typedef _TileKey = ({TileCoordinates coordinates, Object layerKey});
236238

237239
extension type _TilesTracker<D extends BaseTileData>._(
238-
SplayTreeMap<_TileKey, D> map) implements SplayTreeMap<_TileKey, D> {
240+
Map<_TileKey, _TileDataWithPrunableIndicator<D>> _map)
241+
implements Map<_TileKey, _TileDataWithPrunableIndicator<D>> {
239242
_TilesTracker()
240243
: this._(
241-
SplayTreeMap<_TileKey, D>(
242-
(a, b) =>
244+
Map<_TileKey, _TileDataWithPrunableIndicator<D>>(
245+
/*(a, b) =>
243246
a.coordinates.z.compareTo(b.coordinates.z) |
244247
a.coordinates.x.compareTo(b.coordinates.x) |
245-
a.coordinates.y.compareTo(b.coordinates.y),
246-
),
248+
a.coordinates.y.compareTo(b.coordinates.y),*/
249+
),
247250
);
248251

249252
@redeclare
250-
D? remove(Object? key) => map.remove(key)?..dispose();
253+
D? remove(Object? key) => (_map.remove(key)?..dispose())?._data;
254+
}
255+
256+
class _TileDataWithPrunableIndicator<D extends BaseTileData> {
257+
_TileDataWithPrunableIndicator(D data) : _data = data {
258+
_data.triggerPrune.then((_) => isPrunable = true);
259+
}
260+
261+
final D _data;
262+
Future<void> get triggerPrune => _data.triggerPrune;
263+
void dispose() => _data.dispose();
264+
265+
/// `true` when [BaseTileData.triggerPrune] has completed.
266+
///
267+
/// Triggering pruning implies being fully loaded and therefore ready for self
268+
/// pruning.
269+
bool isPrunable = false;
251270
}

lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/widgets.dart';
2+
import 'package:flutter_map/flutter_map.dart';
23
import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart';
34
import 'package:flutter_map/src/layer/modern_tile_layer/options.dart';
45
import 'package:flutter_map/src/layer/modern_tile_layer/source_generators/source_generator.dart';
@@ -9,6 +10,7 @@ import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/raster/tile_
910
import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/raster/tile_loader.dart';
1011
import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart';
1112
import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart';
13+
import 'package:flutter_map/src/layer/tile_layer/tile_scale_calculator.dart';
1214

1315
class RasterTileLayer extends StatefulWidget {
1416
const RasterTileLayer({
@@ -39,7 +41,7 @@ class _RasterTileLayerState extends State<RasterTileLayer> {
3941
Widget build(BuildContext context) => BaseTileLayer(
4042
options: widget.options,
4143
tileLoader: RasterTileLoader(
42-
sourceGenerator: const XYZSourceGenerator(uriTemplates: ['']),
44+
sourceGenerator: widget.sourceGenerator,
4345
bytesFetcher: widget.bytesFetcher,
4446
),
4547
renderer: (context, layerKey, options, visibleTiles) => _RasterRenderer(
@@ -69,19 +71,36 @@ class __RasterRendererState extends State<_RasterRenderer> {
6971
//final Map<({TileCoordinates coordinates, Object layerKey}),
7072
// TileData<Uint8List>> visibleTiles = {};
7173

74+
final _tileScaleCalculator =
75+
TileScaleCalculator(crs: Epsg3857(), tileDimension: 256);
76+
7277
@override
7378
void didUpdateWidget(covariant _RasterRenderer oldWidget) {
7479
super.didUpdateWidget(oldWidget);
7580
}
7681

7782
@override
7883
Widget build(BuildContext context) {
84+
final map = MapCamera.of(context);
85+
86+
_tileScaleCalculator.clearCacheUnlessZoomMatches(map.zoom);
87+
7988
return CustomPaint(
8089
size: Size.infinite,
8190
willChange: true,
8291
painter: _RasterPainter(
8392
options: widget.options,
84-
visibleTiles: widget.visibleTiles,
93+
visibleTiles: widget.visibleTiles.entries.map(
94+
(tile) => (
95+
coordinates: tile.key.coordinates,
96+
scaledTileDimension: _tileScaleCalculator.scaledTileDimension(
97+
map.zoom,
98+
tile.key.coordinates.z,
99+
),
100+
currentPixelOrigin: map.pixelOrigin,
101+
data: tile.value,
102+
),
103+
),
85104
//tiles: tiles..sort(renderOrder),
86105
//tilePaint: widget.tilePaint,
87106
//tileOverlayPainter: widget.tileOverlayPainter,
@@ -92,24 +111,53 @@ class __RasterRendererState extends State<_RasterRenderer> {
92111

93112
class _RasterPainter extends CustomPainter {
94113
final TileLayerOptions options;
95-
final Map<({TileCoordinates coordinates, Object layerKey}), RasterTileData>
96-
visibleTiles;
114+
final Iterable<
115+
({
116+
TileCoordinates coordinates,
117+
double scaledTileDimension,
118+
Offset currentPixelOrigin,
119+
RasterTileData data
120+
})> visibleTiles;
97121

98122
_RasterPainter({
99123
super.repaint,
100124
required this.options,
101125
required this.visibleTiles,
102126
});
103127

128+
final Paint _basePaint = (null ?? Paint())
129+
..filterQuality = FilterQuality.high
130+
..isAntiAlias = true;
131+
104132
@override
105133
void paint(Canvas canvas, Size size) {
106-
for (final MapEntry(key: (:coordinates, layerKey: _), value: tile)
107-
in visibleTiles.entries) {}
134+
for (final tile in visibleTiles) {
135+
if (tile.data.loaded?.successfulImageInfo?.image case final image?) {
136+
final origin = Offset(
137+
tile.coordinates.x * tile.scaledTileDimension -
138+
tile.currentPixelOrigin.dx,
139+
tile.coordinates.y * tile.scaledTileDimension -
140+
tile.currentPixelOrigin.dy,
141+
);
142+
final destSize = Size.square(tile.scaledTileDimension);
143+
144+
//final paint = _basePaint
145+
// ..color = (null?.color.withOpacity(tile.tileImage.opacity) ??
146+
// Color.fromRGBO(0, 0, 0, tile.tileImage.opacity));
147+
148+
canvas.drawImageRect(
149+
image,
150+
Offset.zero &
151+
Size(image.width.toDouble(), image.height.toDouble()), // src
152+
origin & destSize, // dest
153+
_basePaint,
154+
);
155+
}
156+
}
108157
}
109158

110159
@override
111-
bool shouldRepaint(covariant CustomPainter oldDelegate) {
112-
// TODO: implement shouldRepaint
113-
throw UnimplementedError();
160+
bool shouldRepaint(CustomPainter oldDelegate) {
161+
return true;
114162
}
115163
}

lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'package:flutter_map/flutter_map.dart';
22
import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/stub.dart'
3-
if (dart.library.io) 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart'
4-
if (dart.library.js_interop) 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/web/web.dart';
3+
if (dart.library.io) 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/native.dart'
4+
if (dart.library.js_interop) 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/web/web.dart';
55
import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart';
66
import 'package:uuid/data.dart';
77
import 'package:uuid/rng.dart';

lib/src/layer/modern_tile_layer/tile_loader/raster/tile_data.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,8 @@ class RasterTileData implements BaseTileData {
3131

3232
final _loadedTracker = Completer<void>();
3333
@override
34-
Future<void> get whenLoaded => _loadedTracker.future;
34+
Future<void> get triggerPrune => _loadedTracker.future;
3535

36-
@override
37-
bool get isLoaded => loaded != null;
3836
({
3937
DateTime time,
4038
ImageInfo? successfulImageInfo,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_data.dart';
2+
import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart';
23
import 'package:flutter_map/src/layer/modern_tile_layer/options.dart';
34
import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart';
45
import 'package:meta/meta.dart';
56

67
/// Responsible for generating a tile's data ([D]), given the [TileCoordinates]
78
/// and ambient [TileLayerOptions].
9+
///
10+
/// Once a tile has been generated, it is passed to the [BaseTileLayer.renderer]
11+
/// for rendering.
812
@immutable
913
abstract interface class TileLoader<D extends BaseTileData> {
1014
/// Generate data ([D]) for the tile at [coordinates], with the ambient layer
1115
/// [options].
16+
///
17+
/// The data must be generated synchronously. However, the interface of
18+
/// [BaseTileData] allows for integration of asynchronous work with the tile
19+
/// layer.
20+
///
21+
/// If this throws an error then the tile will not be rendered.
1222
D call(TileCoordinates coordinates, TileLayerOptions options);
1323
}

0 commit comments

Comments
 (0)