1use std::fmt::Display;
2use std::path::PathBuf;
3use std::str::FromStr as _;
4
5use futures::TryStreamExt as _;
6use log::{info, warn};
7use serde::Serialize;
8use serde_json::{Value as JSONValue, Value, json};
9use sqlx::{SqliteConnection, SqliteExecutor, query};
10use tilejson::{Bounds, Center, TileJSON, tilejson};
11
12use crate::MbtError::InvalidZoomValue;
13use crate::Mbtiles;
14use crate::errors::MbtResult;
15
16#[serde_with::skip_serializing_none]
42#[derive(Clone, Debug, PartialEq, Serialize)]
43pub struct Metadata {
44 pub id: String,
46
47 pub layer_type: Option<String>,
50
51 pub tilejson: TileJSON,
55
56 pub json: Option<JSONValue>,
58
59 pub agg_tiles_hash: Option<String>,
63}
64
65impl Mbtiles {
66 fn to_val<V, E: Display>(&self, val: Result<V, E>, title: &str) -> Option<V> {
67 match val {
68 Ok(v) => Some(v),
69 Err(err) => {
70 let name = &self.filename();
71 warn!("Unable to parse metadata {title} value in {name}: {err}");
72 None
73 }
74 }
75 }
76
77 pub async fn get_metadata_value<T>(&self, conn: &mut T, key: &str) -> MbtResult<Option<String>>
79 where
80 for<'e> &'e mut T: SqliteExecutor<'e>,
81 {
82 let query = query!("SELECT value from metadata where name = ?", key);
83 let row = query.fetch_optional(conn).await?;
84 if let Some(row) = row
85 && let Some(value) = row.value
86 {
87 return Ok(Some(value));
88 }
89 Ok(None)
90 }
91
92 pub async fn get_metadata_zoom_value<T>(
93 &self,
94 conn: &mut T,
95 zoom_name: &'static str,
96 ) -> MbtResult<Option<u8>>
97 where
98 for<'e> &'e mut T: SqliteExecutor<'e>,
99 {
100 self.get_metadata_value(conn, zoom_name)
101 .await?
102 .map(|v| v.parse().map_err(|_| InvalidZoomValue(zoom_name, v)))
103 .transpose()
104 }
105
106 pub async fn set_metadata_value<T, S>(&self, conn: &mut T, key: &str, value: S) -> MbtResult<()>
107 where
108 S: ToString,
109 for<'e> &'e mut T: SqliteExecutor<'e>,
110 {
111 let value = value.to_string();
112 query!(
113 "INSERT OR REPLACE INTO metadata(name, value) VALUES(?, ?)",
114 key,
115 value
116 )
117 .execute(conn)
118 .await?;
119 Ok(())
120 }
121
122 pub async fn delete_metadata_value<T>(&self, conn: &mut T, key: &str) -> MbtResult<()>
123 where
124 for<'e> &'e mut T: SqliteExecutor<'e>,
125 {
126 query!("DELETE FROM metadata WHERE name=?", key)
127 .execute(conn)
128 .await?;
129 Ok(())
130 }
131
132 pub async fn get_metadata<T>(&self, conn: &mut T) -> MbtResult<Metadata>
177 where
178 for<'e> &'e mut T: SqliteExecutor<'e>,
179 {
180 let query = query!("SELECT name, value FROM metadata WHERE value IS NOT ''");
181 let mut rows = query.fetch(&mut *conn);
182
183 let mut tj = tilejson! { tiles: vec![] };
184 let mut layer_type: Option<String> = None;
185 let mut json: Option<JSONValue> = None;
186 let mut agg_tiles_hash: Option<String> = None;
187
188 while let Some(row) = rows.try_next().await? {
189 if let (Some(name), Some(value)) = (row.name, row.value) {
190 match name.as_ref() {
191 "name" => tj.name = Some(value),
193 "version" => tj.version = Some(value),
194 "bounds" => tj.bounds = self.to_val(Bounds::from_str(value.as_str()), &name),
195 "center" => tj.center = self.to_val(Center::from_str(value.as_str()), &name),
196 "minzoom" => tj.minzoom = self.to_val(value.parse(), &name),
197 "maxzoom" => tj.maxzoom = self.to_val(value.parse(), &name),
198 "description" => tj.description = Some(value),
199 "attribution" => tj.attribution = Some(value),
200 "type" => layer_type = Some(value),
201 "legend" => tj.legend = Some(value),
202 "template" => tj.template = Some(value),
203 "json" => json = self.to_val(serde_json::from_str(&value), &name),
204 "format" | "generator" => {
205 tj.other.insert(name, Value::String(value));
206 }
207 "agg_tiles_hash" => agg_tiles_hash = Some(value),
208 "scheme" => {
209 if value != "tms" {
210 let file = &self.filename();
211 warn!(
212 "File {file} has an unexpected metadata value {name}='{value}'. Only 'tms' is supported. Ignoring."
213 );
214 }
215 }
216 _ => {
217 let file = &self.filename();
218 info!("{file} has an unrecognized metadata value {name}={value}");
219 tj.other.insert(name, Value::String(value));
220 }
221 }
222 }
223 }
224
225 if let Some(JSONValue::Object(obj)) = &mut json {
226 if let Some(value) = obj.remove("vector_layers") {
227 if let Ok(v) = serde_json::from_value(value) {
228 tj.vector_layers = Some(v);
229 } else {
230 warn!(
231 "Unable to parse metadata vector_layers value in {}",
232 self.filename()
233 );
234 }
235 }
236 if obj.is_empty() {
237 json = None;
238 }
239 }
240
241 drop(rows);
243
244 Ok(Metadata {
245 id: self.filename().to_string(),
246 tilejson: tj,
247 layer_type,
248 json,
249 agg_tiles_hash,
250 })
251 }
252
253 pub async fn insert_metadata<T>(&self, conn: &mut T, tile_json: &TileJSON) -> MbtResult<()>
287 where
288 for<'e> &'e mut T: SqliteExecutor<'e>,
289 {
290 for (key, value) in &tile_json.other {
291 if let Some(value) = value.as_str() {
292 self.set_metadata_value(conn, key, value).await?;
293 } else {
294 self.set_metadata_value(conn, key, &serde_json::to_string(value)?)
295 .await?;
296 }
297 }
298 for (key, value) in &[
299 ("name", tile_json.name.as_deref()),
300 ("version", tile_json.version.as_deref()),
301 ("description", tile_json.description.as_deref()),
302 ("attribution", tile_json.attribution.as_deref()),
303 ("legend", tile_json.legend.as_deref()),
304 ("template", tile_json.template.as_deref()),
305 ] {
306 if let Some(value) = value {
307 self.set_metadata_value(conn, key, value).await?;
308 }
309 }
310 if let Some(bounds) = &tile_json.bounds {
311 self.set_metadata_value(conn, "bounds", bounds).await?;
312 }
313 if let Some(center) = &tile_json.center {
314 self.set_metadata_value(conn, "center", center).await?;
315 }
316 if let Some(minzoom) = &tile_json.minzoom {
317 self.set_metadata_value(conn, "minzoom", minzoom).await?;
318 }
319 if let Some(maxzoom) = &tile_json.maxzoom {
320 self.set_metadata_value(conn, "maxzoom", maxzoom).await?;
321 }
322 if let Some(vector_layers) = &tile_json.vector_layers {
323 self.set_metadata_value(
324 conn,
325 "json",
326 &serde_json::to_string(&json!({ "vector_layers": vector_layers }))?,
327 )
328 .await?;
329 }
330
331 Ok(())
332 }
333}
334
335pub async fn anonymous_mbtiles(script: &str) -> (Mbtiles, SqliteConnection) {
337 let mbt = Mbtiles::new(":memory:").expect("in-memory mbtiles can be created");
338 let mut conn = mbt.open().await.expect("in-memory mbtiles can be opened");
339 sqlx::raw_sql(script)
340 .execute(&mut conn)
341 .await
342 .expect("script execution succeeded");
343 (mbt, conn)
344}
345
346#[expect(
348 clippy::panic,
349 reason = "only useful for testing and the debug messages are better with panic"
350)]
351pub async fn temp_named_mbtiles(
352 file_name: &str,
353 script: &str,
354) -> (Mbtiles, SqliteConnection, PathBuf) {
355 let file = PathBuf::from(format!("file:{file_name}?mode=memory&cache=shared"));
356 let mbt =
357 Mbtiles::new(&file).unwrap_or_else(|_| panic!("can create pool for {}", file.display()));
358 let mut conn = mbt
359 .open()
360 .await
361 .unwrap_or_else(|_| panic!("can open connection to {}", file.display()));
362 sqlx::raw_sql(script)
363 .execute(&mut conn)
364 .await
365 .unwrap_or_else(|_| panic!("can execute script on {}", file.display()));
366 (mbt, conn, file)
367}
368
369#[cfg(test)]
370mod tests {
371 use std::collections::BTreeMap;
372
373 use martin_tile_utils::{Encoding, Format, TileInfo};
374 use sqlx::Executor as _;
375 use tilejson::VectorLayer;
376
377 use super::*;
378 use crate::mbtiles::tests::open;
379 use crate::{MbtType, init_mbtiles_schema};
380
381 #[actix_rt::test]
382 async fn mbtiles_meta() {
383 let script = include_str!("../../tests/fixtures/mbtiles/geography-class-jpg.sql");
384 let (mbt, _) = anonymous_mbtiles(script).await;
385 assert_eq!(mbt.filepath(), ":memory:");
386 assert_eq!(mbt.filename(), ":memory:");
387 }
388
389 #[actix_rt::test]
390 async fn metadata_jpeg() {
391 let script = include_str!("../../tests/fixtures/mbtiles/geography-class-jpg.sql");
392 let (mbt, mut conn) = anonymous_mbtiles(script).await;
393 let meta = mbt.get_metadata(&mut conn).await.unwrap();
394
395 let tile_info = mbt.detect_format(&meta.tilejson, &mut conn).await.unwrap();
396 assert_eq!(tile_info, Some(Format::Jpeg.into()));
397
398 let tj = meta.tilejson;
399 assert_eq!(
400 tj.description.unwrap(),
401 "One of the example maps that comes with TileMill - a bright & colorful world map that blends retro and high-tech with its folded paper texture and interactive flag tooltips. "
402 );
403 assert!(tj.legend.unwrap().starts_with("<div style="));
404 assert_eq!(tj.maxzoom.unwrap(), 1);
405 assert_eq!(tj.minzoom.unwrap(), 0);
406 assert_eq!(tj.name.unwrap(), "Geography Class");
407 assert_eq!(tj.template.unwrap(), "foobar");
408 assert_eq!(tj.version.unwrap(), "1.0.0");
409 assert_eq!(meta.id, ":memory:");
410 }
411
412 #[actix_rt::test]
413 async fn metadata_mvt() {
414 let script = include_str!("../../tests/fixtures/mbtiles/world_cities.sql");
415 let (mbt, mut conn) = anonymous_mbtiles(script).await;
416 let meta = mbt.get_metadata(&mut conn).await.unwrap();
417
418 let tile_info = mbt.detect_format(&meta.tilejson, &mut conn).await.unwrap();
419 assert_eq!(tile_info, Some(TileInfo::new(Format::Mvt, Encoding::Gzip)));
420
421 let tj = meta.tilejson;
422
423 assert_eq!(tj.maxzoom.unwrap(), 6);
424 assert_eq!(tj.minzoom.unwrap(), 0);
425 assert_eq!(tj.name.unwrap(), "Major cities from Natural Earth data");
426 assert_eq!(tj.version.unwrap(), "2");
427 assert_eq!(
428 tj.vector_layers,
429 Some(vec![VectorLayer {
430 id: "cities".to_string(),
431 fields: vec![("name".to_string(), "String".to_string())]
432 .into_iter()
433 .collect(),
434 description: Some(String::new()),
435 minzoom: Some(0),
436 maxzoom: Some(6),
437 other: BTreeMap::default()
438 }])
439 );
440 assert_eq!(meta.id, ":memory:");
441 assert_eq!(meta.layer_type, Some("overlay".to_string()));
442 }
443
444 #[actix_rt::test]
445 async fn metadata_get_key() {
446 let script = include_str!("../../tests/fixtures/mbtiles/world_cities.sql");
447 let (mbt, mut conn) = anonymous_mbtiles(script).await;
448 let res = mbt
449 .get_metadata_value(&mut conn, "bounds")
450 .await
451 .unwrap()
452 .unwrap();
453 assert_eq!(res, "-123.123590,-37.818085,174.763027,59.352706");
454 let res = mbt
455 .get_metadata_value(&mut conn, "name")
456 .await
457 .unwrap()
458 .unwrap();
459 assert_eq!(res, "Major cities from Natural Earth data");
460 let res = mbt
461 .get_metadata_value(&mut conn, "maxzoom")
462 .await
463 .unwrap()
464 .unwrap();
465 assert_eq!(res, "6");
466 let res = mbt
467 .get_metadata_value(&mut conn, "nonexistent_key")
468 .await
469 .unwrap();
470 assert_eq!(res, None);
471 let res = mbt.get_metadata_value(&mut conn, "").await.unwrap();
472 assert_eq!(res, None);
473 }
474
475 #[actix_rt::test]
476 async fn metadata_set_key() {
477 let (mut conn, mbt) = open(":memory:").await.unwrap();
478
479 conn.execute("CREATE TABLE metadata (name text NOT NULL PRIMARY KEY, value text);")
480 .await
481 .unwrap();
482
483 mbt.set_metadata_value(&mut conn, "bounds", "0.0, 0.0, 0.0, 0.0")
484 .await
485 .unwrap();
486 assert_eq!(
487 mbt.get_metadata_value(&mut conn, "bounds")
488 .await
489 .unwrap()
490 .unwrap(),
491 "0.0, 0.0, 0.0, 0.0"
492 );
493
494 mbt.set_metadata_value(
495 &mut conn,
496 "bounds",
497 "-123.123590,-37.818085,174.763027,59.352706",
498 )
499 .await
500 .unwrap();
501 assert_eq!(
502 mbt.get_metadata_value(&mut conn, "bounds")
503 .await
504 .unwrap()
505 .unwrap(),
506 "-123.123590,-37.818085,174.763027,59.352706"
507 );
508
509 mbt.delete_metadata_value(&mut conn, "bounds")
510 .await
511 .unwrap();
512 assert_eq!(
513 mbt.get_metadata_value(&mut conn, "bounds").await.unwrap(),
514 None
515 );
516 }
517
518 #[actix_rt::test]
519 async fn metadata_empty_tileset() {
520 let mbt = Mbtiles::new(":memory:").unwrap();
521 let mut conn = mbt.open().await.unwrap();
522 init_mbtiles_schema(&mut conn, MbtType::Flat).await.unwrap();
523
524 let meta = mbt.get_metadata(&mut conn).await;
526 let meta = meta.expect("get_metadata works on empty tileset");
527
528 let tile_info = mbt.detect_format(&meta.tilejson, &mut conn).await.unwrap();
530 assert_eq!(tile_info, None);
531 }
532}