use super::funejson::Value; /// Whether `point` lies inside `polygon`, by ray casting on the lng/lat plane. /// /// A point exactly on an edge or a vertex counts as inside: a delivery address /// on the boundary line of a zone is in the zone. That is checked explicitly /// first, because plain ray casting answers boundary points inconsistently. /// /// Only + - * / and comparisons are used, in the same order in every language, /// so the answer is identical in Rust, TypeScript and Python even where /// floating point rounding decides it. /// /// # Panics /// Panics if a coordinate is out of range or the polygon has fewer than 3 /// distinct vertices. pub fn point_in_polygon(point: &GeoPoint, polygon: &[GeoPoint]) -> bool { check_point(point); for vertex in polygon { check_point(vertex); } let mut ring: &[GeoPoint] = polygon; // A closed ring repeats its first vertex at the end; that is not an edge. if ring.len() >= 2 && same(&ring[0], &ring[ring.len() - 1]) { ring = &ring[..ring.len() - 1]; } if count_distinct(ring) < 3 { panic!("a polygon needs at least 3 distinct vertices"); } let n = ring.len(); for i in 0..n { if on_segment(point, &ring[i], &ring[(i + 1) % n]) { return true; } } let mut inside = false; for i in 0..n { let a = &ring[i]; let b = &ring[(i + 1) % n]; // Half-open test so a ray passing exactly through a vertex counts it once. if (a.lat > point.lat) != (b.lat > point.lat) { let cross_lng = a.lng + ((point.lat - a.lat) * (b.lng - a.lng)) / (b.lat - a.lat); if point.lng < cross_lng { inside = !inside; } } } inside } fn on_segment(p: &GeoPoint, a: &GeoPoint, b: &GeoPoint) -> bool { let cross = (b.lng - a.lng) * (p.lat - a.lat) - (b.lat - a.lat) * (p.lng - a.lng); if cross != 0.0 { return false; } p.lng >= a.lng.min(b.lng) && p.lng <= a.lng.max(b.lng) && p.lat >= a.lat.min(b.lat) && p.lat <= a.lat.max(b.lat) } fn same(a: &GeoPoint, b: &GeoPoint) -> bool { a.lat == b.lat && a.lng == b.lng } fn count_distinct(ring: &[GeoPoint]) -> usize { let mut count = 0; for i in 0..ring.len() { if !(0..i).any(|j| same(&ring[i], &ring[j])) { count += 1; } } count } fn check_point(p: &GeoPoint) { if !p.lat.is_finite() { panic!("latitude must be a finite number of degrees"); } if !p.lng.is_finite() { panic!("longitude must be a finite number of degrees"); } if p.lat < -90.0 || p.lat > 90.0 { panic!("latitude must be between -90 and 90 degrees"); } if p.lng < -180.0 || p.lng > 180.0 { panic!("longitude must be between -180 and 180 degrees"); } } pub fn geo_point_from_value(v: &Value) -> GeoPoint { GeoPoint { lat: v.get("lat").as_f64(), lng: v.get("lng").as_f64(), } } pub fn geo_point_to_value(p: &GeoPoint) -> Value { Value::obj(vec![("lat", Value::Float(p.lat)), ("lng", Value::Float(p.lng))]) } pub fn fune_vector(args: &[Value]) -> Value { let polygon: Vec = args[1].as_arr().iter().map(geo_point_from_value).collect(); Value::Bool(point_in_polygon(&geo_point_from_value(&args[0]), &polygon)) }