1use 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(), 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 let fmt_string = fmt_string.trim();
158 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); 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}