Skip to content

Commit d74a2f5

Browse files
committed
Ergonomic de-9im specification matching
e.g. `intersection_matrix.matches('TTT***FF2')`
1 parent 9a81c55 commit d74a2f5

File tree

1 file changed

+122
-1
lines changed

1 file changed

+122
-1
lines changed

geo/src/algorithm/relate/geomgraph/intersection_matrix.rs

+122-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use crate::{coordinate_position::CoordPos, dimensions::Dimensions};
22

3+
use crate::geometry_cow::GeometryCow::Point;
4+
use std::str::FromStr;
5+
36
/// Models a *Dimensionally Extended Nine-Intersection Model (DE-9IM)* matrix.
47
///
58
/// DE-9IM matrix values (such as "212FF1FF2") specify the topological relationship between
@@ -261,6 +264,52 @@ impl IntersectionMatrix {
261264
pub fn get(&self, lhs: CoordPos, rhs: CoordPos) -> Dimensions {
262265
self.0[lhs][rhs]
263266
}
267+
268+
/// Does the intersection matrix match the provided de-9im specification string?
269+
///
270+
/// A de-9im spec string must be 9 characters long, and each character
271+
/// must be one of the following:
272+
///
273+
/// - 0: matches a 0-dimensional (point) intersection
274+
/// - 1: matches a 1-dimensional (line) intersection
275+
/// - 2: matches a 2-dimensional (area) intersection
276+
/// - f or F: matches only empty dimensions
277+
/// - t or T: matches anything non-empty
278+
/// - *: matches anything
279+
///
280+
/// ```
281+
/// use geo::algorithm::Relate;
282+
/// use wkt::TryFromWkt;
283+
/// use geo_types::Polygon;
284+
///
285+
/// let a = Polygon::<f64>::try_from_wkt_str("POLYGON((0 0,4 0,4 4,0 4,0 0))").expect("valid WKT");
286+
/// let b = Polygon::<f64>::try_from_wkt_str("POLYGON((1 1,4 0,4 4,0 4,1 1))").expect("valid WKT");
287+
/// let im = a.relate(&b);
288+
/// assert!(im.matches("212F11FF2").expect("valid de-9im spec"));
289+
/// assert!(im.matches("TTT***FF2").expect("valid de-9im spec"));
290+
/// assert!(!im.matches("TTT***FFF").expect("valid de-9im spec"));
291+
/// ```
292+
pub fn matches(&self, spec: &str) -> Result<bool, InvalidInputError> {
293+
if spec.len() != 9 {
294+
return Err(InvalidInputError::new(format!(
295+
"de-9im specification must be exactly 9 characters. Got {len}",
296+
len = spec.len()
297+
)));
298+
}
299+
300+
let mut chars = spec.chars();
301+
for a in &[CoordPos::Inside, CoordPos::OnBoundary, CoordPos::Outside] {
302+
for b in &[CoordPos::Inside, CoordPos::OnBoundary, CoordPos::Outside] {
303+
let dim_spec = dimension_matcher::DimensionMatcher::try_from(
304+
chars.next().expect("already validated length is 9"),
305+
)?;
306+
if !dim_spec.matches(self.0[*a][*b]) {
307+
return Ok(false);
308+
}
309+
}
310+
}
311+
Ok(true)
312+
}
264313
}
265314

266315
/// Build an IntersectionMatrix based on a string specification.
@@ -272,11 +321,83 @@ impl IntersectionMatrix {
272321
/// assert!(intersection_matrix.is_intersects());
273322
/// assert!(!intersection_matrix.is_contains());
274323
/// ```
275-
impl std::str::FromStr for IntersectionMatrix {
324+
impl FromStr for IntersectionMatrix {
276325
type Err = InvalidInputError;
277326
fn from_str(str: &str) -> Result<Self, Self::Err> {
278327
let mut im = IntersectionMatrix::empty();
279328
im.set_at_least_from_string(str)?;
280329
Ok(im)
281330
}
282331
}
332+
333+
pub(crate) mod dimension_matcher {
334+
use super::Dimensions;
335+
use super::InvalidInputError;
336+
337+
/// A single letter from a de-9im matching specification like "1*T**FFF*"
338+
pub(crate) enum DimensionMatcher {
339+
Anything,
340+
NonEmpty,
341+
Exact(Dimensions),
342+
}
343+
344+
impl DimensionMatcher {
345+
pub fn matches(&self, dim: Dimensions) -> bool {
346+
match (self, dim) {
347+
(Self::Anything, _) => true,
348+
(DimensionMatcher::NonEmpty, d) => d != Dimensions::Empty,
349+
(DimensionMatcher::Exact(a), b) => a == &b,
350+
}
351+
}
352+
}
353+
354+
impl TryFrom<char> for DimensionMatcher {
355+
type Error = InvalidInputError;
356+
357+
fn try_from(value: char) -> Result<Self, Self::Error> {
358+
Ok(match value {
359+
'*' => Self::Anything,
360+
't' | 'T' => Self::NonEmpty,
361+
'f' | 'F' => Self::Exact(Dimensions::Empty),
362+
'0' => Self::Exact(Dimensions::ZeroDimensional),
363+
'1' => Self::Exact(Dimensions::OneDimensional),
364+
'2' => Self::Exact(Dimensions::TwoDimensional),
365+
_ => {
366+
return Err(InvalidInputError::new(format!(
367+
"invalid de-9im specification character: {value}"
368+
)))
369+
}
370+
})
371+
}
372+
}
373+
}
374+
375+
#[cfg(test)]
376+
mod tests {
377+
use super::*;
378+
379+
fn subject() -> IntersectionMatrix {
380+
// Topologically, this is a nonsense IM
381+
IntersectionMatrix::from_str("F00111222").unwrap()
382+
}
383+
384+
#[test]
385+
fn matches_exactly() {
386+
assert!(subject().matches("F00111222").unwrap());
387+
}
388+
389+
#[test]
390+
fn doesnt_match() {
391+
assert!(!subject().matches("222222222").unwrap());
392+
}
393+
394+
#[test]
395+
fn matches_truthy() {
396+
assert!(subject().matches("FTTTTTTTT").unwrap());
397+
}
398+
399+
#[test]
400+
fn matches_wildcard() {
401+
assert!(subject().matches("F0011122*").unwrap());
402+
}
403+
}

0 commit comments

Comments
 (0)