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 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 _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 Function() render; final void Function() onReady; }