173 lines
4.3 KiB
Dart
173 lines
4.3 KiB
Dart
import 'dart:collection';
|
|
import 'dart:ui' as ui;
|
|
|
|
class _TileCoord {
|
|
const _TileCoord(this.z, this.x, this.y);
|
|
|
|
final int z;
|
|
final int x;
|
|
final int y;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
other is _TileCoord && other.z == z && other.x == x && other.y == y;
|
|
|
|
@override
|
|
int get hashCode => Object.hash(z, x, y);
|
|
}
|
|
|
|
class CachedTile {
|
|
CachedTile(this.image);
|
|
|
|
final ui.Image image;
|
|
}
|
|
|
|
// Static tile pyramid image cache. Stores one rasterised ui.Image per
|
|
// deterministic pyramid tile coordinate (z, x, y).
|
|
class TilePyramidCache {
|
|
TilePyramidCache._();
|
|
|
|
static const int _maxEntries = 384;
|
|
static const int _maxApproxBytes = 768 * 1024 * 1024;
|
|
|
|
// Cap concurrent renders to avoid saturating the main isolate with
|
|
// synchronous canvas recording. I/O (network/Hive) is still concurrent
|
|
// up to this limit.
|
|
static const int _maxConcurrent = 4;
|
|
|
|
static final LinkedHashMap<_TileCoord, CachedTile> _cache =
|
|
LinkedHashMap<_TileCoord, CachedTile>();
|
|
|
|
static final Set<_TileCoord> _inFlight = <_TileCoord>{};
|
|
static final Queue<_Queued> _queue = Queue<_Queued>();
|
|
static int _active = 0;
|
|
static bool Function(int z, int x, int y)? _queueFilter;
|
|
|
|
static int _approxBytes = 0;
|
|
|
|
static CachedTile? peek(int z, int x, int y) {
|
|
final coord = _TileCoord(z, x, y);
|
|
final tile = _cache.remove(coord);
|
|
if (tile == null) return null;
|
|
_cache[coord] = tile; // promote to MRU
|
|
return tile;
|
|
}
|
|
|
|
static bool isInFlight(int z, int x, int y) =>
|
|
_inFlight.contains(_TileCoord(z, x, y));
|
|
|
|
// Queue a render for (z, x, y). No-op if already in flight.
|
|
// render() produces the image; onReady() is called after it's stored.
|
|
static void enqueue(
|
|
int z,
|
|
int x,
|
|
int y,
|
|
Future<ui.Image?> Function() render,
|
|
void Function() onReady,
|
|
) {
|
|
final coord = _TileCoord(z, x, y);
|
|
if (_inFlight.contains(coord)) return;
|
|
final filter = _queueFilter;
|
|
if (filter != null && !filter(z, x, y)) return;
|
|
_inFlight.add(coord);
|
|
|
|
_queue.add(_Queued(coord: coord, render: render, onReady: onReady));
|
|
_drain();
|
|
}
|
|
|
|
static void setQueueFilter(bool Function(int z, int x, int y)? filter) {
|
|
_queueFilter = filter;
|
|
_pruneQueue();
|
|
}
|
|
|
|
static void _drain() {
|
|
while (_active < _maxConcurrent && _queue.isNotEmpty) {
|
|
_active++;
|
|
_run(_queue.removeFirst());
|
|
}
|
|
}
|
|
|
|
static Future<void> _run(_Queued item) async {
|
|
try {
|
|
final image = await item.render();
|
|
_inFlight.remove(item.coord);
|
|
if (image == null) return;
|
|
|
|
final filter = _queueFilter;
|
|
if (filter != null && !filter(item.coord.z, item.coord.x, item.coord.y)) {
|
|
image.dispose();
|
|
return;
|
|
}
|
|
|
|
final existing = _cache.remove(item.coord);
|
|
if (existing != null) {
|
|
_approxBytes -= _estimateBytes(existing.image);
|
|
existing.image.dispose();
|
|
}
|
|
|
|
_cache[item.coord] = CachedTile(image);
|
|
_approxBytes += _estimateBytes(image);
|
|
_trim();
|
|
|
|
item.onReady();
|
|
} catch (_) {
|
|
_inFlight.remove(item.coord);
|
|
} finally {
|
|
_active--;
|
|
_drain();
|
|
}
|
|
}
|
|
|
|
static int _estimateBytes(ui.Image img) => img.width * img.height * 4;
|
|
|
|
static void _trim() {
|
|
while (_cache.length > _maxEntries || _approxBytes > _maxApproxBytes) {
|
|
final key = _cache.keys.first;
|
|
final tile = _cache.remove(key);
|
|
if (tile == null) continue;
|
|
_approxBytes -= _estimateBytes(tile.image);
|
|
tile.image.dispose();
|
|
}
|
|
}
|
|
|
|
static void _pruneQueue() {
|
|
final filter = _queueFilter;
|
|
if (filter == null || _queue.isEmpty) return;
|
|
|
|
final kept = Queue<_Queued>();
|
|
while (_queue.isNotEmpty) {
|
|
final item = _queue.removeFirst();
|
|
if (filter(item.coord.z, item.coord.x, item.coord.y)) {
|
|
kept.add(item);
|
|
} else {
|
|
_inFlight.remove(item.coord);
|
|
}
|
|
}
|
|
|
|
_queue.addAll(kept);
|
|
}
|
|
|
|
static void clear() {
|
|
for (final tile in _cache.values) {
|
|
tile.image.dispose();
|
|
}
|
|
_cache.clear();
|
|
_inFlight.clear();
|
|
_queue.clear();
|
|
_active = 0;
|
|
_approxBytes = 0;
|
|
_queueFilter = null;
|
|
}
|
|
}
|
|
|
|
class _Queued {
|
|
const _Queued({
|
|
required this.coord,
|
|
required this.render,
|
|
required this.onReady,
|
|
});
|
|
|
|
final _TileCoord coord;
|
|
final Future<ui.Image?> Function() render;
|
|
final void Function() onReady;
|
|
}
|