utiles_core/
tile_strfmt.rs

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