1const EARTH_RADIUS_M: f64 = 6_371_008.8;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum GeoFn {
22 Distance,
23 Contains,
24 Within,
25 Intersects,
26 Touches,
27}
28
29pub const GEO_FUNCTION_IRIS: &[(&str, GeoFn)] = &[
33 (
34 "http://www.opengis.net/def/function/geosparql/distance",
35 GeoFn::Distance,
36 ),
37 (
38 "http://www.opengis.net/def/function/geosparql/sfContains",
39 GeoFn::Contains,
40 ),
41 (
42 "http://www.opengis.net/def/function/geosparql/sfWithin",
43 GeoFn::Within,
44 ),
45 (
46 "http://www.opengis.net/def/function/geosparql/sfIntersects",
47 GeoFn::Intersects,
48 ),
49 (
50 "http://www.opengis.net/def/function/geosparql/sfTouches",
51 GeoFn::Touches,
52 ),
53];
54
55pub fn geo_function_iri(local: &str) -> Option<&'static str> {
59 let want = match local {
60 "distance" => GeoFn::Distance,
61 "sfContains" => GeoFn::Contains,
62 "sfWithin" => GeoFn::Within,
63 "sfIntersects" => GeoFn::Intersects,
64 "sfTouches" => GeoFn::Touches,
65 _ => return None,
66 };
67 GEO_FUNCTION_IRIS
68 .iter()
69 .find(|(_, k)| *k == want)
70 .map(|(iri, _)| *iri)
71}
72
73pub fn geo_fn_for_hash(hash: u64) -> Option<GeoFn> {
76 GEO_FUNCTION_IRIS
77 .iter()
78 .find(|(iri, _)| crate::lexicon::generate_60bit_token(iri.as_bytes()) == hash)
79 .map(|(_, k)| *k)
80}
81
82#[derive(Debug, Clone, Copy, PartialEq)]
84pub enum GeoValue {
85 Bool(bool),
86 Number(f64),
88}
89
90pub fn eval_geo_fn(func: GeoFn, a: &Geometry, b: &Geometry) -> GeoValue {
92 match func {
93 GeoFn::Distance => GeoValue::Number(distance_metres(a, b)),
94 GeoFn::Contains => GeoValue::Bool(contains(a, b)),
95 GeoFn::Within => GeoValue::Bool(within(a, b)),
96 GeoFn::Intersects => GeoValue::Bool(intersects(a, b)),
97 GeoFn::Touches => GeoValue::Bool(touches(a, b)),
98 }
99}
100
101#[derive(Debug, Clone, PartialEq)]
104pub enum Geometry {
105 Point(Coord),
106 LineString(Vec<Coord>),
107 Polygon(Vec<Vec<Coord>>),
109 MultiPoint(Vec<Coord>),
110 MultiLineString(Vec<Vec<Coord>>),
111 MultiPolygon(Vec<Vec<Vec<Coord>>>),
112 GeometryCollection(Vec<Geometry>),
113}
114
115#[derive(Debug, Clone, Copy, PartialEq)]
117pub struct Coord {
118 pub x: f64,
119 pub y: f64,
120}
121
122impl Coord {
123 fn new(x: f64, y: f64) -> Self {
124 Self { x, y }
125 }
126}
127
128pub fn parse_wkt(input: &str) -> Result<Geometry, String> {
131 let s = input.trim();
132 let s = if let Some(rest) = s.strip_prefix('<') {
134 match rest.find('>') {
135 Some(i) => rest[i + 1..].trim_start(),
136 None => return Err("unterminated CRS URI in WKT".to_string()),
137 }
138 } else {
139 s
140 };
141 let mut p = WktParser {
142 bytes: s.as_bytes(),
143 pos: 0,
144 src: s,
145 };
146 let g = p.parse_geometry()?;
147 Ok(g)
148}
149
150struct WktParser<'a> {
151 bytes: &'a [u8],
152 pos: usize,
153 src: &'a str,
154}
155
156impl<'a> WktParser<'a> {
157 fn skip_ws(&mut self) {
158 while self.pos < self.bytes.len() && (self.bytes[self.pos] as char).is_ascii_whitespace() {
159 self.pos += 1;
160 }
161 }
162
163 fn keyword(&mut self) -> String {
165 self.skip_ws();
166 let start = self.pos;
167 while self.pos < self.bytes.len() && (self.bytes[self.pos] as char).is_ascii_alphabetic() {
168 self.pos += 1;
169 }
170 self.src[start..self.pos].to_ascii_uppercase()
171 }
172
173 fn skip_dim(&mut self) {
175 self.skip_ws();
176 let start = self.pos;
177 while self.pos < self.bytes.len() {
178 match self.bytes[self.pos] {
179 b'Z' | b'z' | b'M' | b'm' => self.pos += 1,
180 _ => break,
181 }
182 }
183 if self.pos > start {
185 let ok = self.pos >= self.bytes.len()
186 || matches!(self.bytes[self.pos] as char, ' ' | '\t' | '(' | '\n' | '\r');
187 if !ok {
188 self.pos = start; }
190 }
191 }
192
193 fn expect(&mut self, c: u8) -> Result<(), String> {
194 self.skip_ws();
195 if self.pos < self.bytes.len() && self.bytes[self.pos] == c {
196 self.pos += 1;
197 Ok(())
198 } else {
199 Err(format!(
200 "expected '{}' in WKT at byte {}",
201 c as char, self.pos
202 ))
203 }
204 }
205
206 fn peek(&mut self) -> Option<u8> {
207 self.skip_ws();
208 self.bytes.get(self.pos).copied()
209 }
210
211 fn parse_geometry(&mut self) -> Result<Geometry, String> {
212 let kw = self.keyword();
213 self.skip_dim();
214 self.skip_ws();
216 if self.src[self.pos..]
217 .to_ascii_uppercase()
218 .starts_with("EMPTY")
219 {
220 self.pos += 5;
221 return Ok(match kw.as_str() {
222 "POINT" => Geometry::Point(Coord::new(f64::NAN, f64::NAN)),
223 "LINESTRING" => Geometry::LineString(Vec::new()),
224 "POLYGON" => Geometry::Polygon(Vec::new()),
225 _ => Geometry::GeometryCollection(Vec::new()),
226 });
227 }
228 match kw.as_str() {
229 "POINT" => {
230 self.expect(b'(')?;
231 let c = self.coord()?;
232 self.expect(b')')?;
233 Ok(Geometry::Point(c))
234 }
235 "LINESTRING" => Ok(Geometry::LineString(self.coord_list()?)),
236 "POLYGON" => Ok(Geometry::Polygon(self.ring_list()?)),
237 "MULTIPOINT" => {
238 self.expect(b'(')?;
240 let mut pts = Vec::new();
241 loop {
242 if self.peek() == Some(b'(') {
243 self.expect(b'(')?;
244 pts.push(self.coord()?);
245 self.expect(b')')?;
246 } else {
247 pts.push(self.coord()?);
248 }
249 self.skip_ws();
250 if self.peek() == Some(b',') {
251 self.expect(b',')?;
252 } else {
253 break;
254 }
255 }
256 self.expect(b')')?;
257 Ok(Geometry::MultiPoint(pts))
258 }
259 "MULTILINESTRING" => {
260 self.expect(b'(')?;
261 let mut lines = Vec::new();
262 loop {
263 lines.push(self.coord_list()?);
264 if self.peek() == Some(b',') {
265 self.expect(b',')?;
266 } else {
267 break;
268 }
269 }
270 self.expect(b')')?;
271 Ok(Geometry::MultiLineString(lines))
272 }
273 "MULTIPOLYGON" => {
274 self.expect(b'(')?;
275 let mut polys = Vec::new();
276 loop {
277 polys.push(self.ring_list()?);
278 if self.peek() == Some(b',') {
279 self.expect(b',')?;
280 } else {
281 break;
282 }
283 }
284 self.expect(b')')?;
285 Ok(Geometry::MultiPolygon(polys))
286 }
287 "GEOMETRYCOLLECTION" => {
288 self.expect(b'(')?;
289 let mut geoms = Vec::new();
290 loop {
291 geoms.push(self.parse_geometry()?);
292 if self.peek() == Some(b',') {
293 self.expect(b',')?;
294 } else {
295 break;
296 }
297 }
298 self.expect(b')')?;
299 Ok(Geometry::GeometryCollection(geoms))
300 }
301 other => Err(format!("unsupported WKT geometry type '{other}'")),
302 }
303 }
304
305 fn coord_list(&mut self) -> Result<Vec<Coord>, String> {
307 self.expect(b'(')?;
308 let mut out = Vec::new();
309 loop {
310 out.push(self.coord()?);
311 self.skip_ws();
312 if self.peek() == Some(b',') {
313 self.expect(b',')?;
314 } else {
315 break;
316 }
317 }
318 self.expect(b')')?;
319 Ok(out)
320 }
321
322 fn ring_list(&mut self) -> Result<Vec<Vec<Coord>>, String> {
324 self.expect(b'(')?;
325 let mut rings = Vec::new();
326 loop {
327 rings.push(self.coord_list()?);
328 if self.peek() == Some(b',') {
329 self.expect(b',')?;
330 } else {
331 break;
332 }
333 }
334 self.expect(b')')?;
335 Ok(rings)
336 }
337
338 fn coord(&mut self) -> Result<Coord, String> {
340 let x = self.number()?;
341 let y = self.number()?;
342 while let Some(b) = self.peek() {
344 if (b as char).is_ascii_digit() || b == b'-' || b == b'+' || b == b'.' {
345 let _ = self.number()?;
346 } else {
347 break;
348 }
349 }
350 Ok(Coord::new(x, y))
351 }
352
353 fn number(&mut self) -> Result<f64, String> {
354 self.skip_ws();
355 let start = self.pos;
356 while self.pos < self.bytes.len() {
357 let c = self.bytes[self.pos] as char;
358 if c.is_ascii_digit() || matches!(c, '-' | '+' | '.' | 'e' | 'E') {
359 self.pos += 1;
360 } else {
361 break;
362 }
363 }
364 if self.pos == start {
365 return Err(format!("expected a number in WKT at byte {}", self.pos));
366 }
367 self.src[start..self.pos]
368 .parse::<f64>()
369 .map_err(|_| format!("invalid number '{}' in WKT", &self.src[start..self.pos]))
370 }
371}
372
373pub fn distance_metres(a: &Geometry, b: &Geometry) -> f64 {
379 let pa = representative_point(a);
380 let pb = representative_point(b);
381 haversine(pa, pb)
382}
383
384fn haversine(a: Coord, b: Coord) -> f64 {
385 let lat1 = a.y.to_radians();
386 let lat2 = b.y.to_radians();
387 let dlat = (b.y - a.y).to_radians();
388 let dlon = (b.x - a.x).to_radians();
389 let h = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
390 2.0 * EARTH_RADIUS_M * h.sqrt().asin()
391}
392
393fn representative_point(g: &Geometry) -> Coord {
394 match g {
395 Geometry::Point(c) => *c,
396 Geometry::LineString(cs) | Geometry::MultiPoint(cs) => centroid(cs),
397 Geometry::Polygon(rings) => rings
398 .first()
399 .map(|r| centroid(r))
400 .unwrap_or(Coord::new(0.0, 0.0)),
401 Geometry::MultiLineString(ls) => {
402 centroid(&ls.iter().flatten().copied().collect::<Vec<_>>())
403 }
404 Geometry::MultiPolygon(ps) => centroid(
405 &ps.iter()
406 .filter_map(|p| p.first())
407 .flatten()
408 .copied()
409 .collect::<Vec<_>>(),
410 ),
411 Geometry::GeometryCollection(gs) => gs
412 .first()
413 .map(representative_point)
414 .unwrap_or(Coord::new(0.0, 0.0)),
415 }
416}
417
418fn centroid(cs: &[Coord]) -> Coord {
419 if cs.is_empty() {
420 return Coord::new(0.0, 0.0);
421 }
422 let (sx, sy) = cs
423 .iter()
424 .fold((0.0, 0.0), |(ax, ay), c| (ax + c.x, ay + c.y));
425 Coord::new(sx / cs.len() as f64, sy / cs.len() as f64)
426}
427
428pub fn contains(a: &Geometry, b: &Geometry) -> bool {
431 match a {
432 Geometry::Polygon(rings) => all_points(b).iter().all(|p| point_in_polygon(*p, rings)),
433 Geometry::MultiPolygon(polys) => all_points(b)
434 .iter()
435 .all(|p| polys.iter().any(|r| point_in_polygon(*p, r))),
436 _ => false,
437 }
438}
439
440pub fn within(a: &Geometry, b: &Geometry) -> bool {
442 contains(b, a)
443}
444
445pub fn intersects(a: &Geometry, b: &Geometry) -> bool {
448 if contains(a, b) || contains(b, a) {
450 return true;
451 }
452 let pa = all_points(a);
454 let pb = all_points(b);
455 for x in &pa {
456 for y in &pb {
457 if coords_eq(*x, *y) {
458 return true;
459 }
460 }
461 }
462 let sa = segments(a);
464 let sb = segments(b);
465 for (p1, p2) in &sa {
466 for (q1, q2) in &sb {
467 if segments_intersect(*p1, *p2, *q1, *q2) {
468 return true;
469 }
470 }
471 }
472 false
473}
474
475pub fn touches(a: &Geometry, b: &Geometry) -> bool {
480 if !intersects(a, b) {
481 return false;
482 }
483 let interior = probe_points(b).iter().any(|p| strictly_inside(*p, a))
488 || probe_points(a).iter().any(|p| strictly_inside(*p, b));
489 !interior
490}
491
492fn probe_points(g: &Geometry) -> Vec<Coord> {
494 let mut pts = all_points(g);
495 for (p, q) in segments(g) {
496 pts.push(Coord::new((p.x + q.x) / 2.0, (p.y + q.y) / 2.0));
497 }
498 pts
499}
500
501fn strictly_inside(p: Coord, g: &Geometry) -> bool {
502 match g {
503 Geometry::Polygon(rings) => point_in_polygon(p, rings) && !on_boundary(p, rings),
504 Geometry::MultiPolygon(polys) => polys
505 .iter()
506 .any(|r| point_in_polygon(p, r) && !on_boundary(p, r)),
507 _ => false,
508 }
509}
510
511fn on_boundary(p: Coord, rings: &[Vec<Coord>]) -> bool {
512 for ring in rings {
513 for w in ring.windows(2) {
514 if point_on_segment(p, w[0], w[1]) {
515 return true;
516 }
517 }
518 }
519 false
520}
521
522fn all_points(g: &Geometry) -> Vec<Coord> {
523 match g {
524 Geometry::Point(c) => vec![*c],
525 Geometry::LineString(cs) | Geometry::MultiPoint(cs) => cs.clone(),
526 Geometry::Polygon(rings) => rings.iter().flatten().copied().collect(),
527 Geometry::MultiLineString(ls) => ls.iter().flatten().copied().collect(),
528 Geometry::MultiPolygon(ps) => ps.iter().flatten().flatten().copied().collect(),
529 Geometry::GeometryCollection(gs) => gs.iter().flat_map(all_points).collect(),
530 }
531}
532
533fn segments(g: &Geometry) -> Vec<(Coord, Coord)> {
534 let mut out = Vec::new();
535 let ring_segs = |cs: &[Coord], out: &mut Vec<(Coord, Coord)>| {
536 for w in cs.windows(2) {
537 out.push((w[0], w[1]));
538 }
539 };
540 match g {
541 Geometry::LineString(cs) => ring_segs(cs, &mut out),
542 Geometry::Polygon(rings) => {
543 for r in rings {
544 ring_segs(r, &mut out);
545 }
546 }
547 Geometry::MultiLineString(ls) => {
548 for l in ls {
549 ring_segs(l, &mut out);
550 }
551 }
552 Geometry::MultiPolygon(ps) => {
553 for p in ps {
554 for r in p {
555 ring_segs(r, &mut out);
556 }
557 }
558 }
559 _ => {}
560 }
561 out
562}
563
564fn point_in_polygon(p: Coord, rings: &[Vec<Coord>]) -> bool {
567 let Some(exterior) = rings.first() else {
568 return false;
569 };
570 if on_boundary(p, rings) {
571 return true;
572 }
573 if !point_in_ring(p, exterior) {
574 return false;
575 }
576 for hole in &rings[1..] {
578 if point_in_ring(p, hole) {
579 return false;
580 }
581 }
582 true
583}
584
585fn point_in_ring(p: Coord, ring: &[Coord]) -> bool {
586 let n = ring.len();
587 if n < 3 {
588 return false;
589 }
590 let mut inside = false;
591 let mut j = n - 1;
592 for i in 0..n {
593 let vi = ring[i];
594 let vj = ring[j];
595 if (vi.y > p.y) != (vj.y > p.y) {
596 let x_int = (vj.x - vi.x) * (p.y - vi.y) / (vj.y - vi.y) + vi.x;
597 if p.x < x_int {
598 inside = !inside;
599 }
600 }
601 j = i;
602 }
603 inside
604}
605
606fn coords_eq(a: Coord, b: Coord) -> bool {
607 (a.x - b.x).abs() < 1e-9 && (a.y - b.y).abs() < 1e-9
608}
609
610fn point_on_segment(p: Coord, a: Coord, b: Coord) -> bool {
611 let cross = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
612 if cross.abs() > 1e-9 {
613 return false;
614 }
615 let within_x = p.x >= a.x.min(b.x) - 1e-9 && p.x <= a.x.max(b.x) + 1e-9;
616 let within_y = p.y >= a.y.min(b.y) - 1e-9 && p.y <= a.y.max(b.y) + 1e-9;
617 within_x && within_y
618}
619
620fn orient(a: Coord, b: Coord, c: Coord) -> f64 {
621 (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
622}
623
624fn segments_intersect(p1: Coord, p2: Coord, q1: Coord, q2: Coord) -> bool {
625 let d1 = orient(q1, q2, p1);
626 let d2 = orient(q1, q2, p2);
627 let d3 = orient(p1, p2, q1);
628 let d4 = orient(p1, p2, q2);
629 if ((d1 > 0.0) != (d2 > 0.0)) && ((d3 > 0.0) != (d4 > 0.0)) {
630 return true;
631 }
632 point_on_segment(p1, q1, q2)
634 || point_on_segment(p2, q1, q2)
635 || point_on_segment(q1, p1, p2)
636 || point_on_segment(q2, p1, p2)
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642
643 #[test]
644 fn parse_point() {
645 assert_eq!(
646 parse_wkt("POINT(1.5 -2.25)").unwrap(),
647 Geometry::Point(Coord::new(1.5, -2.25))
648 );
649 }
650
651 #[test]
652 fn parse_point_z_ignored() {
653 assert_eq!(
654 parse_wkt("POINT Z (1 2 3)").unwrap(),
655 Geometry::Point(Coord::new(1.0, 2.0))
656 );
657 }
658
659 #[test]
660 fn parse_polygon_with_hole() {
661 let g = parse_wkt("POLYGON((0 0, 4 0, 4 4, 0 4, 0 0),(1 1, 2 1, 2 2, 1 2, 1 1))").unwrap();
662 match g {
663 Geometry::Polygon(rings) => {
664 assert_eq!(rings.len(), 2);
665 assert_eq!(rings[0].len(), 5);
666 }
667 other => panic!("expected polygon, got {other:?}"),
668 }
669 }
670
671 #[test]
672 fn parse_skips_crs_prefix() {
673 let g = parse_wkt("<http://www.opengis.net/def/crs/OGC/1.3/CRS84> POINT(10 20)").unwrap();
674 assert_eq!(g, Geometry::Point(Coord::new(10.0, 20.0)));
675 }
676
677 #[test]
678 fn haversine_known_distance() {
679 let d = distance_metres(
681 &Geometry::Point(Coord::new(-0.1276, 51.5074)),
682 &Geometry::Point(Coord::new(2.3522, 48.8566)),
683 );
684 assert!((d - 343_556.0).abs() < 2_000.0, "distance was {d}");
685 }
686
687 #[test]
688 fn contains_point_in_square() {
689 let square = parse_wkt("POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))").unwrap();
690 assert!(contains(&square, &Geometry::Point(Coord::new(2.0, 2.0))));
691 assert!(!contains(&square, &Geometry::Point(Coord::new(5.0, 5.0))));
692 }
693
694 #[test]
695 fn hole_excludes_point() {
696 let g = parse_wkt("POLYGON((0 0, 4 0, 4 4, 0 4, 0 0),(1 1, 3 1, 3 3, 1 3, 1 1))").unwrap();
697 assert!(
698 !contains(&g, &Geometry::Point(Coord::new(2.0, 2.0))),
699 "in hole"
700 );
701 assert!(
702 contains(&g, &Geometry::Point(Coord::new(0.5, 0.5))),
703 "outside hole"
704 );
705 }
706
707 #[test]
708 fn within_is_contains_flipped() {
709 let square = parse_wkt("POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))").unwrap();
710 let pt = Geometry::Point(Coord::new(2.0, 2.0));
711 assert!(within(&pt, &square));
712 assert!(!within(&square, &pt));
713 }
714
715 #[test]
716 fn intersecting_lines() {
717 let a = parse_wkt("LINESTRING(0 0, 4 4)").unwrap();
718 let b = parse_wkt("LINESTRING(0 4, 4 0)").unwrap();
719 assert!(intersects(&a, &b));
720 let c = parse_wkt("LINESTRING(0 1, 4 5)").unwrap();
721 assert!(!intersects(&a, &c), "parallel, should not intersect");
722 }
723
724 #[test]
725 fn touching_squares() {
726 let a = parse_wkt("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))").unwrap();
728 let b = parse_wkt("POLYGON((1 0, 2 0, 2 1, 1 1, 1 0))").unwrap();
729 assert!(touches(&a, &b), "edge-sharing squares should touch");
730 let c = parse_wkt("POLYGON((0.5 0, 1.5 0, 1.5 1, 0.5 1, 0.5 0))").unwrap();
732 assert!(intersects(&a, &c));
733 assert!(!touches(&a, &c), "overlap is not a touch");
734 }
735}