Mercurial > lbo > hg > geohub
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,