deltamap 0.2.0

A simple map viewer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
use clap;
use directories::ProjectDirs;
use query::QueryArgs;
use session::Session;
use std::fmt::Debug;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use tile_source::TileSource;
use toml::Value;

static DEFAULT_CONFIG: &'static str = "";
static DEFAULT_TILE_SOURCES: &'static str = include_str!("../default_tile_sources.toml");

lazy_static! {
    static ref PROJ_DIRS: Option<ProjectDirs> = ProjectDirs::from("", "", "DeltaMap");
}

fn proj_dirs_result() -> Result<&'static ProjectDirs, String> {
    PROJ_DIRS.as_ref().ok_or_else(|| "could not retrieve project directories".to_string())
}


#[derive(Debug)]
pub struct Config {
    config_file_path: Option<PathBuf>,
    tile_sources_file_path: Option<PathBuf>,
    tile_cache_dir: PathBuf,
    sources: Vec<(String, TileSource)>,
    pbf_path: Option<PathBuf>,
    search_pattern: Option<String>,
    keyval: Option<(String, String)>,
    fps: f64,
    use_network: bool,
    async: bool,
    open_last_session: bool,
}

impl Config {
    //TODO use builder pattern to create config

    pub fn from_arg_matches<'a>(matches: &clap::ArgMatches<'a>) -> Result<Config, String> {
        let mut config = if let Some(config_path) = matches.value_of_os("config") {
            Config::from_toml_file(config_path)?
        } else {
            Config::find_or_create()?
        };

        if let Some(tile_sources_path) = matches.value_of_os("tile-sources") {
            config.add_tile_sources_from_file(tile_sources_path)?;
        } else {
            config.add_tile_sources_from_default_or_create()?;
        };

        if let Some(os_path) = matches.value_of_os("pbf") {
            let path = PathBuf::from(os_path);
            if path.is_file() {
                config.pbf_path = Some(path);
            } else {
                return Err(format!("PBF file does not exist: {:?}", os_path));
            }
        }

        config.merge_arg_matches(matches);

        Ok(config)
    }

    fn merge_arg_matches<'a>(&mut self, matches: &clap::ArgMatches<'a>) {
        self.search_pattern = matches.value_of("search").map(|s| s.to_string());

        self.keyval = matches.value_of("keyval").and_then(|kv| {
            let mut split = kv.split(':');

            if let (Some(key), Some(value)) = (split.next(), split.next()) {
                Some((key.to_string(), value.to_string()))
            } else {
                None
            }
        });

        if let Some(Ok(fps)) = matches.value_of("fps").map(|s| s.parse()) {
            self.fps = fps;
        }

        if matches.is_present("offline") {
            self.use_network = false;
        }

        if matches.is_present("sync") {
            self.async = false;
        }
    }

    fn find_or_create() -> Result<Config, String> {
        let config_dir = proj_dirs_result()?.config_dir();
        let config_file = {
            let mut path = PathBuf::from(config_dir);
            path.push("config.toml");
            path
        };

        if config_file.is_file() {
            info!("load config from path {:?}", config_file);

            Config::from_toml_file(config_file)
        } else {
            // try to write a default config file

            match create_config_file(
                config_dir,
                &config_file,
                DEFAULT_CONFIG.as_bytes()
            ) {
                Err(err) => {
                    warn!("{}", err);
                    Config::from_toml_str::<&str>(DEFAULT_CONFIG, None)
                },
                Ok(()) => {
                    info!("create default config file {:?}", config_file);
                    Config::from_toml_str(DEFAULT_CONFIG, Some(config_file))
                },
            }

        }
    }

    fn add_tile_sources_from_default_or_create(&mut self) -> Result<(), String> {
        let config_dir = proj_dirs_result()?.config_dir();
        let sources_file = {
            let mut path = PathBuf::from(config_dir);
            path.push("tile_sources.toml");
            path
        };

        if sources_file.is_file() {
            info!("load tile sources from path {:?}", sources_file);

            self.add_tile_sources_from_file(sources_file)
        } else {
            // try to write a default config file

            match create_config_file(
                config_dir,
                &sources_file,
                DEFAULT_TILE_SOURCES.as_bytes()
            ) {
                Err(err) => {
                    warn!("{}", err);
                    self.add_tile_sources_from_str::<&str>(DEFAULT_TILE_SOURCES, None)
                },
                Ok(()) => {
                    info!("create default tile sources file {:?}", sources_file);
                    self.add_tile_sources_from_str(DEFAULT_TILE_SOURCES, Some(sources_file))
                },
            }

        }
    }

    fn from_toml_str<P: AsRef<Path>>(toml_str: &str, config_path: Option<P>) -> Result<Config, String> {
        match toml_str.parse::<Value>() {
            Ok(Value::Table(ref table)) => {
                let tile_cache_dir = {
                    match table.get("tile_cache_dir") {
                        Some(dir) => {
                            PathBuf::from(
                                dir.as_str()
                                   .ok_or_else(|| "tile_cache_dir has to be a string".to_string())?
                            )
                        },
                        None => {
                            let mut path = PathBuf::from(proj_dirs_result()?.cache_dir());
                            path.push("tiles");
                            path
                        },
                    }
                };

                let pbf_path = {
                    match table.get("pbf_file") {
                        Some(&Value::String(ref pbf_file)) => {
                            match config_path.as_ref() {
                                Some(config_path) => {
                                    let p = config_path.as_ref().parent()
                                        .ok_or_else(|| "root path is not a valid config file.")?;
                                    let mut p = PathBuf::from(p);
                                    p.push(pbf_file);
                                    p = p.canonicalize().
                                        map_err(|e| format!("pbf_file ({:?}): {}", p, e))?;
                                    Some(p)
                                },
                                None => Some(PathBuf::from(pbf_file)),
                            }
                        },
                        Some(_) => {
                            return Err("pbf_file has to be a string.".to_string());
                        },
                        None => None,
                    }
                };

                let fps = {
                    match table.get("fps") {
                        Some(&Value::Float(fps)) => fps,
                        Some(&Value::Integer(fps)) => fps as f64,
                        Some(_) => return Err("fps has to be an integer or a float.".to_string()),
                        None => 60.0,
                    }
                };

                let use_network = {
                    match table.get("use_network") {
                        Some(&Value::Boolean(x)) => x,
                        Some(_) => return Err("use_network has to be a boolean.".to_string()),
                        None => true,
                    }
                };

                let async = {
                    match table.get("async") {
                        Some(&Value::Boolean(x)) => x,
                        Some(_) => return Err("async has to be a boolean.".to_string()),
                        None => true,
                    }
                };

                let open_last_session = {
                    match table.get("open_last_session") {
                        Some(&Value::Boolean(x)) => x,
                        Some(_) => return Err("open_last_session has to be a boolean.".to_string()),
                        None => false,
                    }
                };

                Ok(
                    Config {
                        config_file_path: config_path.map(|p| PathBuf::from(p.as_ref())),
                        tile_sources_file_path: None,
                        tile_cache_dir,
                        sources: vec![],
                        pbf_path,
                        search_pattern: None,
                        keyval: None,
                        fps,
                        use_network,
                        async,
                        open_last_session,
                    }
                )
            },
            Ok(_) => Err("TOML file has invalid structure. Expected a Table as the top-level element.".to_string()),
            Err(e) => Err(format!("{}", e)),
        }
    }

    fn from_toml_file<P: AsRef<Path>>(path: P) -> Result<Config, String> {
        let mut file = File::open(&path).map_err(|e| format!("{}", e))?;

        let mut content = String::new();
        file.read_to_string(&mut content).map_err(|e| format!("{}", e))?;

        Config::from_toml_str(&content, Some(path))
    }

    fn add_tile_sources_from_str<P>(
        &mut self,
        toml_str: &str,
        file_path: Option<P>
        ) -> Result<(), String>
        where P: AsRef<Path>
    {
        match toml_str.parse::<Value>() {
            Ok(Value::Table(ref table)) => {
                let sources_array = table.get("tile_sources")
                    .ok_or_else(|| "missing \"tile_sources\" table".to_string())?
                    .as_array()
                    .ok_or_else(|| "\"tile_sources\" has to be an array.".to_string())?;

                for (id, source) in sources_array.iter().enumerate() {
                    let name = source.get("name")
                        .ok_or_else(|| "tile_source is missing \"name\" entry.".to_string())?
                        .as_str()
                        .ok_or_else(|| "\"name\" has to be a string".to_string())?;

                    let min_zoom = source.get("min_zoom")
                        .unwrap_or_else(|| &Value::Integer(0))
                        .as_integer()
                        .ok_or_else(|| "min_zoom has to be an integer".to_string())
                        .and_then(|m| {
                            if m < 0 || m > 30 {
                                Err(format!("min_zoom = {} is out of bounds, has to be in interval [0, 30]", m))
                            } else {
                                Ok(m)
                            }
                        })?;

                    let max_zoom = source.get("max_zoom")
                        .ok_or_else(|| format!("source {:?} is missing \"max_zoom\" entry", name))?
                        .as_integer()
                        .ok_or_else(|| "max_zoom has to be an integer".to_string())
                        .and_then(|m| {
                            if m < 0 || m > 30 {
                                Err(format!("max_zoom = {} is out of bounds, has to be in interval [0, 30]", m))
                            } else {
                                Ok(m)
                            }
                        })?;

                    if min_zoom > max_zoom {
                        warn!("min_zoom ({}) and max_zoom ({}) allow no valid tiles", min_zoom, max_zoom);
                    } else if min_zoom == max_zoom {
                        warn!("min_zoom ({}) and max_zoom ({}) allow only one zoom level", min_zoom, max_zoom);
                    }

                    let url_template = source.get("url_template")
                        .ok_or_else(|| format!("source {:?} is missing \"url_template\" entry", name))?
                        .as_str()
                        .ok_or_else(|| "url_template has to be a string".to_string())?;

                    let extension = source.get("extension")
                        .ok_or_else(|| format!("source {:?} is missing \"extension\" entry", name))?
                        .as_str()
                        .ok_or_else(|| "extension has to be a string".to_string())?;

                    //TODO reduce allowed strings to a reasonable subset of valid UTF-8 strings
                    // that can also be used as a directory name or introduce a dir_name key with
                    // more restrictions.
                    if name.contains('/') || name.contains('\\') {
                        return Err(format!("source name ({:?}) must not contain slashes (\"/\" or \"\\\")", name));
                    }

                    let mut path = PathBuf::from(&self.tile_cache_dir);
                    path.push(name);

                    self.sources.push((
                        name.to_string(),
                        TileSource::new(
                            id as u32,
                            url_template.to_string(),
                            path,
                            extension.to_string(),
                            min_zoom as u32,
                            max_zoom as u32,
                        )?,
                    ));
                }

                self.tile_sources_file_path = file_path.map(|p| PathBuf::from(p.as_ref()));
                Ok(())
            },
            Ok(_) => Err("TOML file has invalid structure. Expected a Table as the top-level element.".to_string()),
            Err(e) => Err(format!("{}", e)),
        }
    }

    fn add_tile_sources_from_file<P: AsRef<Path>>(&mut self, path: P) -> Result<(), String> {
        let mut file = File::open(&path).map_err(|e| format!("{}", e))?;

        let mut content = String::new();
        file.read_to_string(&mut content).map_err(|e| format!("{}", e))?;

        self.add_tile_sources_from_str(&content, Some(path))
    }

    pub fn list_paths(&self) {
        let config = match self.config_file_path.as_ref() {
            Some(path) => format!("{:?}", path),
            None => "<None>".to_string(),
        };

        let sources = match self.tile_sources_file_path.as_ref() {
            Some(path) => format!("{:?}", path),
            None => "<None>".to_string(),
        };

        let pbf = match self.pbf_path.as_ref() {
            Some(path) => format!("{:?}", path),
            None => "<None>".to_string(),
        };

        println!("\
            main configuration file: {}\n\
            tile sources file:       {}\n\
            tile cache directory:    {:?}\n\
            OSM PBF file:            {}",
            config,
            sources,
            self.tile_cache_dir,
            pbf,
        );
    }

    pub fn tile_sources(&self) -> &[(String, TileSource)] {
        &self.sources
    }

    pub fn pbf_path(&self) -> Option<&Path> {
        self.pbf_path.as_ref().map(|p| p.as_path())
    }

    pub fn search_pattern(&self) -> Option<&str> {
        self.search_pattern.as_ref().map(|s| s.as_str())
    }

    pub fn keyval(&self) -> Option<(&str, &str)> {
        self.keyval.as_ref().map(|kv| (kv.0.as_str(), kv.1.as_str()))
    }

    pub fn query_args(&self) -> Option<QueryArgs> {
        match (&self.search_pattern, &self.keyval) {
            (&Some(ref pattern), &None) => Some(QueryArgs::ValuePattern(pattern.to_string())),
            (&None, &Some(ref kv)) => Some(QueryArgs::KeyValue(kv.0.to_string(), kv.1.to_string())),
            (&Some(_), &Some(_)) => {
                //TODO implement
                unimplemented!();
            },
            (&None, &None) => None,
        }
    }

    pub fn fps(&self) -> f64 {
        self.fps
    }

    pub fn use_network(&self) -> bool {
        self.use_network
    }

    pub fn async(&self) -> bool {
        self.async
    }

    pub fn open_last_session(&self) -> bool {
        self.open_last_session
    }
}

fn create_config_file<P: AsRef<Path> + Debug>(dir_path: P, file_path: P, contents: &[u8]) -> Result<(), String> {
    if !dir_path.as_ref().is_dir() {
        if let Err(err) = ::std::fs::create_dir_all(&dir_path) {
            return Err(format!("failed to create config directory ({:?}): {}",
                dir_path,
                err
            ));
        }
    }

    let mut file = File::create(&file_path)
        .map_err(|err| format!("failed to create config file {:?}: {}", &file_path, err))?;

    file.write_all(contents)
        .map_err(|err| format!(
            "failed to write contents to config file {:?}: {}",
            &file_path,
            err
        ))
}

pub fn read_last_session() -> Result<Session, String> {
    let session_path = {
        let config_dir = proj_dirs_result()?.config_dir();
        let mut path = PathBuf::from(config_dir);
        path.push("last_session.toml");
        path
    };
    Session::from_toml_file(session_path)
}

pub fn save_session(session: &Session) -> Result<(), String>
{
    let config_dir = proj_dirs_result()?.config_dir();
    let session_path = {
        let mut path = PathBuf::from(config_dir);
        path.push("last_session.toml");
        path
    };
    let contents = session.to_toml_string();
    create_config_file(config_dir, &session_path, contents.as_bytes())
}


#[cfg(test)]
mod tests {
    use config::*;

    #[test]
    fn default_config() {
        let mut config = Config::from_toml_str::<&str>(DEFAULT_CONFIG, None).unwrap();
        config.add_tile_sources_from_str::<&str>(DEFAULT_TILE_SOURCES, None).unwrap();
    }
}