1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use std::sync::Arc;
use actix_web::error::ErrorNotFound;
use dashmap::DashMap;
use martin_core::tiles::catalog::TileCatalog;
use martin_core::tiles::{BoxedSource, Source};
use martin_tile_utils::TileInfo;
use tracing::debug;
/// Thread-safe registry of tile sources indexed by ID.
///
/// Uses a [`DashMap`] for concurrent access without explicit locking.
#[derive(Default, Clone)]
pub struct TileSources(Arc<DashMap<String, BoxedSource>>);
impl TileSources {
/// Creates a new registry from flattened source collections.
#[must_use]
pub fn new(sources: Vec<Vec<BoxedSource>>) -> Self {
Self(Arc::new(
sources
.into_iter()
.flatten()
.map(|src| (src.get_id().to_string(), src))
.collect(),
))
}
/// Creates a registry backed by an existing shared `DashMap`.
#[must_use]
pub(crate) fn from_dashmap(map: Arc<DashMap<String, BoxedSource>>) -> Self {
Self(map)
}
/// Returns a catalog of all sources with their metadata.
#[must_use]
pub fn get_catalog(&self) -> TileCatalog {
self.0
.iter()
.map(|v| (v.key().clone(), v.get_catalog_entry()))
.collect()
}
/// Returns all source IDs.
#[must_use]
pub fn source_names(&self) -> Vec<String> {
self.0.iter().map(|v| v.key().clone()).collect()
}
/// Gets a source by ID, returning 404 error if not found.
pub fn get_source(&self, id: &str) -> actix_web::Result<BoxedSource> {
Ok(self
.0
.get(id)
.ok_or_else(|| ErrorNotFound(format!("Source {id} does not exist")))?
.value()
.clone())
}
/// Gets multiple sources for composite tiles, ensuring format compatibility.
///
/// Parses comma-separated source IDs and validates all sources have matching
/// format/encoding. Optionally filters by zoom level support.
///
/// Returns (`sources`, `supports_url_query`, `merged_tile_info`).
#[hotpath::measure]
pub fn get_sources(
&self,
source_ids: &str,
zoom: Option<u8>,
) -> actix_web::Result<(Vec<BoxedSource>, bool, TileInfo)> {
let mut sources = Vec::new();
let mut info: Option<TileInfo> = None;
let mut use_url_query = false;
for id in source_ids.split(',') {
let src = self.get_source(id)?;
let src_inf = src.get_tile_info();
use_url_query |= src.support_url_query();
// make sure all sources have the same format and encoding
// TODO: support multiple encodings of the same format
match info {
Some(inf) if inf == src_inf => {}
Some(inf) => Err(ErrorNotFound(format!(
"Cannot merge sources with {inf} with {src_inf}"
)))?,
None => info = Some(src_inf),
}
// TODO: Use chained-if-let once available
if match zoom {
Some(zoom) if Self::check_zoom(&*src, id, zoom) => true,
None => true,
_ => false,
} {
sources.push(src);
}
}
Ok((
sources,
use_url_query,
info.expect("source_ids should be non-empty and contain at least one valid source"),
))
}
/// Validates zoom level support for a source
#[must_use]
pub fn check_zoom(src: &dyn Source, id: &str, zoom: u8) -> bool {
let is_valid = src.is_valid_zoom(zoom);
if !is_valid {
debug!("Zoom {zoom} is not valid for source {id}");
}
is_valid
}
/// Returns if any source benefits from concurrent scraping by martin-cp
#[must_use]
pub fn benefits_from_concurrent_scraping(&self) -> bool {
self.0.iter().any(|s| s.benefits_from_concurrent_scraping())
}
}