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