use geo::BoundingRect as _;
use geo::Simplify as _;
use super::OsmFeature;
use crate::FeaturesVecLayer;
pub fn simplify_layer(
layer: &FeaturesVecLayer<OsmFeature>,
epsilon: f64,
) -> FeaturesVecLayer<OsmFeature>
{
layer
.features_iter()
.map(|f| OsmFeature {
id: f.id,
tags: f.tags.clone(),
geometry: simplify_geometry(&f.geometry, epsilon),
})
.collect()
}
fn simplify_geometry(geometry: &geo::Geometry, epsilon: f64) -> geo::Geometry
{
match geometry
{
geo::Geometry::LineString(g) => geo::Geometry::LineString(g.simplify(epsilon)),
geo::Geometry::Polygon(g) => geo::Geometry::Polygon(g.simplify(epsilon)),
geo::Geometry::MultiLineString(g) => geo::Geometry::MultiLineString(g.simplify(epsilon)),
geo::Geometry::MultiPolygon(g) => geo::Geometry::MultiPolygon(g.simplify(epsilon)),
geo::Geometry::GeometryCollection(gc) => geo::Geometry::GeometryCollection(
gc.0
.iter()
.map(|geom| simplify_geometry(geom, epsilon))
.collect(),
),
_ => geometry.clone(),
}
}
pub fn filter_layer_by_min_bbox(
layer: &FeaturesVecLayer<OsmFeature>,
min_degrees: f64,
) -> FeaturesVecLayer<OsmFeature>
{
layer
.features_iter()
.filter(|f| bbox_exceeds_threshold(&f.geometry, min_degrees))
.cloned()
.collect()
}
fn bbox_exceeds_threshold(geometry: &geo::Geometry, min_degrees: f64) -> bool
{
match geometry
{
geo::Geometry::Point(_) => true,
g => g
.bounding_rect()
.map(|r| r.width() >= min_degrees || r.height() >= min_degrees)
.unwrap_or(false),
}
}
#[cfg(test)]
mod tests
{
use super::*;
use geo::coords_iter::CoordsIter as _;
fn make_feature(geometry: geo::Geometry) -> OsmFeature
{
OsmFeature {
id: 1,
geometry,
tags: vec![("highway".to_owned(), "residential".to_owned())],
}
}
fn collinear_linestring() -> geo::Geometry
{
geo::Geometry::LineString(geo::LineString::from(vec![
(0.0, 0.0),
(1.0, 0.0),
(2.0, 0.0),
]))
}
fn simple_polygon() -> geo::Geometry
{
geo::Geometry::Polygon(geo::Polygon::new(
geo::LineString::from(vec![
(0.0, 0.0),
(0.001, 0.0),
(1.0, 0.0),
(1.0, 1.0),
(0.0, 1.0),
(0.0, 0.0),
]),
vec![],
))
}
#[test]
fn collinear_midpoint_removed()
{
let layer: FeaturesVecLayer<OsmFeature> = vec![make_feature(collinear_linestring())].into();
let simplified = simplify_layer(&layer, 0.5);
let feat = simplified.features_iter().next().unwrap();
assert_eq!(feat.geometry.coords_count(), 2);
}
#[test]
fn polygon_vertex_count_reduced()
{
let layer: FeaturesVecLayer<OsmFeature> = vec![make_feature(simple_polygon())].into();
let orig_count = layer
.features_iter()
.next()
.unwrap()
.geometry
.coords_count();
let simplified = simplify_layer(&layer, 0.01);
let simp_count = simplified
.features_iter()
.next()
.unwrap()
.geometry
.coords_count();
assert!(
simp_count < orig_count,
"simplification should reduce vertex count"
);
}
#[test]
fn tags_preserved_after_simplification()
{
let layer: FeaturesVecLayer<OsmFeature> = vec![make_feature(collinear_linestring())].into();
let simplified = simplify_layer(&layer, 0.5);
let feat = simplified.features_iter().next().unwrap();
assert_eq!(feat.tag("highway"), Some("residential"));
}
#[test]
fn point_feature_unchanged()
{
let point = geo::Geometry::Point(geo::Point::new(1.0, 2.0));
let layer: FeaturesVecLayer<OsmFeature> = vec![make_feature(point.clone())].into();
let simplified = simplify_layer(&layer, 999.0);
let feat = simplified.features_iter().next().unwrap();
assert_eq!(feat.geometry, point);
}
#[test]
fn empty_layer_produces_empty_output()
{
let layer: FeaturesVecLayer<OsmFeature> = vec![].into();
let simplified = simplify_layer(&layer, 1.0);
assert_eq!(simplified.features_iter().count(), 0);
}
#[test]
fn feature_count_preserved()
{
let layer: FeaturesVecLayer<OsmFeature> = vec![
make_feature(collinear_linestring()),
make_feature(collinear_linestring()),
]
.into();
let simplified = simplify_layer(&layer, 0.5);
assert_eq!(simplified.features_iter().count(), 2);
}
#[test]
fn small_polygon_filtered_out()
{
let tiny = make_feature(geo::Geometry::Polygon(geo::Polygon::new(
geo::LineString::from(vec![
(0.0, 0.0),
(0.001, 0.0),
(0.001, 0.001),
(0.0, 0.001),
(0.0, 0.0),
]),
vec![],
)));
let layer: FeaturesVecLayer<OsmFeature> = vec![tiny].into();
let filtered = filter_layer_by_min_bbox(&layer, 0.1);
assert_eq!(
filtered.features_iter().count(),
0,
"sub-threshold polygon must be dropped"
);
}
#[test]
fn large_polygon_kept()
{
let large = make_feature(geo::Geometry::Polygon(geo::Polygon::new(
geo::LineString::from(vec![
(0.0, 0.0),
(1.0, 0.0),
(1.0, 1.0),
(0.0, 1.0),
(0.0, 0.0),
]),
vec![],
)));
let layer: FeaturesVecLayer<OsmFeature> = vec![large].into();
let filtered = filter_layer_by_min_bbox(&layer, 0.1);
assert_eq!(
filtered.features_iter().count(),
1,
"above-threshold polygon must be kept"
);
}
#[test]
fn point_always_kept_regardless_of_threshold()
{
let pt = make_feature(geo::Geometry::Point(geo::Point::new(1.0, 2.0)));
let layer: FeaturesVecLayer<OsmFeature> = vec![pt].into();
let filtered = filter_layer_by_min_bbox(&layer, 999.0);
assert_eq!(
filtered.features_iter().count(),
1,
"points must never be filtered out"
);
}
#[test]
fn mixed_layer_keeps_only_large_features()
{
let tiny = make_feature(geo::Geometry::Polygon(geo::Polygon::new(
geo::LineString::from(vec![
(0.0, 0.0),
(0.001, 0.0),
(0.001, 0.001),
(0.0, 0.001),
(0.0, 0.0),
]),
vec![],
)));
let large = make_feature(geo::Geometry::Polygon(geo::Polygon::new(
geo::LineString::from(vec![
(0.0, 0.0),
(1.0, 0.0),
(1.0, 1.0),
(0.0, 1.0),
(0.0, 0.0),
]),
vec![],
)));
let layer: FeaturesVecLayer<OsmFeature> = vec![tiny, large].into();
let filtered = filter_layer_by_min_bbox(&layer, 0.1);
assert_eq!(
filtered.features_iter().count(),
1,
"only large polygon should survive filtering"
);
}
#[test]
fn filter_preserves_feature_ids()
{
let large = OsmFeature {
id: 12345,
geometry: geo::Geometry::Polygon(geo::Polygon::new(
geo::LineString::from(vec![
(0.0, 0.0),
(1.0, 0.0),
(1.0, 1.0),
(0.0, 1.0),
(0.0, 0.0),
]),
vec![],
)),
tags: vec![],
};
let layer: FeaturesVecLayer<OsmFeature> = vec![large].into();
let filtered = filter_layer_by_min_bbox(&layer, 0.1);
let feat = filtered.features_iter().next().unwrap();
assert_eq!(feat.id, 12345, "feature ID must be preserved");
}
#[test]
fn linestring_filtered_by_width()
{
let narrow_line = make_feature(geo::Geometry::LineString(geo::LineString::from(vec![
(0.0, 0.0),
(0.05, 0.0),
])));
let layer: FeaturesVecLayer<OsmFeature> = vec![narrow_line].into();
let filtered = filter_layer_by_min_bbox(&layer, 0.1);
assert_eq!(filtered.features_iter().count(), 0);
}
#[test]
fn linestring_kept_if_height_exceeds_threshold()
{
let tall_line = make_feature(geo::Geometry::LineString(geo::LineString::from(vec![
(0.0, 0.0),
(0.05, 0.2),
])));
let layer: FeaturesVecLayer<OsmFeature> = vec![tall_line].into();
let filtered = filter_layer_by_min_bbox(&layer, 0.1);
assert_eq!(filtered.features_iter().count(), 1);
}
#[test]
fn larger_epsilon_produces_fewer_vertices()
{
use geo::coords_iter::CoordsIter as _;
let coords: Vec<geo::Coord> = (0..=100)
.map(|i| {
let t = i as f64 / 100.0 * std::f64::consts::TAU;
geo::coord! { x: t.cos(), y: t.sin() }
})
.collect();
let layer: FeaturesVecLayer<OsmFeature> = vec![make_feature(geo::Geometry::Polygon(
geo::Polygon::new(geo::LineString::new(coords), vec![]),
))]
.into();
let medium = simplify_layer(&layer, 0.01);
let coarse = simplify_layer(&layer, 0.1);
let verts = |l: &FeaturesVecLayer<OsmFeature>| -> usize {
l.features_iter().map(|f| f.geometry.coords_count()).sum()
};
assert!(
verts(&coarse) <= verts(&medium),
"coarse (ε=0.1) must have ≤ vertices than medium (ε=0.01)"
);
}
}