A simple map viewer

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. use std::fs::File;
  2. use std::io::{Read, Write};
  3. use std::path::{Path, PathBuf};
  4. use tile_source::TileSource;
  5. use toml::Value;
  6. use xdg;
  7. static DEFAULT_CONFIG: &'static str = include_str!("../default_config.toml");
  8. #[derive(Debug)]
  9. pub struct Config {
  10. tile_cache_dir: PathBuf,
  11. sources: Vec<(String, TileSource)>,
  12. fps: f64,
  13. }
  14. impl Config {
  15. pub fn load() -> Result<Config, String> {
  16. if let Ok(xdg_dirs) = xdg::BaseDirectories::with_prefix("deltamap") {
  17. if let Some(config_path) = xdg_dirs.find_config_file("config.toml") {
  18. info!("load config from path {:?}", config_path);
  19. Config::from_toml_file(config_path)
  20. } else {
  21. // try to write a default config file
  22. if let Ok(path) = xdg_dirs.place_config_file("config.toml") {
  23. if let Ok(mut file) = File::create(&path) {
  24. if file.write_all(DEFAULT_CONFIG.as_bytes()).is_ok() {
  25. info!("write default config to {:?}", &path);
  26. }
  27. }
  28. }
  29. Config::from_toml_str(DEFAULT_CONFIG)
  30. }
  31. } else {
  32. info!("load default config");
  33. Config::from_toml_str(DEFAULT_CONFIG)
  34. }
  35. }
  36. /// Returns a tile cache directory path at a standard XDG cache location. The returned path may
  37. /// not exist.
  38. fn default_tile_cache_dir() -> Result<PathBuf, String> {
  39. let xdg_dirs = xdg::BaseDirectories::with_prefix("deltamap")
  40. .map_err(|e| format!("{}", e))?;
  41. match xdg_dirs.find_cache_file("tiles") {
  42. Some(dir) => Ok(dir),
  43. None => Ok(xdg_dirs.get_cache_home().join("tiles")),
  44. }
  45. }
  46. pub fn from_toml_str(toml_str: &str) -> Result<Config, String> {
  47. match toml_str.parse::<Value>() {
  48. Ok(Value::Table(ref table)) => {
  49. let tile_cache_dir = {
  50. match table.get("tile_cache_dir") {
  51. Some(dir) => {
  52. PathBuf::from(
  53. dir.as_str()
  54. .ok_or_else(|| "tile_cache_dir has to be a string".to_string())?
  55. )
  56. },
  57. None => Config::default_tile_cache_dir()?,
  58. }
  59. };
  60. let fps = {
  61. match table.get("fps") {
  62. Some(&Value::Float(fps)) => fps,
  63. Some(&Value::Integer(fps)) => fps as f64,
  64. Some(_) => return Err("fps has to be an integer or a float.".to_string()),
  65. None => 60.0,
  66. }
  67. };
  68. let sources_table = table.get("tile_sources")
  69. .ok_or_else(|| "missing \"tile_sources\" table".to_string())?
  70. .as_table()
  71. .ok_or_else(|| "\"tile_sources\" has to be a table".to_string())?;
  72. let mut sources_vec: Vec<(String, TileSource)> = Vec::with_capacity(sources_table.len());
  73. for (id, (name, source)) in sources_table.iter().enumerate() {
  74. let min_zoom = source.get("min_zoom")
  75. .unwrap_or_else(|| &Value::Integer(0))
  76. .as_integer()
  77. .ok_or_else(|| "min_zoom has to be an integer".to_string())
  78. .and_then(|m| {
  79. if m < 0 || m > 30 {
  80. Err(format!("min_zoom = {} is out of bounds, has to be in interval [0, 30]", m))
  81. } else {
  82. Ok(m)
  83. }
  84. })?;
  85. let max_zoom = source.get("max_zoom")
  86. .ok_or_else(|| format!("source {:?} is missing \"max_zoom\" entry", name))?
  87. .as_integer()
  88. .ok_or_else(|| "max_zoom has to be an integer".to_string())
  89. .and_then(|m| {
  90. if m < 0 || m > 30 {
  91. Err(format!("max_zoom = {} is out of bounds, has to be in interval [0, 30]", m))
  92. } else {
  93. Ok(m)
  94. }
  95. })?;
  96. if min_zoom > max_zoom {
  97. warn!("min_zoom ({}) and max_zoom ({}) allow no valid tiles", min_zoom, max_zoom);
  98. } else if min_zoom == max_zoom {
  99. warn!("min_zoom ({}) and max_zoom ({}) allow only one zoom level", min_zoom, max_zoom);
  100. }
  101. let url_template = source.get("url_template")
  102. .ok_or_else(|| format!("source {:?} is missing \"url_template\" entry", name))?
  103. .as_str()
  104. .ok_or_else(|| "url_template has to be a string".to_string())?;
  105. let extension = source.get("extension")
  106. .ok_or_else(|| format!("source {:?} is missing \"extension\" entry", name))?
  107. .as_str()
  108. .ok_or_else(|| "extension has to be a string".to_string())?;
  109. if name.contains('/') || name.contains('\\') {
  110. return Err(format!("source name ({:?}) must not contain slashes (\"/\" or \"\\\")", name));
  111. }
  112. let mut path = PathBuf::from(&tile_cache_dir);
  113. path.push(name);
  114. sources_vec.push((
  115. name.clone(),
  116. TileSource::new(
  117. id as u32,
  118. url_template.to_string(),
  119. path,
  120. extension.to_string(),
  121. min_zoom as u32,
  122. max_zoom as u32,
  123. ),
  124. ));
  125. }
  126. Ok(
  127. Config {
  128. tile_cache_dir: tile_cache_dir,
  129. sources: sources_vec,
  130. fps: fps,
  131. }
  132. )
  133. },
  134. Ok(_) => Err("TOML file has invalid structure. Expected a Table as the top-level element.".to_string()),
  135. Err(e) => Err(format!("{}", e)),
  136. }
  137. }
  138. pub fn from_toml_file<P: AsRef<Path>>(path: P) -> Result<Config, String> {
  139. let mut file = File::open(path).map_err(|e| format!("{}", e))?;
  140. let mut content = String::new();
  141. file.read_to_string(&mut content).map_err(|e| format!("{}", e))?;
  142. Config::from_toml_str(&content)
  143. }
  144. pub fn tile_sources(&self) -> &[(String, TileSource)] {
  145. &self.sources
  146. }
  147. pub fn fps(&self) -> f64 {
  148. self.fps
  149. }
  150. }
  151. #[cfg(test)]
  152. mod tests {
  153. use config::*;
  154. #[test]
  155. fn default_config() {
  156. assert!(Config::from_toml_str(DEFAULT_CONFIG).is_ok())
  157. }
  158. }