diff --git a/apps/gipy/TODO b/apps/gipy/TODO index a5d534b63..8149d2636 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -2,6 +2,9 @@ - gps direction is weak when speed is low - add a version number to gpc files - water points + ---> we group them following path by groups of cst_size and record segments ids marking limits + +- gps wait screen - store several tracks - turn off gps when moving to next waypoint diff --git a/apps/gipy/app.js b/apps/gipy/app.js index a4c005073..2196246c8 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -38,7 +38,7 @@ class Status { // detect segment we are on now let next_segment = this.path.nearest_segment(this.position, Math.max(0, this.current_segment-1), Math.min(this.current_segment+2, path.len - 1)); - + if (this.is_lost(next_segment)) { // it did not work, try anywhere next_segment = this.path.nearest_segment(this.position, 0, path.len - 1); @@ -67,7 +67,12 @@ class Status { this.distance_to_next_point = Math.ceil(this.position.distance(this.path.point(next_point))); if (this.reaching != next_point && this.distance_to_next_point <= 20) { this.reaching = next_point; - Bangle.buzz(); + if (Bangle.isLocked()) { + if (this.path.is_waypoint(next_point)) { + Bangle.buzz(); + Bangle.setLocked(false); + } + } } // re-display unless locked @@ -128,6 +133,12 @@ class Status { for (let i = start ; i < end + 1 ; i++) { let p = this.path.point(i); let c = p.coordinates(pos, cos, sin); + if (this.path.is_waypoint(i)) { + g.setColor(g.theme.fg); + g.fillCircle(c[0], c[1], 6); + g.setColor(g.theme.bg); + g.fillCircle(c[0], c[1], 5); + } g.setColor(g.theme.fg); g.fillCircle(c[0], c[1], 4); g.setColor(g.theme.bg); @@ -146,6 +157,24 @@ class Path { this.points = Float64Array(buffer); } + // if start, end or steep direction change + // we are buzzing and displayed specially + is_waypoint(point_index) { + if ((point_index == 0)||(point_index == this.len -1)) { + return true; + } else { + let p1 = this.point(point_index-1); + let p2 = this.point(point_index); + let p3 = this.point(point_index+1); + let d1 = p2.minus(p1); + let d2 = p3.minus(p2); + let a1 = Math.atan2(d1.lat, d1.lon); + let a2 = Math.atan2(d2.lat, d2.lon); + let direction_change = Math.abs(a2-a1); + return (direction_change > Math.PI / 3.0); + } + } + // execute op on all segments. // start is index of first wanted segment // end is 1 after index of last wanted segment @@ -293,3 +322,4 @@ if (simulated) { Bangle.setGPSPower(true, "gipy"); Bangle.on('GPS', set_coordinates); } + diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 74acaa8e5..a6033bfb2 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -7,95 +7,55 @@ use std::path::Path; use gpx::read; use gpx::Gpx; -use lazy_static::lazy_static; -use openstreetmap_api::{ - types::{BoundingBox, Credentials}, - Openstreetmap, -}; +mod osm; +use osm::InterestPoint; -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -enum Interest { - Bakery, - DrinkingWater, - Toilets, - BikeShop, - ChargingStation, - Bank, - Supermarket, - Table, - // TourismOffice, - Artwork, - Pharmacy, +#[derive(Debug, PartialEq, Clone, Copy)] +pub struct Point { + x: f64, + y: f64, } -#[derive(Debug, PartialEq)] -struct InterestPoint { - lat: f64, - lon: f64, - interest: Interest, -} - -impl Eq for InterestPoint {} -impl std::hash::Hash for InterestPoint { +impl Eq for Point {} +impl std::hash::Hash for Point { fn hash(&self, state: &mut H) { - unsafe { std::mem::transmute::(self.lat) }.hash(state); - unsafe { std::mem::transmute::(self.lon) }.hash(state); - self.interest.hash(state); + unsafe { std::mem::transmute::(self.x) }.hash(state); + unsafe { std::mem::transmute::(self.y) }.hash(state); } } -lazy_static! { - static ref INTERESTS: HashMap<(&'static str, &'static str), Interest> = { - [ - (("shop", "bakery"), Interest::Bakery), - (("amenity", "drinking_water"), Interest::DrinkingWater), - (("amenity", "toilets"), Interest::Toilets), - (("shop", "bicycle"), Interest::BikeShop), - (("amenity", "charging_station"), Interest::ChargingStation), - (("amenity", "bank"), Interest::Bank), - (("shop", "supermarket"), Interest::Supermarket), - (("leisure", "picnic_table"), Interest::Table), - // (("tourism", "information"), Interest::TourismOffice), - (("tourism", "artwork"), Interest::Artwork), - (("amenity", "pharmacy"), Interest::Pharmacy), - ] - .into_iter() - .collect() - }; -} - -impl Interest { - fn new(key: &str, value: &str) -> Option { - INTERESTS.get(&(key, value)).cloned() +impl Point { + fn squared_distance_between(&self, other: &Point) -> f64 { + let dx = other.x - self.x; + let dy = other.y - self.y; + dx * dx + dy * dy } -} - -impl InterestPoint { - fn color(&self) -> &'static str { - match self.interest { - Interest::Bakery => "red", - Interest::DrinkingWater => "blue", - Interest::Toilets => "brown", - Interest::BikeShop => "purple", - Interest::ChargingStation => "green", - Interest::Bank => "black", - Interest::Supermarket => "red", - Interest::Table => "pink", - Interest::Artwork => "orange", - Interest::Pharmacy => "chartreuse", + fn distance_to_segment(&self, v: &Point, w: &Point) -> f64 { + let l2 = v.squared_distance_between(w); + if l2 == 0.0 { + return self.squared_distance_between(v).sqrt(); } + // Consider the line extending the segment, parameterized as v + t (w - v). + // We find projection of point p onto the line. + // It falls where t = [(p-v) . (w-v)] / |w-v|^2 + // We clamp t from [0,1] to handle points outside the segment vw. + let x0 = self.x - v.x; + let y0 = self.y - v.y; + let x1 = w.x - v.x; + let y1 = w.y - v.y; + let dot = x0 * x1 + y0 * y1; + let t = (dot / l2).min(1.0).max(0.0); + + let proj = Point { + x: v.x + x1 * t, + y: v.y + y1 * t, + }; + + proj.squared_distance_between(self).sqrt() } } -fn squared_distance_between(p1: &(f64, f64), p2: &(f64, f64)) -> f64 { - let (x1, y1) = *p1; - let (x2, y2) = *p2; - let dx = x2 - x1; - let dy = y2 - y1; - dx * dx + dy * dy -} - -fn points(filename: &str) -> impl Iterator { +fn points(filename: &str) -> impl Iterator { // This XML file actually exists — try it for yourself! let file = File::open(filename).unwrap(); let reader = BufReader::new(file); @@ -112,28 +72,7 @@ fn points(filename: &str) -> impl Iterator { .flat_map(|segment| segment.linestring().points().collect::>()) .map(|point| (point.x(), point.y())) .dedup() -} - -fn distance_to_segment(p: &(f64, f64), v: &(f64, f64), w: &(f64, f64)) -> f64 { - let l2 = squared_distance_between(v, w); - if l2 == 0.0 { - return squared_distance_between(p, v).sqrt(); - } - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. - let x0 = p.0 - v.0; - let y0 = p.1 - v.1; - let x1 = w.0 - v.0; - let y1 = w.1 - v.1; - let dot = x0 * x1 + y0 * y1; - let t = (dot / l2).min(1.0).max(0.0); - - let proj_x = v.0 + x1 * t; - let proj_y = v.1 + y1 * t; - - squared_distance_between(&(proj_x, proj_y), p).sqrt() + .map(|(x, y)| Point { x, y }) } // // NOTE: this angles idea could maybe be use to get dp from n^3 to n^2 @@ -199,11 +138,11 @@ fn distance_to_segment(p: &(f64, f64), v: &(f64, f64), w: &(f64, f64)) -> f64 { // } fn extract_prog_dyn_solution( - points: &[(f64, f64)], + points: &[Point], start: usize, end: usize, cache: &HashMap<(usize, usize), (Option, usize)>, -) -> Vec<(f64, f64)> { +) -> Vec { if let Some(choice) = cache.get(&(start, end)).unwrap().0 { let mut v1 = extract_prog_dyn_solution(points, start, choice + 1, cache); let mut v2 = extract_prog_dyn_solution(points, choice, end, cache); @@ -216,7 +155,7 @@ fn extract_prog_dyn_solution( } fn simplify_prog_dyn( - points: &[(f64, f64)], + points: &[Point], start: usize, end: usize, epsilon: f64, @@ -234,7 +173,7 @@ fn simplify_prog_dyn( if points[(start + 1)..end] .iter() - .map(|p| distance_to_segment(p, first_point, last_point)) + .map(|p| p.distance_to_segment(first_point, last_point)) .all(|d| d <= epsilon) { (None, 2) @@ -255,7 +194,7 @@ fn simplify_prog_dyn( } } -fn rdp(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { +fn rdp(points: &[Point], epsilon: f64) -> Vec { if points.len() <= 2 { points.iter().copied().collect() } else { @@ -266,8 +205,9 @@ fn rdp(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { .enumerate() .skip(1) .max_by(|(_, p1), (_, p2)| { - squared_distance_between(first, p1) - .partial_cmp(&squared_distance_between(first, p2)) + first + .squared_distance_between(p1) + .partial_cmp(&first.squared_distance_between(p2)) .unwrap() }) .map(|(i, _)| i) @@ -282,7 +222,7 @@ fn rdp(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { } else { let (index_farthest, farthest_distance) = points .iter() - .map(|p| distance_to_segment(p, points.first().unwrap(), points.last().unwrap())) + .map(|p| p.distance_to_segment(points.first().unwrap(), points.last().unwrap())) .enumerate() .max_by(|(_, d1), (_, d2)| { if d1.is_nan() { @@ -313,7 +253,7 @@ fn rdp(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { } } -fn simplify_path(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { +fn simplify_path(points: &[Point], epsilon: f64) -> Vec { if points.len() <= 600 { optimal_simplification(points, epsilon) } else { @@ -384,13 +324,7 @@ fn compress_coordinates(points: &[(i32, i32)]) -> Vec<(i16, i16)> { xdiffs.zip(ydiffs).collect() } -fn save_coordinates>( - path: P, - //xmin: f64, - //ymin: f64, - // points: &[(i32, i32)], - points: &[(f64, f64)], -) -> std::io::Result<()> { +fn save_coordinates>(path: P, points: &[Point]) -> std::io::Result<()> { let mut writer = BufWriter::new(File::create(path)?); eprintln!("saving {} points", points.len()); @@ -398,19 +332,19 @@ fn save_coordinates>( // writer.write_all(&ymin.to_be_bytes())?; points .iter() - .flat_map(|(x, y)| [x, y]) + .flat_map(|p| [p.x, p.y]) .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; Ok(()) } -fn optimal_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { +fn optimal_simplification(points: &[Point], epsilon: f64) -> Vec { let mut cache = HashMap::new(); simplify_prog_dyn(&points, 0, points.len(), epsilon, &mut cache); extract_prog_dyn_solution(&points, 0, points.len(), &cache) } -fn hybrid_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { +fn hybrid_simplification(points: &[Point], epsilon: f64) -> Vec { if points.len() <= 300 { optimal_simplification(points, epsilon) } else { @@ -421,8 +355,9 @@ fn hybrid_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> .enumerate() .skip(1) .max_by(|(_, p1), (_, p2)| { - squared_distance_between(first, p1) - .partial_cmp(&squared_distance_between(first, p2)) + first + .squared_distance_between(p1) + .partial_cmp(&first.squared_distance_between(p2)) .unwrap() }) .map(|(i, _)| i) @@ -437,7 +372,7 @@ fn hybrid_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> } else { let (index_farthest, farthest_distance) = points .iter() - .map(|p| distance_to_segment(p, points.first().unwrap(), points.last().unwrap())) + .map(|p| p.distance_to_segment(points.first().unwrap(), points.last().unwrap())) .enumerate() .max_by(|(_, d1), (_, d2)| { if d1.is_nan() { @@ -468,73 +403,36 @@ fn hybrid_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> } } -async fn get_openstreetmap_data(points: &[(f64, f64)]) -> HashSet { - let osm = Openstreetmap::new("https://openstreetmap.org", Credentials::None); - let mut interest_points = HashSet::new(); - let border = 0.0001; - for (&(x1, y1), &(x2, y2)) in points.iter().tuple_windows() { - let left = x1.min(x2) - border; - let right = x1.max(x2) + border; - let bottom = y1.min(y2) - border; - let top = y1.max(y2) + border; - if let Ok(map) = osm - .map(&BoundingBox { - bottom, - left, - top, - right, - }) - .await - { - let points = map.nodes.iter().flat_map(|n| { - n.tags.iter().filter_map(|t| { - let latlon = n.lat.and_then(|lat| n.lon.map(|lon| (lat, lon))); - latlon.and_then(|(lat, lon)| { - Interest::new(&t.k, &t.v).map(|i| InterestPoint { - lat, - lon, - interest: i, - }) - }) - }) - }); - interest_points.extend(points) - } else { - eprintln!("failed retrieving osm data") - } - } - interest_points -} - -fn save_path(writer: &mut W, p: &[(f64, f64)], stroke: &str) -> std::io::Result<()> { +fn save_path(writer: &mut W, p: &[Point], stroke: &str) -> std::io::Result<()> { write!( writer, "")?; Ok(()) } fn save_svg>( filename: P, - p: &[(f64, f64)], - rp: &[(f64, f64)], + p: &[Point], + rp: &[Point], interest_points: &HashSet, + waypoints: &HashSet, ) -> std::io::Result<()> { let mut writer = BufWriter::new(std::fs::File::create(filename)?); let (xmin, xmax) = p .iter() - .map(|&(x, _)| x) + .map(|p| p.x) .minmax_by(|a, b| a.partial_cmp(b).unwrap()) .into_option() .unwrap(); let (ymin, ymax) = p .iter() - .map(|&(_, y)| y) + .map(|p| p.y) .minmax_by(|a, b| a.partial_cmp(b).unwrap()) .into_option() .unwrap(); @@ -563,15 +461,54 @@ fn save_svg>( writeln!( &mut writer, "", - point.lon, - point.lat, + point.point.x, + point.point.y, point.color(), )?; } + + let rpoints = rp.iter().cloned().collect::>(); + waypoints.difference(&rpoints).try_for_each(|p| { + writeln!( + &mut writer, + "", + p.x, p.y, + ) + })?; + waypoints.intersection(&rpoints).try_for_each(|p| { + writeln!( + &mut writer, + "", + p.x, p.y, + ) + })?; + writeln!(&mut writer, "")?; Ok(()) } +fn detect_waypoints(points: &[Point]) -> HashSet { + points + .first() + .into_iter() + .chain(points.iter().tuple_windows().filter_map(|(p1, p2, p3)| { + let x1 = p2.x - p1.x; + let y1 = p2.y - p1.y; + let a1 = y1.atan2(x1); + let x2 = p3.x - p2.x; + let y2 = p3.y - p2.y; + let a2 = y2.atan2(x2); + if (a2 - a1).abs() <= std::f64::consts::PI / 3.0 { + None + } else { + Some(p2) + } + })) + .chain(points.last().into_iter()) + .copied() + .collect::>() +} + #[tokio::main] async fn main() { let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); @@ -580,6 +517,7 @@ async fn main() { eprintln!("initialy we have {} points", p.len()); let start = std::time::Instant::now(); let rp = simplify_path(&p, 0.00015); + let waypoints = detect_waypoints(&rp); eprintln!("we took {:?}", start.elapsed()); eprintln!("we now have {} points", rp.len()); let start = std::time::Instant::now(); @@ -589,5 +527,5 @@ async fn main() { save_coordinates("test.gpc", &rp).unwrap(); // let i = get_openstreetmap_data(&rp).await; let i = HashSet::new(); - save_svg("test.svg", &p, &rp, &i).unwrap(); + save_svg("test.svg", &p, &rp, &i, &waypoints).unwrap(); } diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs new file mode 100644 index 000000000..a70e7ff5d --- /dev/null +++ b/apps/gipy/gpconv/src/osm.rs @@ -0,0 +1,130 @@ +use super::Point; +use lazy_static::lazy_static; +use openstreetmap_api::{ + types::{BoundingBox, Credentials}, + Openstreetmap, +}; +use std::collections::{HashMap, HashSet}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Interest { + Bakery, + DrinkingWater, + Toilets, + BikeShop, + ChargingStation, + Bank, + Supermarket, + Table, + // TourismOffice, + Artwork, + Pharmacy, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct InterestPoint { + pub point: Point, + pub interest: Interest, +} + +lazy_static! { + static ref INTERESTS: HashMap<(&'static str, &'static str), Interest> = { + [ + (("shop", "bakery"), Interest::Bakery), + (("amenity", "drinking_water"), Interest::DrinkingWater), + (("amenity", "toilets"), Interest::Toilets), + (("shop", "bicycle"), Interest::BikeShop), + (("amenity", "charging_station"), Interest::ChargingStation), + (("amenity", "bank"), Interest::Bank), + (("shop", "supermarket"), Interest::Supermarket), + (("leisure", "picnic_table"), Interest::Table), + // (("tourism", "information"), Interest::TourismOffice), + (("tourism", "artwork"), Interest::Artwork), + (("amenity", "pharmacy"), Interest::Pharmacy), + ] + .into_iter() + .collect() + }; +} + +impl Interest { + fn new(key: &str, value: &str) -> Option { + INTERESTS.get(&(key, value)).cloned() + } +} + +impl InterestPoint { + pub fn color(&self) -> &'static str { + match self.interest { + Interest::Bakery => "red", + Interest::DrinkingWater => "blue", + Interest::Toilets => "brown", + Interest::BikeShop => "purple", + Interest::ChargingStation => "green", + Interest::Bank => "black", + Interest::Supermarket => "red", + Interest::Table => "pink", + Interest::Artwork => "orange", + Interest::Pharmacy => "chartreuse", + } + } +} + +async fn get_openstreetmap_data(points: &[(f64, f64)]) -> HashSet { + let osm = Openstreetmap::new("https://openstreetmap.org", Credentials::None); + let mut interest_points = HashSet::new(); + let border = 0.0001; + let mut boxes = Vec::new(); + let max_size = 0.005; + points.iter().fold( + (std::f64::MAX, std::f64::MIN, std::f64::MAX, std::f64::MIN), + |in_box, &(x, y)| { + let (mut xmin, mut xmax, mut ymin, mut ymax) = in_box; + xmin = xmin.min(x); + xmax = xmax.max(x); + ymin = ymin.min(y); + ymax = ymax.max(y); + if (xmax - xmin > max_size) || (ymax - ymin > max_size) { + boxes.push(in_box); + (x, x, y, y) + } else { + (xmin, xmax, ymin, ymax) + } + }, + ); + eprintln!("we need {} requests to openstreetmap", boxes.len()); + for (xmin, xmax, ymin, ymax) in boxes { + let left = xmin - border; + let right = xmax + border; + let bottom = ymin - border; + let top = ymax + border; + match osm + .map(&BoundingBox { + bottom, + left, + top, + right, + }) + .await + { + Ok(map) => { + let points = map.nodes.iter().flat_map(|n| { + n.tags.iter().filter_map(|t| { + let latlon = n.lat.and_then(|lat| n.lon.map(|lon| (lat, lon))); + latlon.and_then(|(lat, lon)| { + Interest::new(&t.k, &t.v).map(|i| InterestPoint { + point: Point { x: lon, y: lat }, + interest: i, + }) + }) + }) + }); + interest_points.extend(points) + } + Err(e) => { + eprintln!("failed retrieving osm data: {:?}", e); + } + } + } + interest_points +}