Skip to main content

utiles_core/
tile_strfmt.rs

1//! Tile string formatting
2use std::fmt::{Debug, Display, Formatter};
3use std::hash::Hash;
4
5use crate::bbox::WebBBox;
6use crate::{BBox, TileLike};
7
8#[derive(Debug, PartialEq, Clone, Eq, Hash)]
9pub enum FormatTokens {
10    X,
11    Y,
12    Z,
13    Yup,
14    ZxyFslash,
15    Quadkey,
16    #[cfg(feature = "pmtiles")]
17    PmtileId,
18    JsonObj,
19    JsonArr,
20    GeoBBox,
21    Projwin,
22    BBoxWeb,
23    ProjwinWeb,
24}
25
26impl Display for FormatTokens {
27    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
28        f.write_str(match *self {
29            Self::X => "{x}",
30            Self::Y => "{y}",
31            Self::Z => "{z}",
32            Self::Yup => "{-y}",
33            Self::ZxyFslash => "{z}/{x}/{y}",
34            Self::Quadkey => "{quadkey}",
35            #[cfg(feature = "pmtiles")]
36            Self::PmtileId => "{pmtileid}",
37            Self::JsonObj => "{json_obj}",
38            Self::JsonArr => "{json_arr}",
39            Self::GeoBBox => "{bbox}",
40            Self::Projwin => "{projwin}",
41            Self::BBoxWeb => "{bbox_web}",
42            Self::ProjwinWeb => "{projwin_web}",
43        })
44    }
45}
46
47#[derive(Debug, PartialEq, Clone, Eq, Hash)]
48pub enum FormatParts {
49    Str(String),
50    Token(FormatTokens),
51}
52
53impl From<FormatTokens> for &'static str {
54    fn from(t: FormatTokens) -> Self {
55        match t {
56            FormatTokens::X => "{x}",
57            FormatTokens::Y => "{y}",
58            FormatTokens::Z => "{z}",
59            FormatTokens::Yup => "{-y}",
60            FormatTokens::ZxyFslash => "{z}/{x}/{y}",
61            FormatTokens::Quadkey => "{quadkey}",
62            #[cfg(feature = "pmtiles")]
63            FormatTokens::PmtileId => "{pmtileid}",
64            FormatTokens::JsonObj => "{json_obj}",
65            FormatTokens::JsonArr => "{json_arr}",
66            FormatTokens::GeoBBox => "{bbox}",
67            FormatTokens::Projwin => "{projwin}",
68            FormatTokens::BBoxWeb => "{bbox_web}",
69            FormatTokens::ProjwinWeb => "{projwin_web}",
70        }
71    }
72}
73
74impl From<&str> for FormatParts {
75    fn from(s: &str) -> Self {
76        match s.to_lowercase().as_str() {
77            "x" => Self::Token(FormatTokens::X),
78            "y" => Self::Token(FormatTokens::Y),
79            "z" => Self::Token(FormatTokens::Z),
80            "yup" | "-y" => Self::Token(FormatTokens::Yup),
81            "zxy" => Self::Token(FormatTokens::ZxyFslash),
82            "quadkey" | "qk" => Self::Token(FormatTokens::Quadkey),
83            #[cfg(feature = "pmtiles")]
84            "pmtileid" | "pmid" => Self::Token(FormatTokens::PmtileId),
85            "json" | "json_arr" => Self::Token(FormatTokens::JsonArr),
86            "json_obj" | "obj" => Self::Token(FormatTokens::JsonObj),
87            "bbox" => Self::Token(FormatTokens::GeoBBox),
88            "projwin" => Self::Token(FormatTokens::Projwin),
89            "bbox_web" => Self::Token(FormatTokens::BBoxWeb),
90            "projwin_web" => Self::Token(FormatTokens::ProjwinWeb),
91
92            _ => Self::Str(s.to_string()),
93        }
94    }
95}
96
97impl From<&FormatTokens> for String {
98    fn from(t: &FormatTokens) -> Self {
99        match t {
100            FormatTokens::X => "{x}".to_string(),
101            FormatTokens::Y => "{y}".to_string(),
102            FormatTokens::Z => "{z}".to_string(),
103            FormatTokens::Yup => "{-y}".to_string(),
104            FormatTokens::ZxyFslash => "{z}/{x}/{y}".to_string(),
105            FormatTokens::Quadkey => "{quadkey}".to_string(),
106            #[cfg(feature = "pmtiles")]
107            FormatTokens::PmtileId => "{pmtileid}".to_string(),
108            FormatTokens::JsonObj => "{json_obj}".to_string(),
109            FormatTokens::JsonArr => "{json_arr}".to_string(),
110            FormatTokens::GeoBBox => "{bbox}".to_string(),
111            FormatTokens::Projwin => "{projwin}".to_string(),
112            FormatTokens::BBoxWeb => "{bbox_web}".to_string(),
113            FormatTokens::ProjwinWeb => "{projwin_web}".to_string(),
114        }
115    }
116}
117
118impl From<&FormatParts> for String {
119    fn from(p: &FormatParts) -> Self {
120        match p {
121            FormatParts::Str(s) => s.clone(), // yolo clone here
122            FormatParts::Token(t) => Self::from(t),
123        }
124    }
125}
126
127#[derive(Debug, PartialEq, Clone, Eq, Hash)]
128pub struct TileStringFormat {
129    fmtstr: String,
130    tokens: Vec<FormatParts>,
131    n_tokens: usize,
132}
133
134impl Default for TileStringFormat {
135    fn default() -> Self {
136        Self {
137            fmtstr: "{json_arr}".to_string(),
138            tokens: vec![FormatParts::Token(FormatTokens::ZxyFslash)],
139            n_tokens: 1,
140        }
141    }
142}
143
144impl TileStringFormat {
145    pub fn new(fmt: &str) -> Self {
146        let (tokens, n_tokens) = Self::parse(fmt);
147        let fmt_str = tokens.iter().map(String::from).collect::<String>();
148        Self {
149            fmtstr: fmt_str,
150            tokens,
151            n_tokens,
152        }
153    }
154
155    fn parse(fmt_string: &str) -> (Vec<FormatParts>, usize) {
156        // check if the fmt string is just a token
157        let fmt_string = fmt_string.trim();
158        // else we do the full parse
159        let fmt = fmt_string
160            .replace("{z}/{x}/{y}", "{zxy}")
161            .replace("{x}/{y}/{z}", "{xyz}");
162        let mut tokens = Vec::new();
163        let mut token = String::new();
164        for c in fmt.chars() {
165            if c == '{' {
166                if !token.is_empty() {
167                    tokens.push(FormatParts::Str(token.clone()));
168                    token.clear();
169                }
170                continue;
171            }
172            if c == '}' {
173                if !token.is_empty() {
174                    tokens.push(FormatParts::from(token.as_str()));
175                    token.clear();
176                }
177                continue;
178            }
179            token.push(c);
180        }
181        if !token.is_empty() {
182            tokens.push(FormatParts::Str(token));
183        }
184        let n_tokens = tokens
185            .iter()
186            .filter(|t| !matches!(t, FormatParts::Str(_)))
187            .count();
188        (tokens, n_tokens)
189    }
190}
191
192pub struct TileStringFormatter {
193    tile_fmt: TileStringFormat,
194    parts: Vec<FmtPart>,
195}
196
197impl Hash for TileStringFormatter {
198    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
199        self.tile_fmt.hash(state);
200    }
201}
202
203#[expect(clippy::missing_fields_in_debug)]
204impl Debug for TileStringFormatter {
205    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
206        f.debug_struct("TileStringFormatter")
207            .field("fmt", &self.tile_fmt.fmtstr)
208            .field("tokens", &self.tile_fmt.tokens)
209            .field("n_tokens", &self.tile_fmt.n_tokens)
210            .finish()
211    }
212}
213
214impl Clone for TileStringFormatter {
215    fn clone(&self) -> Self {
216        let tile_fmt = self.tile_fmt.clone();
217        let parts = Self::parse_parts(&tile_fmt);
218        Self { tile_fmt, parts }
219    }
220}
221
222impl PartialEq<Self> for TileStringFormatter {
223    fn eq(&self, other: &Self) -> bool {
224        self.tile_fmt == other.tile_fmt
225    }
226}
227
228enum FmtPart {
229    Static(&'static str),
230    Dynamic(fn(&dyn TileLike) -> String),
231}
232
233impl TileStringFormatter {
234    #[must_use]
235    pub fn new(fmt: &str) -> Self {
236        let tile_fmt = TileStringFormat::new(fmt);
237        let parts = Self::parse_parts(&tile_fmt);
238        Self { tile_fmt, parts }
239    }
240
241    fn parse_parts(tile_fmt: &TileStringFormat) -> Vec<FmtPart> {
242        let mut parts = Vec::new();
243        for token in &tile_fmt.tokens {
244            match token {
245                FormatParts::Str(s) => {
246                    parts.push(FmtPart::Static(Box::leak(s.clone().into_boxed_str())));
247                }
248                FormatParts::Token(t) => match t {
249                    FormatTokens::X => {
250                        parts.push(FmtPart::Dynamic(|tile| tile.x().to_string()));
251                    }
252                    FormatTokens::Y => {
253                        parts.push(FmtPart::Dynamic(|tile| tile.y().to_string()));
254                    }
255                    FormatTokens::Yup => {
256                        parts.push(FmtPart::Dynamic(|tile| tile.yup().to_string()));
257                    }
258                    FormatTokens::Z => {
259                        parts.push(FmtPart::Dynamic(|tile| tile.z().to_string()));
260                    }
261                    FormatTokens::ZxyFslash => {
262                        parts.push(FmtPart::Dynamic(|tile| tile.zxy_str_fslash()));
263                    }
264                    FormatTokens::Quadkey => {
265                        parts.push(FmtPart::Dynamic(|tile| tile.quadkey()));
266                    }
267                    #[cfg(feature = "pmtiles")]
268                    FormatTokens::PmtileId => {
269                        parts
270                            .push(FmtPart::Dynamic(|tile| tile.pmtileid().to_string()));
271                    }
272                    FormatTokens::JsonArr => {
273                        parts.push(FmtPart::Dynamic(|tile| tile.json_arr()));
274                    }
275                    FormatTokens::JsonObj => {
276                        parts.push(FmtPart::Dynamic(|tile| tile.json_obj()));
277                    }
278                    FormatTokens::GeoBBox => {
279                        parts.push(FmtPart::Dynamic(|tile| {
280                            let b: BBox = tile.bbox().into();
281                            b.json_arr()
282                        }));
283                    }
284                    FormatTokens::Projwin => {
285                        parts.push(FmtPart::Dynamic(|tile| {
286                            let b: BBox = tile.bbox().into();
287                            b.projwin_str()
288                        }));
289                    }
290
291                    FormatTokens::ProjwinWeb => {
292                        parts.push(FmtPart::Dynamic(|tile| {
293                            let b: WebBBox = tile.webbbox();
294                            b.projwin_str()
295                        }));
296                    }
297                    FormatTokens::BBoxWeb => {
298                        parts.push(FmtPart::Dynamic(|tile| {
299                            let b: WebBBox = tile.webbbox();
300                            b.json_arr()
301                        }));
302                    }
303                },
304            }
305        }
306        parts
307    }
308
309    #[must_use]
310    pub const fn tokens(&self) -> &Vec<FormatParts> {
311        &self.tile_fmt.tokens
312    }
313
314    #[must_use]
315    pub const fn n_tokens(&self) -> usize {
316        self.tile_fmt.n_tokens
317    }
318
319    pub fn fmt_tile_custom<T: TileLike>(&self, tile: &T) -> String {
320        let mut out = String::with_capacity(self.tile_fmt.fmtstr.len() * 2); // Assuming average length doubling due to replacements
321        for part in &self.parts {
322            match part {
323                FmtPart::Static(s) => out.push_str(s),
324                FmtPart::Dynamic(f) => out.push_str(&f(tile)),
325            }
326        }
327        out
328    }
329
330    pub fn fmt_tile<T: TileLike>(&self, tile: &T) -> String {
331        match self.tile_fmt.fmtstr.as_str() {
332            "{json_arr}" => tile.json_arr(),
333            "{json_obj}" => tile.json_obj(),
334            "{quadkey}" => tile.quadkey(),
335            "{zxy}" => tile.zxy_str_fslash(),
336            _ => self.fmt_tile_custom(tile),
337        }
338    }
339    pub fn fmt<T: TileLike>(&self, tile: &T) -> String {
340        self.fmt_tile(tile)
341    }
342
343    #[must_use]
344    pub const fn has_token(&self) -> bool {
345        self.tile_fmt.n_tokens > 0
346    }
347
348    #[must_use]
349    pub fn fmtstr(&self) -> &str {
350        &self.tile_fmt.fmtstr
351    }
352}
353
354impl Default for TileStringFormatter {
355    fn default() -> Self {
356        Self::new("{json_arr}")
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use crate::Tile;
364
365    #[test]
366    fn test_formatter_zxy() {
367        let fmt = "{z}/{x}/{y}";
368        let f = TileStringFormatter::new(fmt);
369        assert_eq!(f.n_tokens(), 1);
370        let tile = Tile::new(1, 2, 3);
371        assert_eq!(f.fmt_tile(&tile), "3/1/2");
372    }
373
374    #[test]
375    fn test_formatter_zxy_fslash() {
376        let fmt = "{zxy}";
377        let f = TileStringFormatter::new(fmt);
378        assert_eq!(f.n_tokens(), 1);
379        let tile = Tile::new(1, 2, 3);
380        assert_eq!(f.fmt_tile(&tile), "3/1/2");
381    }
382
383    #[test]
384    fn test_formatter_quadkey() {
385        let fmt = "{quadkey}";
386        let f = TileStringFormatter::new(fmt);
387        assert_eq!(f.n_tokens(), 1);
388        let tile = Tile::new(1, 2, 3);
389        assert_eq!(f.fmt_tile(&tile), "021");
390    }
391
392    #[test]
393    fn test_formatter_json_arr() {
394        let fmt = "{json_arr}";
395        let f = TileStringFormatter::new(fmt);
396        assert_eq!(f.n_tokens(), 1);
397        let tile = Tile::new(1, 2, 3);
398        assert_eq!(f.fmt_tile(&tile), "[1, 2, 3]");
399    }
400
401    #[test]
402    fn test_formatter_json_obj() {
403        let fmt = "{json_obj}";
404        let f = TileStringFormatter::new(fmt);
405        assert_eq!(f.n_tokens(), 1);
406        let tile = Tile::new(1, 2, 3);
407        assert_eq!(f.fmt_tile(&tile), "{\"x\":1, \"y\":2, \"z\":3}");
408    }
409
410    #[test]
411    fn test_formatter_combined() {
412        let fmt = "tiles/{z}/{x}/{y}";
413        let f = TileStringFormatter::new(fmt);
414        let tile = Tile::new(1, 2, 3);
415        assert_eq!(
416            *f.tokens(),
417            vec![
418                FormatParts::Str("tiles/".to_string()),
419                FormatParts::Token(FormatTokens::ZxyFslash),
420            ]
421        );
422        assert_eq!(f.n_tokens(), 1);
423        assert_eq!(f.fmt_tile(&tile), "tiles/3/1/2");
424    }
425}