Skip to content

Commit 132d00f

Browse files
Merge pull request #113 from NatLabRockies/copilot/migrate-to-geozero
Migrate all dependencies to geozero
2 parents 0b221b6 + 9815356 commit 132d00f

File tree

32 files changed

+487
-212
lines changed

32 files changed

+487
-212
lines changed

rust/Cargo.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ geo = { version = "0.32.0", features = ["use-serde"] }
3232
geo-buffer = "0.2.0"
3333
geo-traits = "0.3.0"
3434
geo-types = "0.7.16"
35-
geojson = { version = "0.24.1" }
36-
geozero = { version = "0.14.0", features = ["with-wkb"] }
35+
geozero = { version = "0.14.0", features = ["with-geo", "with-wkb", "with-wkt", "with-geojson"] }
3736
gtfs-structures = "0.43.0"
3837
h3o = { version = "0.9.4", features = ["serde", "geo"] }
3938
hex = "0.4.3"
@@ -72,6 +71,5 @@ thiserror = "2.0.12"
7271
tokio = "1.39.2"
7372
toml = { version = "0.9.8" }
7473
uom = { version = "0.36.0", features = ["serde"] }
75-
wkb = "0.9.2"
76-
wkt = { version = "0.14.0", features = ["serde"] }
74+
7775
zip = "5.1.1"

rust/bambam-core/Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ chrono = { workspace = true }
1616
geo = { workspace = true }
1717
geo-traits = { workspace = true }
1818
geo-types = { workspace = true }
19-
geojson = { workspace = true }
19+
geozero = { workspace = true }
2020
hex = { workspace = true }
2121
itertools = { workspace = true }
2222
log = { workspace = true }
@@ -28,5 +28,3 @@ serde = { workspace = true }
2828
serde_json = { workspace = true }
2929
thiserror = { workspace = true }
3030
uom = { workspace = true }
31-
wkb = { workspace = true }
32-
wkt = { workspace = true }

rust/bambam-core/src/model/bambam_ops.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use crate::model::bambam_state;
22

33
use super::{bambam_field, TimeBin};
4-
use geo::{line_measures::LengthMeasurable, Haversine, InterpolatableLine, LineString, Point};
4+
use geo::{
5+
line_measures::LengthMeasurable, Convert, Haversine, InterpolatableLine, LineString, Point,
6+
};
7+
use geozero::ToWkt;
58
use routee_compass::{app::search::SearchAppResult, plugin::PluginError};
69
use routee_compass_core::{
710
algorithm::search::SearchTreeNode,
@@ -16,7 +19,6 @@ use uom::{
1619
si::f64::{Length, Time},
1720
ConstZero,
1821
};
19-
use wkt::ToWkt;
2022

2123
pub type DestinationsIter<'a> =
2224
Box<dyn Iterator<Item = Result<(Label, &'a SearchTreeNode), StateModelError>> + 'a>;
@@ -102,7 +104,10 @@ pub fn points_along_linestring(
102104
distance_to_point,
103105
(fraction * 10000.0).trunc() / 100.0,
104106
length_meters,
105-
linestring.to_wkt()
107+
{
108+
let ls_f64: geo::LineString<f64> = linestring.convert();
109+
geo::Geometry::from(ls_f64).to_wkt().unwrap_or_default()
110+
}
106111
)
107112
})?;
108113
Ok(point)
Lines changed: 158 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
use geo::{Geometry, MapCoords, TryConvert};
2-
use geo_traits::to_geo::ToGeoGeometry;
3-
use geojson;
2+
use geozero::{
3+
geojson::GeoJsonString, wkt::Wkt as WktReader, CoordDimensions, ToGeo, ToJson, ToWkb, ToWkt,
4+
};
45
use routee_compass::plugin::output::OutputPluginError;
56
use serde::{Deserialize, Serialize};
67
use serde_json::Value;
7-
use wkb;
8-
use wkt::{ToWkt, TryFromWkt};
98

109
#[derive(Deserialize, Serialize, Clone, Debug)]
1110
#[serde(rename_all = "snake_case")]
@@ -29,8 +28,12 @@ impl IsochroneOutputFormat {
2928
"expected WKT string for geometry deserialization, found: {value:?}"
3029
))
3130
})?;
32-
let g = Geometry::try_from_wkt_str(wkt).map_err(|e| OutputPluginError::OutputPluginFailed(format!("failure deserializing WKT geometry from output row due to: {e} - WKT string: \"{wkt}\"")))?;
33-
Ok(g)
31+
let geometry_f64 = WktReader(wkt).to_geo().map_err(|e| {
32+
OutputPluginError::OutputPluginFailed(format!(
33+
"failure deserializing WKT geometry from output row due to: {e} - WKT string: \"{wkt}\""
34+
))
35+
})?;
36+
try_convert_f32(&geometry_f64)
3437
}
3538
IsochroneOutputFormat::Wkb => {
3639
let wkb_str = value.as_str().ok_or_else(|| {
@@ -44,31 +47,41 @@ impl IsochroneOutputFormat {
4447
"failed to decode WKB hex string: {e} - WKB string: \"{wkb_str}\""
4548
))
4649
})?;
47-
// Read geometry as f64, then convert to f32
48-
let geom_trait = wkb::reader::read_wkb(&wkb_bytes).map_err(|e| OutputPluginError::OutputPluginFailed(format!(
49-
"failure deserializing WKB geometry from output row due to: {e} - WKB string: \"{wkb_str}\""
50-
)))?;
51-
let geometry_f64 = geom_trait.to_geometry();
52-
let geometry_f32 = try_convert_f32(&geometry_f64)?;
53-
Ok(geometry_f32)
50+
// Read geometry as f64 via geozero, then convert to f32
51+
let geometry_f64 = geozero::wkb::Wkb(wkb_bytes).to_geo().map_err(|e| {
52+
OutputPluginError::OutputPluginFailed(format!(
53+
"failure deserializing WKB geometry from output row due to: {e} - WKB string: \"{wkb_str}\""
54+
))
55+
})?;
56+
try_convert_f32(&geometry_f64)
5457
}
5558
IsochroneOutputFormat::GeoJson => {
5659
let geojson_str = value.as_str().ok_or_else(|| {
5760
OutputPluginError::OutputPluginFailed(format!(
5861
"expected string for geometry deserialization, found: {value:?}"
5962
))
6063
})?;
61-
let geojson_obj = geojson_str.parse::<geojson::GeoJson>().map_err(|e| {
64+
// Parse the JSON and extract geometry, handling both raw geometry and Feature format
65+
let parsed: serde_json::Value = serde_json::from_str(geojson_str).map_err(|e| {
6266
OutputPluginError::OutputPluginFailed(format!(
63-
"failure parsing GeoJSON from geometry string due to: {e}, found: {value:?}"
67+
"failure parsing GeoJSON string: {e}, found: {value:?}"
6468
))
6569
})?;
66-
let geometry = geo_types::Geometry::<f32>::try_from(geojson_obj).map_err(|e| {
70+
let geom_json = if parsed["type"] == "Feature" {
71+
serde_json::to_string(&parsed["geometry"]).map_err(|e| {
72+
OutputPluginError::OutputPluginFailed(format!(
73+
"failure extracting geometry from GeoJSON Feature: {e}"
74+
))
75+
})?
76+
} else {
77+
geojson_str.to_string()
78+
};
79+
let geometry_f64 = GeoJsonString(geom_json).to_geo().map_err(|e| {
6780
OutputPluginError::OutputPluginFailed(format!(
68-
"failure converting GeoJSON to Geometry due to: {e}"
81+
"failure parsing GeoJSON geometry due to: {e}, found: {value:?}"
6982
))
7083
})?;
71-
Ok(geometry)
84+
try_convert_f32(&geometry_f64)
7285
}
7386
}
7487
}
@@ -78,42 +91,55 @@ impl IsochroneOutputFormat {
7891
geometry: &Geometry<f32>,
7992
) -> Result<String, OutputPluginError> {
8093
match self {
81-
IsochroneOutputFormat::Wkt => Ok(geometry.wkt_string()),
94+
IsochroneOutputFormat::Wkt => {
95+
let geom: Geometry<f64> = geometry.try_convert().map_err(|e| {
96+
OutputPluginError::OutputPluginFailed(format!(
97+
"unable to convert geometry from f32 to f64: {e}"
98+
))
99+
})?;
100+
geom.to_wkt().map_err(|e| {
101+
OutputPluginError::OutputPluginFailed(format!(
102+
"failed to write geometry as WKT: {e}"
103+
))
104+
})
105+
}
82106
IsochroneOutputFormat::Wkb => {
83-
let mut out_bytes = vec![];
84107
let geom: Geometry<f64> = geometry.try_convert().map_err(|e| {
85108
OutputPluginError::OutputPluginFailed(format!(
86109
"unable to convert geometry from f32 to f64: {e}"
87110
))
88111
})?;
89-
let write_options = wkb::writer::WriteOptions {
90-
endianness: wkb::Endianness::BigEndian,
91-
};
92-
wkb::writer::write_geometry(&mut out_bytes, &geom, &write_options).map_err(
93-
|e| {
94-
OutputPluginError::OutputPluginFailed(format!(
95-
"failed to write geometry as WKB: {e}"
96-
))
97-
},
98-
)?;
99-
112+
let out_bytes = geom.to_wkb(CoordDimensions::xy()).map_err(|e| {
113+
OutputPluginError::OutputPluginFailed(format!(
114+
"failed to write geometry as WKB: {e}"
115+
))
116+
})?;
100117
Ok(out_bytes
101118
.iter()
102119
.map(|b| format!("{b:02X?}"))
103120
.collect::<Vec<String>>()
104121
.join(""))
105122
}
106123
IsochroneOutputFormat::GeoJson => {
107-
let geometry = geojson::Geometry::from(geometry);
108-
let feature = geojson::Feature {
109-
bbox: None,
110-
geometry: Some(geometry),
111-
id: None,
112-
properties: None,
113-
foreign_members: None,
114-
};
115-
let result = serde_json::to_value(feature)?;
116-
Ok(result.to_string())
124+
let geom: Geometry<f64> = geometry.try_convert().map_err(|e| {
125+
OutputPluginError::OutputPluginFailed(format!(
126+
"unable to convert geometry from f32 to f64: {e}"
127+
))
128+
})?;
129+
let geom_json_str = geom.to_json().map_err(|e| {
130+
OutputPluginError::OutputPluginFailed(format!(
131+
"failed to serialize geometry as GeoJSON: {e}"
132+
))
133+
})?;
134+
let geom_json: serde_json::Value = serde_json::from_str(&geom_json_str)?;
135+
let feature = serde_json::json!({
136+
"type": "Feature",
137+
"bbox": null,
138+
"geometry": geom_json,
139+
"id": null,
140+
"properties": null
141+
});
142+
Ok(feature.to_string())
117143
}
118144
}
119145
}
@@ -137,3 +163,94 @@ fn try_convert_f32(g: &Geometry<f64>) -> Result<Geometry<f32>, OutputPluginError
137163
}
138164
})
139165
}
166+
167+
#[cfg(test)]
168+
mod tests {
169+
use super::*;
170+
use geo::polygon;
171+
use serde_json::json;
172+
173+
fn sample_polygon_f32() -> Geometry<f32> {
174+
Geometry::Polygon(polygon![
175+
(x: 0.0f32, y: 0.0f32),
176+
(x: 1.0f32, y: 0.0f32),
177+
(x: 1.0f32, y: 1.0f32),
178+
(x: 0.0f32, y: 1.0f32),
179+
(x: 0.0f32, y: 0.0f32),
180+
])
181+
}
182+
183+
#[test]
184+
fn test_serialize_wkt_roundtrip() {
185+
let fmt = IsochroneOutputFormat::Wkt;
186+
let geom = sample_polygon_f32();
187+
let serialized = fmt
188+
.serialize_geometry(&geom)
189+
.expect("wkt serialization failed");
190+
assert!(
191+
serialized.starts_with("POLYGON"),
192+
"expected WKT POLYGON, got: {serialized}"
193+
);
194+
let deserialized = fmt
195+
.deserialize_geometry(&json!(serialized))
196+
.expect("wkt deserialization failed");
197+
// verify the geometry type is preserved
198+
assert!(matches!(deserialized, Geometry::Polygon(_)));
199+
}
200+
201+
#[test]
202+
fn test_serialize_wkb_roundtrip() {
203+
let fmt = IsochroneOutputFormat::Wkb;
204+
let geom = sample_polygon_f32();
205+
let serialized = fmt
206+
.serialize_geometry(&geom)
207+
.expect("wkb serialization failed");
208+
// WKB is hex-encoded
209+
assert!(serialized.len() > 0, "expected non-empty WKB hex string");
210+
let deserialized = fmt
211+
.deserialize_geometry(&json!(serialized))
212+
.expect("wkb deserialization failed");
213+
assert!(matches!(deserialized, Geometry::Polygon(_)));
214+
}
215+
216+
#[test]
217+
fn test_serialize_geojson_roundtrip() {
218+
let fmt = IsochroneOutputFormat::GeoJson;
219+
let geom = sample_polygon_f32();
220+
let serialized = fmt
221+
.serialize_geometry(&geom)
222+
.expect("geojson serialization failed");
223+
let parsed: serde_json::Value =
224+
serde_json::from_str(&serialized).expect("result should be valid json");
225+
assert_eq!(parsed["type"], "Feature");
226+
assert_eq!(parsed["geometry"]["type"], "Polygon");
227+
let deserialized = fmt
228+
.deserialize_geometry(&json!(serialized))
229+
.expect("geojson deserialization failed");
230+
assert!(matches!(deserialized, Geometry::Polygon(_)));
231+
}
232+
233+
#[test]
234+
fn test_empty_geometry_wkt() {
235+
let fmt = IsochroneOutputFormat::Wkt;
236+
let result = fmt.empty_geometry().expect("empty geometry wkt failed");
237+
assert!(
238+
result.contains("POLYGON"),
239+
"expected WKT POLYGON, got: {result}"
240+
);
241+
}
242+
243+
#[test]
244+
fn test_deserialize_wkt_invalid_input() {
245+
let fmt = IsochroneOutputFormat::Wkt;
246+
let result = fmt.deserialize_geometry(&json!("NOT VALID WKT!!"));
247+
assert!(result.is_err(), "expected error for invalid WKT");
248+
}
249+
250+
#[test]
251+
fn test_deserialize_wkb_invalid_hex() {
252+
let fmt = IsochroneOutputFormat::Wkb;
253+
let result = fmt.deserialize_geometry(&json!("ZZZNOTVALIDHEX"));
254+
assert!(result.is_err(), "expected error for invalid WKB hex");
255+
}
256+
}

rust/bambam-core/src/model/output_plugin/opportunity/opportunity_row_id.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::sync::Arc;
22

33
use geo::{Centroid, Convert};
4+
use geozero::ToWkt;
45
use routee_compass::plugin::output::OutputPluginError;
56
use routee_compass_core::{
67
algorithm::search::{SearchInstance, SearchTreeNode},
@@ -12,7 +13,6 @@ use routee_compass_core::{
1213
};
1314
use rstar::{RTreeObject, AABB};
1415
use serde::Serialize;
15-
use wkt::ToWkt;
1616

1717
use crate::model::output_plugin::opportunity::opportunity_orientation::OpportunityOrientation;
1818

@@ -139,7 +139,9 @@ impl OpportunityRowId {
139139
let centroid = linestring.centroid().ok_or_else(|| {
140140
OutputPluginError::OutputPluginFailed(format!(
141141
"could not get centroid of LINESTRING {}",
142-
linestring.to_wkt()
142+
geo::Geometry::from(linestring.clone())
143+
.to_wkt()
144+
.unwrap_or_default()
143145
))
144146
})?;
145147
Ok(centroid)

rust/bambam-gtfs/Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ downloader = { workspace = true }
2020
env_logger = { workspace = true }
2121
flate2 = { workspace = true }
2222
geo = { workspace = true }
23-
geojson = { workspace = true }
23+
geozero = { workspace = true }
2424
gtfs-structures = { workspace = true }
2525
h3o = { workspace = true }
2626
itertools = { workspace = true }
@@ -35,6 +35,4 @@ skiplist = { workspace = true }
3535
thiserror = { workspace = true }
3636
tokio = { workspace = true }
3737
uom = { workspace = true }
38-
wkb = { workspace = true }
39-
wkt = { workspace = true }
4038
zip = { workspace = true }

0 commit comments

Comments
 (0)