A simple map viewer

config.rs 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. use clap;
  2. use directories::ProjectDirs;
  3. use std::fmt::Debug;
  4. use std::fs::File;
  5. use std::io::{Read, Write};
  6. use std::path::{Path, PathBuf};
  7. use tile_source::TileSource;
  8. use toml::Value;
  9. static DEFAULT_CONFIG: &'static str = "";
  10. static DEFAULT_TILE_SOURCES: &'static str = include_str!("../default_tile_sources.toml");
  11. lazy_static! {
  12. static ref PROJ_DIRS: ProjectDirs = ProjectDirs::from("", "", "DeltaMap");
  13. }
  14. #[derive(Debug)]
  15. pub struct Config {
  16. tile_cache_dir: PathBuf,
  17. sources: Vec<(String, TileSource)>,
  18. pbf_path: Option<PathBuf>,
  19. search_pattern: Option<String>,
  20. fps: f64,
  21. use_network: bool,
  22. async: bool,
  23. }
  24. impl Config {
  25. //TODO use builder pattern to create config
  26. pub fn from_arg_matches<'a>(matches: &clap::ArgMatches<'a>) -> Result<Config, String> {
  27. let mut config = if let Some(config_path) = matches.value_of_os("config") {
  28. Config::from_toml_file(config_path)?
  29. } else {
  30. Config::find_or_create()?
  31. };
  32. if let Some(tile_sources_path) = matches.value_of_os("tile-sources") {
  33. config.add_tile_sources_from_file(tile_sources_path)?;
  34. } else {
  35. config.add_tile_sources_from_default_or_create()?;
  36. };
  37. if let Some(os_path) = matches.value_of_os("pbf") {
  38. let path = PathBuf::from(os_path);
  39. if path.is_file() {
  40. config.pbf_path = Some(path);
  41. } else {
  42. return Err(format!("PBF file does not exist: {:?}", os_path));
  43. }
  44. }
  45. config.merge_arg_matches(matches);
  46. Ok(config)
  47. }
  48. fn merge_arg_matches<'a>(&mut self, matches: &clap::ArgMatches<'a>) {
  49. self.search_pattern = matches.value_of("search").map(|s| s.to_string());
  50. if let Some(Ok(fps)) = matches.value_of("fps").map(|s| s.parse()) {
  51. self.fps = fps;
  52. }
  53. if matches.is_present("offline") {
  54. self.use_network = false;
  55. }
  56. if matches.is_present("sync") {
  57. self.async = false;
  58. }
  59. }
  60. fn create_config_file<P: AsRef<Path> + Debug>(dir_path: P, file_path: P, contents: &[u8]) -> Result<(), String> {
  61. if !dir_path.as_ref().is_dir() {
  62. if let Err(err) = ::std::fs::create_dir_all(&dir_path) {
  63. return Err(format!("failed to create config directory ({:?}): {}",
  64. dir_path,
  65. err
  66. ));
  67. }
  68. }
  69. let mut file = File::create(&file_path)
  70. .map_err(|err| format!("failed to create config file {:?}: {}", &file_path, err))?;
  71. file.write_all(contents)
  72. .map_err(|err| format!(
  73. "failed to write contents to config file {:?}: {}",
  74. &file_path,
  75. err
  76. ))
  77. }
  78. fn find_or_create() -> Result<Config, String> {
  79. let config_dir = PROJ_DIRS.config_dir();
  80. let config_file = {
  81. let mut path = PathBuf::from(config_dir);
  82. path.push("config.toml");
  83. path
  84. };
  85. if config_file.is_file() {
  86. info!("load config from path {:?}", config_file);
  87. Config::from_toml_file(config_file)
  88. } else {
  89. // try to write a default config file
  90. match Config::create_config_file(
  91. config_dir,
  92. &config_file,
  93. DEFAULT_CONFIG.as_bytes()
  94. ) {
  95. Err(err) => warn!("{}", err),
  96. Ok(()) => info!("create default config file {:?}", config_file),
  97. }
  98. Config::from_toml_str(DEFAULT_CONFIG)
  99. }
  100. }
  101. fn add_tile_sources_from_default_or_create(&mut self) -> Result<(), String> {
  102. let config_dir = PROJ_DIRS.config_dir();
  103. let sources_file = {
  104. let mut path = PathBuf::from(config_dir);
  105. path.push("tile_sources.toml");
  106. path
  107. };
  108. if sources_file.is_file() {
  109. info!("load tile sources from path {:?}", sources_file);
  110. self.add_tile_sources_from_file(sources_file)
  111. } else {
  112. // try to write a default config file
  113. match Config::create_config_file(
  114. config_dir,
  115. &sources_file,
  116. DEFAULT_TILE_SOURCES.as_bytes()
  117. ) {
  118. Err(err) => warn!("{}", err),
  119. Ok(()) => info!("create default tile sources file {:?}", sources_file),
  120. }
  121. self.add_tile_sources_from_str(DEFAULT_TILE_SOURCES)
  122. }
  123. }
  124. /// Returns a tile cache directory path at a standard location. The returned path may not
  125. /// exist.
  126. fn default_tile_cache_dir() -> PathBuf {
  127. let mut path = PathBuf::from(PROJ_DIRS.cache_dir());
  128. path.push("tiles");
  129. path
  130. }
  131. fn from_toml_str(toml_str: &str) -> Result<Config, String> {
  132. match toml_str.parse::<Value>() {
  133. Ok(Value::Table(ref table)) => {
  134. let tile_cache_dir = {
  135. match table.get("tile_cache_dir") {
  136. Some(dir) => {
  137. PathBuf::from(
  138. dir.as_str()
  139. .ok_or_else(|| "tile_cache_dir has to be a string".to_string())?
  140. )
  141. },
  142. None => Config::default_tile_cache_dir(),
  143. }
  144. };
  145. let fps = {
  146. match table.get("fps") {
  147. Some(&Value::Float(fps)) => fps,
  148. Some(&Value::Integer(fps)) => fps as f64,
  149. Some(_) => return Err("fps has to be an integer or a float.".to_string()),
  150. None => 60.0,
  151. }
  152. };
  153. let use_network = {
  154. match table.get("use_network") {
  155. Some(&Value::Boolean(x)) => x,
  156. Some(_) => return Err("use_network has to be a boolean.".to_string()),
  157. None => true,
  158. }
  159. };
  160. let async = {
  161. match table.get("async") {
  162. Some(&Value::Boolean(x)) => x,
  163. Some(_) => return Err("async has to be a boolean.".to_string()),
  164. None => true,
  165. }
  166. };
  167. Ok(
  168. Config {
  169. tile_cache_dir,
  170. sources: vec![],
  171. pbf_path: None,
  172. search_pattern: None,
  173. fps,
  174. use_network,
  175. async,
  176. }
  177. )
  178. },
  179. Ok(_) => Err("TOML file has invalid structure. Expected a Table as the top-level element.".to_string()),
  180. Err(e) => Err(format!("{}", e)),
  181. }
  182. }
  183. fn from_toml_file<P: AsRef<Path>>(path: P) -> Result<Config, String> {
  184. let mut file = File::open(path).map_err(|e| format!("{}", e))?;
  185. let mut content = String::new();
  186. file.read_to_string(&mut content).map_err(|e| format!("{}", e))?;
  187. Config::from_toml_str(&content)
  188. }
  189. fn add_tile_sources_from_str(&mut self, toml_str: &str) -> Result<(), String> {
  190. match toml_str.parse::<Value>() {
  191. Ok(Value::Table(ref table)) => {
  192. let sources_array = table.get("tile_sources")
  193. .ok_or_else(|| "missing \"tile_sources\" table".to_string())?
  194. .as_array()
  195. .ok_or_else(|| "\"tile_sources\" has to be an array.".to_string())?;
  196. for (id, source) in sources_array.iter().enumerate() {
  197. let name = source.get("name")
  198. .ok_or_else(|| "tile_source is missing \"name\" entry.".to_string())?
  199. .as_str()
  200. .ok_or_else(|| "\"name\" has to be a string".to_string())?;
  201. let min_zoom = source.get("min_zoom")
  202. .unwrap_or_else(|| &Value::Integer(0))
  203. .as_integer()
  204. .ok_or_else(|| "min_zoom has to be an integer".to_string())
  205. .and_then(|m| {
  206. if m < 0 || m > 30 {
  207. Err(format!("min_zoom = {} is out of bounds, has to be in interval [0, 30]", m))
  208. } else {
  209. Ok(m)
  210. }
  211. })?;
  212. let max_zoom = source.get("max_zoom")
  213. .ok_or_else(|| format!("source {:?} is missing \"max_zoom\" entry", name))?
  214. .as_integer()
  215. .ok_or_else(|| "max_zoom has to be an integer".to_string())
  216. .and_then(|m| {
  217. if m < 0 || m > 30 {
  218. Err(format!("max_zoom = {} is out of bounds, has to be in interval [0, 30]", m))
  219. } else {
  220. Ok(m)
  221. }
  222. })?;
  223. if min_zoom > max_zoom {
  224. warn!("min_zoom ({}) and max_zoom ({}) allow no valid tiles", min_zoom, max_zoom);
  225. } else if min_zoom == max_zoom {
  226. warn!("min_zoom ({}) and max_zoom ({}) allow only one zoom level", min_zoom, max_zoom);
  227. }
  228. let url_template = source.get("url_template")
  229. .ok_or_else(|| format!("source {:?} is missing \"url_template\" entry", name))?
  230. .as_str()
  231. .ok_or_else(|| "url_template has to be a string".to_string())?;
  232. let extension = source.get("extension")
  233. .ok_or_else(|| format!("source {:?} is missing \"extension\" entry", name))?
  234. .as_str()
  235. .ok_or_else(|| "extension has to be a string".to_string())?;
  236. //TODO reduce allowed strings to a reasonable subset of valid UTF-8 strings
  237. // that can also be used as a directory name or introduce a dir_name key with
  238. // more restrictions.
  239. if name.contains('/') || name.contains('\\') {
  240. return Err(format!("source name ({:?}) must not contain slashes (\"/\" or \"\\\")", name));
  241. }
  242. let mut path = PathBuf::from(&self.tile_cache_dir);
  243. path.push(name);
  244. self.sources.push((
  245. name.to_string(),
  246. TileSource::new(
  247. id as u32,
  248. url_template.to_string(),
  249. path,
  250. extension.to_string(),
  251. min_zoom as u32,
  252. max_zoom as u32,
  253. )?,
  254. ));
  255. }
  256. Ok(())
  257. },
  258. Ok(_) => Err("TOML file has invalid structure. Expected a Table as the top-level element.".to_string()),
  259. Err(e) => Err(format!("{}", e)),
  260. }
  261. }
  262. fn add_tile_sources_from_file<P: AsRef<Path>>(&mut self, path: P) -> Result<(), String> {
  263. let mut file = File::open(path).map_err(|e| format!("{}", e))?;
  264. let mut content = String::new();
  265. file.read_to_string(&mut content).map_err(|e| format!("{}", e))?;
  266. self.add_tile_sources_from_str(&content)
  267. }
  268. pub fn tile_sources(&self) -> &[(String, TileSource)] {
  269. &self.sources
  270. }
  271. pub fn pbf_path(&self) -> Option<&Path> {
  272. self.pbf_path.as_ref().map(|p| p.as_path())
  273. }
  274. pub fn search_pattern(&self) -> Option<&str> {
  275. self.search_pattern.as_ref().map(|s| s.as_str())
  276. }
  277. pub fn fps(&self) -> f64 {
  278. self.fps
  279. }
  280. pub fn use_network(&self) -> bool {
  281. self.use_network
  282. }
  283. pub fn async(&self) -> bool {
  284. self.async
  285. }
  286. }
  287. #[cfg(test)]
  288. mod tests {
  289. use config::*;
  290. #[test]
  291. fn default_config() {
  292. let mut config = Config::from_toml_str(DEFAULT_CONFIG).unwrap();
  293. config.add_tile_sources_from_str(DEFAULT_TILE_SOURCES).unwrap();
  294. }
  295. }