A simple map viewer

config.rs 16KB

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