A simple map viewer

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