use crate::cache::TileCache;
use crate::source::{TileKey, TileSource};
use crate::sources::util::{decode_png_to_buffer, format_url};
use slint::{Image, Rgba8Pixel, SharedPixelBuffer};
use std::collections::{HashMap, HashSet, VecDeque};
use std::io::Read;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
type TileReadyCallback = Arc<Mutex<Option<Arc<dyn Fn() + Send + Sync>>>>;
pub const OSM_TILE_URL: &str = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
const DEFAULT_FETCH_WORKERS: usize = 2;
struct DecodeJob {
key: TileKey,
bytes: Option<Vec<u8>>,
}
struct WorkQueue<T> {
items: Mutex<VecDeque<T>>,
cond: Condvar,
shutdown: Mutex<bool>,
}
impl<T> WorkQueue<T> {
fn new() -> Self {
Self {
items: Mutex::new(VecDeque::new()),
cond: Condvar::new(),
shutdown: Mutex::new(false),
}
}
fn push(&self, t: T) {
self.items.lock().unwrap().push_back(t);
self.cond.notify_one();
}
fn pop(&self) -> Option<T> {
let mut items = self.items.lock().unwrap();
loop {
if let Some(t) = items.pop_front() {
return Some(t);
}
if *self.shutdown.lock().unwrap() {
return None;
}
items = self.cond.wait(items).unwrap();
}
}
fn retain<F: FnMut(&T) -> bool>(&self, mut keep: F) -> usize {
let mut items = self.items.lock().unwrap();
let before = items.len();
items.retain(|t| keep(t));
before - items.len()
}
fn shutdown(&self) {
*self.shutdown.lock().unwrap() = true;
self.cond.notify_all();
}
#[cfg(test)]
fn len(&self) -> usize {
self.items.lock().unwrap().len()
}
}
pub struct OsmTileSource {
cache: Arc<dyn TileCache>,
memory: Arc<Mutex<HashMap<TileKey, SharedPixelBuffer<Rgba8Pixel>>>>,
failed: Arc<Mutex<HashSet<TileKey>>>,
user_agent: Arc<Mutex<String>>,
fetch_queue: Arc<WorkQueue<TileKey>>,
decode_queue: Arc<WorkQueue<DecodeJob>>,
in_flight: Arc<Mutex<HashSet<TileKey>>>,
on_tile_ready: TileReadyCallback,
notifier_handle: Mutex<Option<JoinHandle<()>>>,
fetcher_handles: Mutex<Vec<JoinHandle<()>>>,
decoder_handle: Mutex<Option<JoinHandle<()>>>,
tile_size: u32,
min_zoom: u8,
max_zoom: u8,
request_interval: Arc<AtomicU64>,
settle_ms: Arc<AtomicU64>,
}
impl OsmTileSource {
pub fn new(cache: Arc<dyn TileCache>) -> Self {
Self::with_url(OSM_TILE_URL, cache)
}
pub fn with_url(url_template: impl Into<String>, cache: Arc<dyn TileCache>) -> Self {
let url_template = url_template.into();
let user_agent: Arc<Mutex<String>> = Arc::new(Mutex::new(String::from(
"slint-mapping/0.1 (+https://github.com/slint-ui)",
)));
let fetch_queue: Arc<WorkQueue<TileKey>> = Arc::new(WorkQueue::new());
let decode_queue: Arc<WorkQueue<DecodeJob>> = Arc::new(WorkQueue::new());
let in_flight = Arc::new(Mutex::new(HashSet::new()));
let failed: Arc<Mutex<HashSet<TileKey>>> = Arc::new(Mutex::new(HashSet::new()));
let on_tile_ready: TileReadyCallback = Arc::new(Mutex::new(None));
let memory: Arc<Mutex<HashMap<TileKey, SharedPixelBuffer<Rgba8Pixel>>>> =
Arc::new(Mutex::new(HashMap::new()));
let request_interval = Arc::new(AtomicU64::new(300));
let settle_ms = Arc::new(AtomicU64::new(25));
let completions = Arc::new(AtomicU64::new(0));
let notifier_handle = {
let on_tile = Arc::clone(&on_tile_ready);
let completions = Arc::clone(&completions);
let settle = Arc::clone(&settle_ms);
let decode_queue = Arc::clone(&decode_queue);
thread::Builder::new()
.name("slint-mapping-osm-notifier".into())
.spawn(move || loop {
thread::park();
if *decode_queue.shutdown.lock().unwrap() {
return;
}
let settle = Duration::from_millis(settle.load(Ordering::Relaxed));
thread::sleep(settle);
if completions.swap(0, Ordering::SeqCst) == 0 {
continue;
}
let cb = on_tile.lock().unwrap().clone();
if let Some(cb) = cb {
cb();
}
})
.expect("spawn notifier thread")
};
let mut fetcher_handles = Vec::with_capacity(DEFAULT_FETCH_WORKERS);
for worker_id in 0..DEFAULT_FETCH_WORKERS {
let fetch_queue_w = Arc::clone(&fetch_queue);
let decode_queue_w = Arc::clone(&decode_queue);
let cache_w = Arc::clone(&cache);
let failed_w = Arc::clone(&failed);
let in_flight_w = Arc::clone(&in_flight);
let template_w = url_template.clone();
let ua_w = Arc::clone(&user_agent);
let interval_w = Arc::clone(&request_interval);
let completions_w = Arc::clone(&completions);
let notifier_thread = notifier_handle.thread().clone();
let handle = thread::Builder::new()
.name(format!("slint-mapping-osm-fetcher-{worker_id}"))
.spawn(move || {
let mut last_request_at = Instant::now() - Duration::from_secs(60);
while let Some(key) = fetch_queue_w.pop() {
let interval = Duration::from_millis(interval_w.load(Ordering::Relaxed));
let since = last_request_at.elapsed();
if since < interval {
thread::sleep(interval - since);
}
last_request_at = Instant::now();
let url = format_url(&template_w, key);
let ua = ua_w.lock().unwrap().clone();
match fetch_bytes(&url, &ua) {
Ok(b) => {
if let Err(e) = cache_w.put(key, &b) {
eprintln!("[slint-mapping] cache put {key:?}: {e}");
}
decode_queue_w.push(DecodeJob {
key,
bytes: Some(b),
});
}
Err(e) => {
eprintln!("[slint-mapping] fetch {url}: {e}");
failed_w.lock().unwrap().insert(key);
in_flight_w.lock().unwrap().remove(&key);
completions_w.fetch_add(1, Ordering::SeqCst);
notifier_thread.unpark();
}
}
}
})
.expect("spawn fetcher thread");
fetcher_handles.push(handle);
}
let decoder_handle = {
let decode_queue_w = Arc::clone(&decode_queue);
let cache_w = Arc::clone(&cache);
let memory_w = Arc::clone(&memory);
let failed_w = Arc::clone(&failed);
let in_flight_w = Arc::clone(&in_flight);
let completions_w = Arc::clone(&completions);
let notifier_thread = notifier_handle.thread().clone();
thread::Builder::new()
.name("slint-mapping-osm-decoder".into())
.spawn(move || {
while let Some(job) = decode_queue_w.pop() {
let DecodeJob { key, bytes } = job;
let bytes = match bytes {
Some(b) => Some(b),
None => cache_w.get_bytes(key),
};
match bytes.as_deref().and_then(decode_png_to_buffer) {
Some(buf) => {
memory_w.lock().unwrap().insert(key, buf);
failed_w.lock().unwrap().remove(&key);
}
None => {
failed_w.lock().unwrap().insert(key);
}
}
in_flight_w.lock().unwrap().remove(&key);
completions_w.fetch_add(1, Ordering::SeqCst);
notifier_thread.unpark();
}
})
.expect("spawn decoder thread")
};
drop(completions);
Self {
cache,
memory,
failed,
user_agent,
fetch_queue,
decode_queue,
in_flight,
on_tile_ready,
notifier_handle: Mutex::new(Some(notifier_handle)),
fetcher_handles: Mutex::new(fetcher_handles),
decoder_handle: Mutex::new(Some(decoder_handle)),
tile_size: 256,
min_zoom: 0,
max_zoom: 19,
request_interval,
settle_ms,
}
}
pub fn with_user_agent(self, ua: impl Into<String>) -> Self {
*self.user_agent.lock().unwrap() = ua.into();
self
}
pub fn with_zoom_range(mut self, min: u8, max: u8) -> Self {
self.min_zoom = min;
self.max_zoom = max;
self
}
pub fn with_tile_size(mut self, size: u32) -> Self {
self.tile_size = size;
self
}
pub fn with_request_interval(self, ms: u64) -> Self {
self.request_interval.store(ms, Ordering::Relaxed);
self
}
pub fn with_settle_ms(self, ms: u64) -> Self {
self.settle_ms.store(ms, Ordering::Relaxed);
self
}
pub fn on_tile_ready(&self, callback: impl Fn() + Send + Sync + 'static) {
*self.on_tile_ready.lock().unwrap() = Some(Arc::new(callback));
}
}
impl Drop for OsmTileSource {
fn drop(&mut self) {
self.fetch_queue.shutdown();
self.decode_queue.shutdown();
if let Some(notifier) = self.notifier_handle.lock().unwrap().take() {
notifier.thread().unpark();
std::mem::drop(notifier);
}
for h in self.fetcher_handles.lock().unwrap().drain(..) {
std::mem::drop(h);
}
if let Some(decoder) = self.decoder_handle.lock().unwrap().take() {
std::mem::drop(decoder);
}
}
}
impl TileSource for OsmTileSource {
fn tile(&self, key: TileKey) -> Option<slint::Image> {
if let Some(buf) = self.memory.lock().unwrap().get(&key).cloned() {
return Some(Image::from_rgba8(buf));
}
if self.failed.lock().unwrap().contains(&key) {
return None;
}
let mut in_flight = self.in_flight.lock().unwrap();
if !in_flight.insert(key) {
return None;
}
drop(in_flight);
if self.cache.contains(key) {
self.decode_queue.push(DecodeJob { key, bytes: None });
} else {
self.fetch_queue.push(key);
}
None
}
fn tile_size(&self) -> u32 {
self.tile_size
}
fn min_zoom(&self) -> u8 {
self.min_zoom
}
fn max_zoom(&self) -> u8 {
self.max_zoom
}
fn cancel_all_except(&self, keep: &HashSet<TileKey>) {
let removed = self.fetch_queue.retain(|key| keep.contains(key));
if removed > 0 {
let memory = self.memory.lock().unwrap();
let mut inflight = self.in_flight.lock().unwrap();
inflight.retain(|k| keep.contains(k) || memory.contains_key(k));
}
}
}
impl OsmTileSource {
pub fn clear_failed(&self) {
self.failed.lock().unwrap().clear();
}
}
pub(crate) fn fetch_bytes(url: &str, user_agent: &str) -> Result<Vec<u8>, String> {
let resp = ureq::get(url)
.set("User-Agent", user_agent)
.call()
.map_err(|e| format!("{e}"))?;
let mut bytes = Vec::new();
resp.into_reader()
.read_to_end(&mut bytes)
.map_err(|e| format!("read: {e}"))?;
Ok(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_url_substitutes() {
assert_eq!(
format_url("https://x/{z}/{x}/{y}.png", TileKey { x: 5, y: 10, z: 2 }),
"https://x/2/5/10.png"
);
}
#[test]
fn decode_png_roundtrips() {
let bytes = include_bytes!("../../sample-tiles/0/0/0.png");
let buf = decode_png_to_buffer(bytes).expect("decode");
assert_eq!(buf.width(), 256);
assert_eq!(buf.height(), 256);
}
#[test]
fn failed_key_is_not_re_enqueued() {
let cache: Arc<dyn TileCache> = Arc::new(crate::cache::FileTileCache::new(
std::env::temp_dir().join("slint-mapping-failed-key-test"),
));
let src = OsmTileSource::with_url("http://127.0.0.1:1/{z}/{x}/{y}.png", cache);
let key = TileKey { x: 1, y: 2, z: 3 };
src.failed.lock().unwrap().insert(key);
let before_fetch = src.fetch_queue.len();
let before_decode = src.decode_queue.len();
let result = src.tile(key);
let after_fetch = src.fetch_queue.len();
let after_decode = src.decode_queue.len();
assert!(result.is_none());
assert_eq!(after_fetch, before_fetch, "failed key enqueued for fetch");
assert_eq!(
after_decode, before_decode,
"failed key enqueued for decode"
);
src.clear_failed();
assert!(src.failed.lock().unwrap().is_empty());
}
#[test]
fn cancel_drops_queued_work() {
let q: WorkQueue<TileKey> = WorkQueue::new();
q.push(TileKey { x: 1, y: 1, z: 5 });
q.push(TileKey { x: 2, y: 2, z: 5 });
q.push(TileKey { x: 3, y: 3, z: 5 });
let keep: HashSet<TileKey> = [TileKey { x: 2, y: 2, z: 5 }].into_iter().collect();
let dropped = q.retain(|k| keep.contains(k));
assert_eq!(dropped, 2);
assert_eq!(q.len(), 1);
}
}