Roadbound-Map-Utility/lib/pages/map/vector/tile_pyramid_cache.dart

192 lines
4.9 KiB
Dart

import 'dart:collection';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
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 final bool _isMobile =
!kIsWeb && (Platform.isAndroid || Platform.isIOS);
static final int _maxEntries = kIsWeb ? 64 : (_isMobile ? 96 : 384);
static final int _maxApproxBytes = kIsWeb
? 64 * 1024 * 1024
: (_isMobile ? 80 * 1024 * 1024 : 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 final int _maxConcurrent = kIsWeb ? 2 : (_isMobile ? 2 : 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));
static bool contains(int z, int x, int y) =>
_cache.containsKey(_TileCoord(z, x, y));
static int get debugCacheCount => _cache.length;
static int get debugInFlightCount => _inFlight.length;
static int get debugQueueCount => _queue.length;
static double get debugApproxMegabytes => _approxBytes / (1024 * 1024);
// 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;
}