Skip to main content

sioul_app/
map.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! Contacts on a map: where each postal address is, from the local cache of
5//! places; placing the others, once you allow it, one address a second, on
6//! a thread, the map told as they come.
7
8use crate::backend::{QtThread, Shared, json, load_config, tell, tr};
9use crate::work::loaded;
10use serde::Serialize;
11use sioul_core::places::{Place, Places};
12use std::sync::Arc;
13use std::sync::atomic::Ordering;
14
15#[derive(Serialize)]
16struct Pin {
17    key: String,
18    name: String,
19    address: String,
20    lat: f64,
21    lon: f64,
22}
23
24#[derive(Serialize)]
25struct MapView {
26    /// Addresses may be placed (you allowed it).
27    allowed: bool,
28    /// The tile server, as Qt's map wants it: `https://tile.openstreetmap.org/`.
29    tiles: String,
30    pins: Vec<Pin>,
31    /// Addresses not placed yet.
32    waiting: usize,
33    /// Placing is under way.
34    locating: bool,
35}
36
37/// The tile server's address before "{z}/{x}/{y}.png", OpenStreetMap's by default.
38fn tiles() -> String {
39    let configured = load_config().map.tiles.unwrap_or_default();
40    let host = configured.split("{z}").next().unwrap_or("").trim().to_string();
41    if host.is_empty() { "https://tile.openstreetmap.org/".to_string() } else { host }
42}
43
44/// Every contact with a postal address placed, and how many are not yet.
45pub(crate) fn view(shared: &Shared) -> String {
46    let loaded = loaded(shared);
47    let places = Places::load(&Places::default_path());
48    let mut pins = Vec::new();
49    let mut waiting = 0;
50    for contact in &loaded.contacts {
51        for address in contact.addresses.iter().filter(|a| !a.value.trim().is_empty()) {
52            match places.get(&address.value) {
53                Some(Place { lat, lon }) => pins.push(Pin { key: contact.key.clone(), name: contact.name.clone(), address: address.value.clone(), lat, lon }),
54                None if !places.asked(&address.value) => waiting += 1,
55                None => {}
56            }
57        }
58    }
59    json(&MapView { allowed: load_config().map.geocode, tiles: tiles(), pins, waiting, locating: shared.locating.load(Ordering::Relaxed) })
60}
61
62/// Where one address is, when placed: {"lat", "lon"}, else "".
63pub(crate) fn place_of(address: &str) -> String {
64    Places::load(&Places::default_path()).get(address).map(|p| serde_json::json!({ "lat": p.lat, "lon": p.lon }).to_string()).unwrap_or_default()
65}
66
67/// Places the addresses not asked yet, when you allowed it: one a second, on a
68/// thread; the map is told every few, and at the end.
69pub(crate) fn locate(qt: &QtThread, shared: &Arc<Shared>) {
70    if !load_config().map.geocode || crate::backend::offline() || shared.locating.swap(true, Ordering::Relaxed) {
71        return;
72    }
73    let loaded = loaded(shared);
74    let mut addresses: Vec<String> = loaded.contacts.iter().flat_map(|c| c.addresses.iter().map(|a| a.value.clone())).filter(|a| !a.trim().is_empty()).collect();
75    addresses.sort();
76    addresses.dedup();
77    let (qt, shared) = (qt.clone(), Arc::clone(shared));
78    let changed = |qt: &QtThread| {
79        let _ = qt.queue(|mut sioul| sioul.as_mut().places_changed());
80    };
81    changed(&qt);
82    std::thread::spawn(move || {
83        let path = Places::default_path();
84        let language = tr().text("qt-locale", None).replace('_', "-");
85        let mut asked = 0;
86        for address in addresses {
87            let mut places = Places::load(&path);
88            if places.asked(&address) {
89                continue;
90            }
91            if asked > 0 {
92                std::thread::sleep(sioul_sync::geocode::PAUSE);
93            }
94            asked += 1;
95            match sioul_sync::geocode::locate(&address, &language) {
96                Ok(place) => {
97                    places.set(&address, place);
98                    if let Err(e) = places.save(&path) {
99                        tell(&qt, &shared, e);
100                        break;
101                    }
102                }
103                // The network or the service is away: what is left waits for the next time.
104                Err(e) => {
105                    tell(&qt, &shared, e.sentence(tr(), "OpenStreetMap"));
106                    break;
107                }
108            }
109            if asked % 5 == 0 {
110                changed(&qt);
111            }
112        }
113        shared.locating.store(false, Ordering::Relaxed);
114        changed(&qt);
115    });
116}