Skip to content

Commit

Permalink
Add public wrapper layer for Rust noding
Browse files Browse the repository at this point in the history
  • Loading branch information
Notgnoshi committed Feb 25, 2024
1 parent a1d3b3c commit 2962492
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 8 deletions.
8 changes: 4 additions & 4 deletions generative/cxxbridge/geometry_collection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ get_geos_geoms_from_rust(const GeometryCollectionShim& rust_geoms,
for (const auto& linestring : linestrings)
{
auto cs = std::make_unique<geos::geom::CoordinateSequence>(linestring.vec.size(), 2);
for (auto coord : linestring.vec)
for (size_t i = 0; i < linestring.vec.size(); i++)
{
cs->add(geos::geom::CoordinateXY{coord.x, coord.y});
cs->getAt(i) = geos::geom::CoordinateXY{linestring.vec[i].x, linestring.vec[i].y};
}

auto geos_line = factory->createLineString(std::move(cs));
Expand All @@ -49,9 +49,9 @@ get_geos_geoms_from_rust(const GeometryCollectionShim& rust_geoms,
for (const auto& ring : polygon.vec)
{
auto cs = std::make_unique<geos::geom::CoordinateSequence>(ring.vec.size(), 2);
for (auto coord : ring.vec)
for (size_t i = 0; i < ring.vec.size(); i++)
{
cs->add(geos::geom::CoordinateXY{coord.x, coord.y});
cs->getAt(i) = geos::geom::CoordinateXY{ring.vec[i].x, ring.vec[i].y};
}
auto geos_ring = factory->createLinearRing(std::move(cs));
if (shell == nullptr)
Expand Down
3 changes: 0 additions & 3 deletions generative/cxxbridge/noder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
node(const GeometryCollectionShim& rust_geoms, double tolerance) noexcept
{
const auto geos_geoms = copy_rust_collection_to_geos(rust_geoms);
std::cerr << "Noding: " << geos_geoms->toString() << std::endl;

auto noded = std::unique_ptr<geos::geom::Geometry>(nullptr);
if (tolerance == 0.0)
{
Expand All @@ -23,7 +21,6 @@ node(const GeometryCollectionShim& rust_geoms, double tolerance) noexcept
auto noder = std::make_unique<geos::noding::snap::SnappingNoder>(tolerance);
noded = generative::noding::GeometryNoder::node(*geos_geoms, std::move(noder));
}
std::cerr << "Noded: " << noded->toString() << std::endl;
auto graph = generative::noding::GeometryGraph(*noded);

auto graph_shim = std::make_unique<GeometryGraphShim>(std::move(graph));
Expand Down
37 changes: 36 additions & 1 deletion generative/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,39 @@ pub type NodeData = Point;
pub type EdgeWeight = ();
pub type NodeIndex = usize;

pub type GeometryGraph<Direction> = petgraph::Graph<NodeData, EdgeWeight, Direction, NodeIndex>;
pub type GeometryGraph<Direction = petgraph::Undirected> =
petgraph::Graph<NodeData, EdgeWeight, Direction, NodeIndex>;

#[cfg(feature = "geom2graph-bindings")]
mod wrapper {
use super::*;
use crate::cxxbridge::{GeometryGraphShim, GraphEdge};

impl<Direction: petgraph::EdgeType> From<&GeometryGraphShim> for GeometryGraph<Direction> {
fn from(ffi_graph: &GeometryGraphShim) -> GeometryGraph<Direction> {
let nodes = ffi_graph.nodes();
// The edges are indices into the nodes array.
let edges = ffi_graph.edges();

let mut graph = GeometryGraph::default();
graph.reserve_exact_nodes(nodes.len());
graph.reserve_exact_edges(edges.len());

for (_cxx_node_index, node) in nodes.iter().enumerate() {
let point = Point::new(node.x, node.y);
// We rely on the implementation detail of petgraph::Graph that when you insert nodes in
// order, the node indices are generated in the same order.
let _node_index = graph.add_node(point);
debug_assert_eq!(_node_index.index(), _cxx_node_index);
}
for edge in &edges {
let GraphEdge { src, dst } = edge;
let src = petgraph::graph::NodeIndex::new(*src);
let dst = petgraph::graph::NodeIndex::new(*dst);
let _edge_index = graph.add_edge(src, dst, ());
}

graph
}
}
}
2 changes: 2 additions & 0 deletions generative/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ pub mod flatten;
mod geometry_mut_map;
pub mod graph;
pub mod io;
#[cfg(feature = "geom2graph-bindings")]
pub mod noding;
pub mod snap;
pub mod triangulation;

Expand Down
173 changes: 173 additions & 0 deletions generative/noding/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use geo::Geometry;

use crate::cxxbridge;
use crate::graph::GeometryGraph;

pub fn node<G, Direction: petgraph::EdgeType>(geoms: G) -> GeometryGraph<Direction>
where
G: IntoIterator<Item = Geometry>,
{
let collection = cxxbridge::GeometryCollectionShim::new(geoms);
let ffi_graph = unsafe {
// Setting the tolerance to 0 picks the IteratedNoder instead of the SnappingNoder.
// They both have pros and cons.
// * IteratedNoder might throw exceptions if it does not converge on pathological
// geometries
// * SnappingNoder doesn't handle POINT geometries, only LINESTRINGs and POLYGONs
let tolerance = 0.0;
cxxbridge::node(&collection, tolerance)
};
(&*ffi_graph).into()
}

#[cfg(test)]
mod tests {
use geo::Point;
use petgraph::graph::{EdgeIndex, NodeIndex};
use petgraph::Undirected;

use super::*;
use crate::io::read_wkt_geometries;

#[test]
fn test_no_geometries() {
let graph = node::<_, Undirected>(std::iter::empty());
assert_eq!(graph.node_count(), 0);
assert_eq!(graph.edge_count(), 0);
}

#[test]
fn test_points() {
let wkt = b"POINT(0 0)\nLINESTRING(0 0, 1 1)\nPOINT(2 2)\n";
let geometries = read_wkt_geometries(&wkt[..]);

let graph = node::<_, Undirected>(geometries);
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 1);

let node0 = graph.node_weight(NodeIndex::new(0)).unwrap();
let expected0 = Point::new(0.0, 0.0);
assert_eq!(node0, &expected0);

// Geometries aren't handled one at a time in their order in the iterator. Points are
// handled, then linestrings, then finally polygons.
let node1 = graph.node_weight(NodeIndex::new(1)).unwrap();
let expected1 = Point::new(2.0, 2.0);
assert_eq!(node1, &expected1);

let node2 = graph.node_weight(NodeIndex::new(2)).unwrap();
let expected2 = Point::new(1.0, 1.0);
assert_eq!(node2, &expected2);

let edge0 = graph.edge_endpoints(EdgeIndex::new(0)).unwrap();
let expected0 = (NodeIndex::new(0), NodeIndex::new(2));
assert_eq!(edge0, expected0);
}

#[test]
fn test_single_linestring() {
let wkt = b"LINESTRING(0 0, 1 0, 2 0)";
let geometries = read_wkt_geometries(&wkt[..]);

let graph = node::<_, Undirected>(geometries);
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 2);

let node0 = graph.node_weight(NodeIndex::new(0)).unwrap();
let expected0 = Point::new(0.0, 0.0);
assert_eq!(node0, &expected0);

let node1 = graph.node_weight(NodeIndex::new(1)).unwrap();
let expected1 = Point::new(1.0, 0.0);
assert_eq!(node1, &expected1);

let node2 = graph.node_weight(NodeIndex::new(2)).unwrap();
let expected2 = Point::new(2.0, 0.0);
assert_eq!(node2, &expected2);

let edge0 = graph.edge_endpoints(EdgeIndex::new(0)).unwrap();
let expected0 = (NodeIndex::new(0), NodeIndex::new(1));
assert_eq!(edge0, expected0);

let edge1 = graph.edge_endpoints(EdgeIndex::new(1)).unwrap();
let expected1 = (NodeIndex::new(1), NodeIndex::new(2));
assert_eq!(edge1, expected1);
}

#[test]
fn test_crossing_linestrings() {
let wkt = b"LINESTRING(0 0, 1 0)\nLINESTRING(0.5 -1, 0.5 1)";
let geometries = read_wkt_geometries(&wkt[..]);

let graph = node::<_, Undirected>(geometries);
assert_eq!(graph.node_count(), 5);
assert_eq!(graph.edge_count(), 4);

let nodes: Vec<_> = graph.node_weights().collect();
let expected = [
&Point::new(0.0, 0.0), // 0
&Point::new(0.5, 0.0), // 1 - the new intersection point
&Point::new(1.0, 0.0), // 2
&Point::new(0.5, -1.0), // 3
&Point::new(0.5, 1.0), // 4
];
assert_eq!(nodes, expected);

let mut edges = Vec::new();
for i in 0..graph.edge_count() {
let edge = graph.edge_endpoints(EdgeIndex::new(i)).unwrap();
edges.push((edge.0.index(), edge.1.index()))
}
let expected = [(0, 1), (1, 4), (1, 3), (1, 2)];
assert_eq!(edges, expected);
}

#[test]
fn test_rectangle() {
// a tic-tac-toe pattern
let wkt = b"GEOMETRYCOLLECTION( LINESTRING(2 0, 2 8), LINESTRING(6 0, 6 8), LINESTRING(0 2, 8 2), LINESTRING(0 6, 8 6))";
let geoms = read_wkt_geometries(&wkt[..]);

let graph = node::<_, Undirected>(geoms);
assert_eq!(graph.node_count(), 12);
assert_eq!(graph.edge_count(), 12);

let nodes: Vec<_> = graph.node_weights().collect();
let expected = [
&Point::new(2.0, 0.0), // 0 - start left vertical
&Point::new(2.0, 2.0), // 1 - intersection
&Point::new(2.0, 6.0), // 2 - intersection
&Point::new(2.0, 8.0), // 3 - end left vertical
&Point::new(6.0, 0.0), // 4 - start right vertical
&Point::new(6.0, 2.0), // 5 - intersection
&Point::new(6.0, 6.0), // 6 - intersection
&Point::new(6.0, 8.0), // 7 - end left vertical
&Point::new(0.0, 2.0), // 8 - start bottom horizontal
&Point::new(8.0, 2.0), // 9
&Point::new(0.0, 6.0), // 10 - start top horizontal
&Point::new(8.0, 6.0), // 11
];
assert_eq!(nodes, expected);

let mut edges = Vec::new();
for i in 0..graph.edge_count() {
let edge = graph.edge_endpoints(EdgeIndex::new(i)).unwrap();
edges.push((edge.0.index(), edge.1.index()))
}
let expected = [
(0, 1), // bottom left vertical dangle
(1, 5), // bottom inner horizontal
(1, 8), // bottom left horizontal dangle
(1, 2), // left inner vertical
(2, 6), // top inner horizontal
(2, 10), // top left horizontal dangle
(2, 3), // top left verticle dangle
(4, 5), // bottom right verticle dangle
(5, 9), // bottom right horizontal dangle
(5, 6), // right inner horizontal
(6, 11), // top right horizontal dangle
(6, 7), // top right verticle dangle
];
assert_eq!(edges, expected);
}
}
22 changes: 22 additions & 0 deletions tests/geometry-graph-tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,28 @@ TEST(GeometryGraphTests, SingleLinestring)
EXPECT_THAT(third.adjacencies, UnorderedElementsAre(1));
}

TEST(GeometryGraphTests, CrossingLinestring)
{
const auto geometry = generative::io::from_wkt(
// clang-format off
"GEOMETRYCOLLECTION ("
"LINESTRING (0 0, 0.5 0),"
"LINESTRING (0.5 0, 1 0),"
"LINESTRING (0.5 -1, 0.5 0),"
"LINESTRING (0.5 0, 0.5 1)"
")"
// clang-format on
);
ASSERT_TRUE(geometry);

const auto graph = generative::noding::GeometryGraph(*geometry);
const auto& nodes = graph.get_nodes();
const auto& edges = graph.get_edge_pairs();

ASSERT_THAT(nodes, SizeIs(5));
ASSERT_THAT(edges, SizeIs(4));
}

TEST(GeometryGraphTests, Single3DLinestring)
{
const auto geometry = generative::io::from_wkt("LINESTRING Z(0 0 0, 1 1 1, 2 2 2)");
Expand Down
30 changes: 30 additions & 0 deletions tests/geometry-noder-tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,36 @@ TEST(GeometryNoderTests, DisjointPoint)
EXPECT_TRUE(noded->equals(expected.get()));
}

TEST(GeometryNoderTests, CrossingLinestrings)
{
const auto geom = from_wkt(
// clang-format off
"GEOMETRYCOLLECTION("
"LINESTRING(0 0, 1 0),"
"LINESTRING(0.5 -1, 0.5 1)"
")"
// clang-format on
);
ASSERT_TRUE(geom);

const auto expected = from_wkt(
// clang-format off
"GEOMETRYCOLLECTION ("
"LINESTRING (0 0, 0.5 0),"
"LINESTRING (0.5 0, 1 0),"
"LINESTRING (0.5 -1, 0.5 0),"
"LINESTRING (0.5 0, 0.5 1)"
")"
// clang-format on
);

const auto noded = generative::noding::GeometryNoder::node(*geom);
std::cerr << noded->toString() << std::endl;

EXPECT_EQ(noded->getGeometryType(), expected->getGeometryType());
EXPECT_TRUE(noded->equals(expected.get()));
}

TEST(GeometryNoderTests, SimpleRectangle)
{
const std::unique_ptr<geos::geom::Geometry> rectangle = from_wkt(
Expand Down

0 comments on commit 2962492

Please sign in to comment.