changeset 92:df344c293239

geohub: Refactor and allow for GPX export
author Lewin Bormann <lbo@spheniscida.de>
date Tue, 08 Dec 2020 21:32:26 +0100
parents 18e5120ba5e5
children c683521147c7
files Cargo.lock Cargo.toml README.md TODO src/db.rs src/http.rs src/main.rs src/notifier.rs src/types.rs
diffstat 9 files changed, 240 insertions(+), 30 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.lock	Tue Dec 08 20:49:09 2020 +0100
+++ b/Cargo.lock	Tue Dec 08 21:32:26 2020 +0100
@@ -1,6 +1,21 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
 [[package]]
+name = "addr2line"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
+
+[[package]]
 name = "aead"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -62,6 +77,12 @@
 checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
 
 [[package]]
+name = "assert_approx_eq"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd"
+
+[[package]]
 name = "atty"
 version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -79,6 +100,20 @@
 checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
 
 [[package]]
+name = "backtrace"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598"
+dependencies = [
+ "addr2line",
+ "cfg-if 1.0.0",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
 name = "base64"
 version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -306,6 +341,15 @@
 ]
 
 [[package]]
+name = "error-chain"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
+dependencies = [
+ "backtrace",
+]
+
+[[package]]
 name = "fake-simd"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -389,11 +433,22 @@
 ]
 
 [[package]]
+name = "geo-types"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "866e8f6dbd2218b05ea8a25daa1bfac32b0515fe7e0a37cb6a7b9ed0ed82a07e"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
 name = "geohub"
 version = "0.1.0"
 dependencies = [
  "chrono",
  "fallible-iterator",
+ "geo-types",
+ "gpx",
  "postgres",
  "rocket",
  "rocket_contrib",
@@ -422,12 +477,31 @@
 ]
 
 [[package]]
+name = "gimli"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce"
+
+[[package]]
 name = "glob"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 
 [[package]]
+name = "gpx"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d96f48f1635cee5d19d368d8142fb38b3ca5a6ae8b805f0cf42590751f3b33be"
+dependencies = [
+ "assert_approx_eq",
+ "chrono",
+ "error-chain",
+ "geo-types",
+ "xml-rs",
+]
+
+[[package]]
 name = "hashbrown"
 version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -660,6 +734,16 @@
 ]
 
 [[package]]
+name = "miniz_oxide"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d"
+dependencies = [
+ "adler",
+ "autocfg",
+]
+
+[[package]]
 name = "mio"
 version = "0.6.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -761,6 +845,12 @@
 ]
 
 [[package]]
+name = "object"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397"
+
+[[package]]
 name = "opaque-debug"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1141,6 +1231,12 @@
 ]
 
 [[package]]
+name = "rustc-demangle"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
+
+[[package]]
 name = "ryu"
 version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1519,6 +1615,12 @@
 ]
 
 [[package]]
+name = "xml-rs"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a"
+
+[[package]]
 name = "yansi"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
--- a/Cargo.toml	Tue Dec 08 20:49:09 2020 +0100
+++ b/Cargo.toml	Tue Dec 08 21:32:26 2020 +0100
@@ -14,6 +14,9 @@
 serde_json = "~1.0"
 fallible-iterator = "~0.1"
 
+gpx = "~0.8"
+geo-types = "~0.4"
+
 [dependencies.rocket_contrib]
 version = "~0.4"
 default-features = false
--- a/README.md	Tue Dec 08 20:49:09 2020 +0100
+++ b/README.md	Tue Dec 08 21:32:26 2020 +0100
@@ -178,6 +178,9 @@
     }
 ```
 
+Finally, make a copy of `Rocket.toml.example` to `Rocket.toml`, adapt for your
+needs, and run `cargo run --release`.
+
 ## Usage
 
 ![Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © MapBox](examples/livemap.png)
--- a/TODO	Tue Dec 08 20:49:09 2020 +0100
+++ b/TODO	Tue Dec 08 21:32:26 2020 +0100
@@ -1,13 +1,10 @@
 GENERAL
 
 * UI
- * livemap: show circle radius by accuracy
- * livemap: show time of last update
+    * livemap: link to GPX/GeoJSON export.
 * API
 
 FEATURES
 
-* GPX/json export (with UI + API)
-
 BUGS
 
--- a/src/db.rs	Tue Dec 08 20:49:09 2020 +0100
+++ b/src/db.rs	Tue Dec 08 21:32:26 2020 +0100
@@ -9,7 +9,7 @@
 
 impl<'a> DBQuery<'a> {
     /// Fetch records and format as JSON
-    pub fn retrieve_json(
+    pub fn retrieve(
         &self,
         name: &str,
         from_ts: chrono::DateTime<chrono::Utc>,
@@ -17,15 +17,14 @@
         secret: &Option<String>,
         limit: i64,
         last: Option<i32>,
-    ) -> Result<types::GeoJSON, postgres::Error> {
-        let mut returnable = types::GeoJSON::new();
+    ) -> Result<Vec<types::GeoPoint>, postgres::Error> {
         let stmt = self.0.prepare_cached(
             r"SELECT id, t, lat, long, spd, ele, note, accuracy FROM geohub.geodata
         WHERE (client = $1) and (t between $2 and $3) AND (secret = public.digest($4, 'sha256') or secret is null) AND (id > $5)
         ORDER BY t ASC
         LIMIT $6").unwrap(); // Must succeed.
         let rows = stmt.query(&[&name, &from_ts, &to_ts, &secret, &last.unwrap_or(0), &limit])?;
-        returnable.reserve_features(rows.len());
+        let mut returnable = Vec::with_capacity(rows.len());
         for row in rows.iter() {
             let (id, ts, lat, long, spd, ele, note, acc) = (
                 row.get(0),
@@ -38,6 +37,7 @@
                 row.get(7),
             );
             let point = types::GeoPoint {
+                id: id,
                 lat: lat,
                 long: long,
                 spd: spd,
@@ -46,7 +46,7 @@
                 note: note,
                 time: ts,
             };
-            returnable.push_feature(types::geofeature_from_point(id, point));
+            returnable.push(point);
         }
         Ok(returnable)
     }
@@ -81,8 +81,7 @@
         secret: &Option<String>,
         last: &Option<i32>,
         limit: &Option<i64>,
-    ) -> Option<(types::GeoJSON, i32)> {
-        let mut returnable = types::GeoJSON::new();
+    ) -> Option<(Vec<types::GeoPoint>, i32)> {
         let check_for_new = self.0.prepare_cached(
             r"SELECT id, t, lat, long, spd, ele, note, accuracy FROM geohub.geodata
             WHERE (client = $1) and (id > $2) AND (secret = public.digest($3, 'sha256') or secret is null)
@@ -92,11 +91,12 @@
         let last = last.unwrap_or(0);
         let limit = limit.unwrap_or(256);
 
+        let mut returnable = vec![];
         let rows = check_for_new.query(&[&name, &last, &secret, &limit]);
         if let Ok(rows) = rows {
             // If there are unknown entries, return those.
             if rows.len() > 0 {
-                returnable.reserve_features(rows.len());
+                returnable.reserve(rows.len());
                 let mut last = 0;
 
                 for row in rows.iter() {
@@ -111,6 +111,7 @@
                         row.get(7),
                     );
                     let point = types::GeoPoint {
+                        id: Some(id),
                         time: ts,
                         lat: lat,
                         long: long,
@@ -119,7 +120,7 @@
                         note: note,
                         accuracy: acc,
                     };
-                    returnable.push_feature(types::geofeature_from_point(Some(id), point));
+                    returnable.push(point);
                     if id > last {
                         last = id;
                     }
--- a/src/http.rs	Tue Dec 08 20:49:09 2020 +0100
+++ b/src/http.rs	Tue Dec 08 21:32:26 2020 +0100
@@ -4,18 +4,26 @@
 
 #[derive(Responder)]
 pub enum GeoHubResponse {
+    #[response(status = 200, content_type = "plain")]
+    Ok(String),
     #[response(status = 200, content_type = "json")]
-    Ok(String),
+    Json(String),
+    #[response(status = 200, content_type = "application/xml")]
+    Xml(String),
     #[response(status = 400)]
     BadRequest(String),
     #[response(status = 500)]
     ServerError(String),
 }
 
+pub fn return_xml(xml: String) -> GeoHubResponse {
+    GeoHubResponse::Xml(xml)
+}
+
 pub fn return_json<T: serde::Serialize>(obj: &T) -> GeoHubResponse {
     let json = serde_json::to_string(&obj);
     if let Ok(json) = json {
-        return GeoHubResponse::Ok(json);
+        return GeoHubResponse::Json(json);
     } else {
         return GeoHubResponse::ServerError(json.unwrap_err().to_string());
     }
@@ -25,8 +33,10 @@
     GeoHubResponse::BadRequest(msg)
 }
 
-pub fn server_error(msg: String) -> GeoHubResponse {
-    GeoHubResponse::ServerError(msg)
+use std::fmt::Debug;
+
+pub fn server_error<E: Debug>(err: E) -> GeoHubResponse {
+    GeoHubResponse::ServerError(format!("{:?}", err))
 }
 
 pub fn read_data(d: rocket::Data, limit: u64) -> Result<String, GeoHubResponse> {
--- a/src/main.rs	Tue Dec 08 20:49:09 2020 +0100
+++ b/src/main.rs	Tue Dec 08 21:32:26 2020 +0100
@@ -33,7 +33,8 @@
         secret
     };
     let db = db::DBQuery(&db.0);
-    if let Some((geojson, newlast)) = db.check_for_new_rows(&client, &secret, &last, &limit) {
+    if let Some((points, newlast)) = db.check_for_new_rows(&client, &secret, &last, &limit) {
+        let geojson = types::geojson_from_points(points);
         rocket_contrib::json::Json(types::LiveUpdate::new(
             client,
             Some(newlast),
@@ -90,11 +91,58 @@
     limit: Option<i64>,
     last: Option<i32>,
 ) -> http::GeoHubResponse {
+    let result = common_retrieve(db, client, secret, from, to, limit, last);
+    match result {
+        Ok(points) => {
+            let json = types::geojson_from_points(points);
+            http::return_json(&json)
+        }
+        Err(e) => e,
+    }
+}
+
+/// Retrieve GPX data.
+#[rocket::get("/geo/<client>/retrieve/gpx?<secret>&<from>&<to>&<limit>&<last>")]
+fn retrieve_gpx(
+    db: db::DBConn,
+    client: String,
+    secret: Option<String>,
+    from: Option<String>,
+    to: Option<String>,
+    limit: Option<i64>,
+    last: Option<i32>,
+) -> http::GeoHubResponse {
+    let result = common_retrieve(db, client, secret, from, to, limit, last);
+    match result {
+        Ok(points) => {
+            let gx = types::gpx_track_from_points(points);
+            let mut serialized = vec![];
+            if let Err(he) = gpx::write(&gx, &mut serialized).map_err(http::server_error) {
+                return he;
+            }
+            match String::from_utf8(serialized) {
+                Ok(xml) => http::return_xml(xml),
+                Err(e) => http::server_error(e),
+            }
+        }
+        Err(e) => e,
+    }
+}
+
+fn common_retrieve(
+    db: db::DBConn,
+    client: String,
+    secret: Option<String>,
+    from: Option<String>,
+    to: Option<String>,
+    limit: Option<i64>,
+    last: Option<i32>,
+) -> Result<Vec<types::GeoPoint>, http::GeoHubResponse> {
     if !ids::name_and_secret_acceptable(client.as_str(), secret.as_ref().map(|s| s.as_str())) {
-        return http::bad_request(
+        return Err(http::bad_request(
             "You have supplied an invalid secret or client. Both must be ASCII alphanumeric strings."
                 .into(),
-        );
+        ));
     }
     let secret = if let Some(secret) = secret {
         if secret.is_empty() {
@@ -116,11 +164,10 @@
         .and_then(util::flexible_timestamp_parse)
         .unwrap_or(chrono::Utc::now());
     let limit = limit.unwrap_or(16384);
-
-    let result = db.retrieve_json(client.as_str(), from_ts, to_ts, &secret, limit, last);
+    let result = db.retrieve(client.as_str(), from_ts, to_ts, &secret, limit, last);
     match result {
-        Ok(json) => http::return_json(&json),
-        Err(e) => http::server_error(e.to_string()),
+        Ok(points) => Ok(points),
+        Err(e) => Err(http::server_error(e.to_string())),
     }
 }
 
@@ -183,6 +230,7 @@
     };
 
     let point = types::GeoPoint {
+        id: None,
         lat: lat,
         long: longitude,
         time: ts,
@@ -290,7 +338,15 @@
         ))
         .mount(
             "/",
-            rocket::routes![log, log_json, retrieve_json, retrieve_last, retrieve_live, assets],
+            rocket::routes![
+                log,
+                log_json,
+                retrieve_json,
+                retrieve_gpx,
+                retrieve_last,
+                retrieve_live,
+                assets
+            ],
         )
         .launch();
 }
--- a/src/notifier.rs	Tue Dec 08 20:49:09 2020 +0100
+++ b/src/notifier.rs	Tue Dec 08 21:32:26 2020 +0100
@@ -198,12 +198,13 @@
 
             // These queries use the primary key index returning one row only and will be quite fast.
             let rows = db.check_for_new_rows(client.as_str(), &secret, &None, &Some(nrows.unwrap_or(1)));
-            if let Some((geo, last)) = rows {
+            if let Some((points, last)) = rows {
+                let geojson = types::geojson_from_points(points);
                 for request in clients.remove(&client_id).unwrap_or(vec![]) {
                     request
                         .respond
                         .send(NotifyResponse {
-                            geo: Some(geo.clone()),
+                            geo: Some(geojson.clone()),
                             last: Some(last),
                         })
                         .ok();
--- a/src/types.rs	Tue Dec 08 20:49:09 2020 +0100
+++ b/src/types.rs	Tue Dec 08 21:32:26 2020 +0100
@@ -1,6 +1,10 @@
+use gpx::{self, Gpx};
+use geo_types::Point;
+
 /// Non-JSON plain point representation. Flat and representing a database row.
 #[derive(Debug, Clone)]
 pub struct GeoPoint {
+    pub id: Option<i32>,
     pub lat: f64,
     pub long: f64,
     pub spd: Option<f64>,
@@ -10,6 +14,19 @@
     pub note: Option<String>,
 }
 
+impl GeoPoint {
+    fn to_gpx_waypoint(self) -> gpx::Waypoint {
+        let mut wp = gpx::Waypoint::new(Point::new(self.long, self.lat));
+        wp.elevation = self.ele;
+        wp.speed = self.spd;
+        wp.time = Some(self.time);
+        wp.comment = self.note;
+        wp.hdop = self.accuracy;
+        wp
+    }
+}
+
+/// Returned by the retrieve/live endpoint.
 #[derive(serde::Serialize, Debug)]
 pub struct LiveUpdate {
     #[serde(rename = "type")]
@@ -77,7 +94,7 @@
 pub struct GeoJSON {
     #[serde(rename = "type")]
     typ: String, // always "FeatureCollection"
-    features: Vec<GeoFeature>,
+    pub features: Vec<GeoFeature>,
 }
 
 impl GeoJSON {
@@ -95,11 +112,30 @@
     }
 }
 
-pub fn geofeature_from_point(id: Option<i32>, point: GeoPoint) -> GeoFeature {
+pub fn geojson_from_points(points: Vec<GeoPoint>) -> GeoJSON {
+    let mut gj = GeoJSON::new();
+    gj.features = points.into_iter().map(geofeature_from_point).collect();
+    return gj;
+}
+
+pub fn gpx_track_from_points(points: Vec<GeoPoint>) -> Gpx {
+    let waypoints = points.into_iter().map(GeoPoint::to_gpx_waypoint).collect();
+
+    let mut track_segment = gpx::TrackSegment::new();
+    track_segment.points = waypoints;
+    let mut track = gpx::Track::new();
+    track.segments = vec![track_segment];
+    let mut gx = Gpx::default();
+    gx.tracks = vec![track];
+    gx.version = gpx::GpxVersion::Gpx10;
+    gx
+}
+
+pub fn geofeature_from_point(point: GeoPoint) -> GeoFeature {
     GeoFeature {
         typ: "Feature".into(),
         properties: GeoProperties {
-            id: id,
+            id: point.id,
             time: point.time,
             altitude: point.ele,
             speed: point.spd,
@@ -117,6 +153,7 @@
     let geo = feat.geometry;
     let prop = feat.properties;
     GeoPoint {
+        id: prop.id,
         accuracy: prop.accuracy,
         ele: prop.altitude,
         long: geo.coordinates.0,