Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ geo = { version = "0.32.0", features = ["use-serde"] }
geo-buffer = "0.2.0"
geo-traits = "0.3.0"
geo-types = "0.7.16"
geojson = { version = "0.24.1" }
geozero = { version = "0.14.0", features = ["with-wkb"] }
geozero = { version = "0.14.0", features = ["with-geo", "with-wkb", "with-wkt", "with-geojson"] }
gtfs-structures = "0.43.0"
h3o = { version = "0.9.4", features = ["serde", "geo"] }
hex = "0.4.3"
Expand Down Expand Up @@ -72,6 +71,5 @@ thiserror = "2.0.12"
tokio = "1.39.2"
toml = { version = "0.9.8" }
uom = { version = "0.36.0", features = ["serde"] }
wkb = "0.9.2"
wkt = { version = "0.14.0", features = ["serde"] }

zip = "5.1.1"
4 changes: 1 addition & 3 deletions rust/bambam-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ chrono = { workspace = true }
geo = { workspace = true }
geo-traits = { workspace = true }
geo-types = { workspace = true }
geojson = { workspace = true }
geozero = { workspace = true }
hex = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
Expand All @@ -28,5 +28,3 @@ serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
uom = { workspace = true }
wkb = { workspace = true }
wkt = { workspace = true }
11 changes: 8 additions & 3 deletions rust/bambam-core/src/model/bambam_ops.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use crate::model::bambam_state;

use super::{bambam_field, TimeBin};
use geo::{line_measures::LengthMeasurable, Haversine, InterpolatableLine, LineString, Point};
use geo::{
line_measures::LengthMeasurable, Convert, Haversine, InterpolatableLine, LineString, Point,
};
use geozero::ToWkt;
use routee_compass::{app::search::SearchAppResult, plugin::PluginError};
use routee_compass_core::{
algorithm::search::SearchTreeNode,
Expand All @@ -16,7 +19,6 @@ use uom::{
si::f64::{Length, Time},
ConstZero,
};
use wkt::ToWkt;

pub type DestinationsIter<'a> =
Box<dyn Iterator<Item = Result<(Label, &'a SearchTreeNode), StateModelError>> + 'a>;
Expand Down Expand Up @@ -102,7 +104,10 @@ pub fn points_along_linestring(
distance_to_point,
(fraction * 10000.0).trunc() / 100.0,
length_meters,
linestring.to_wkt()
{
let ls_f64: geo::LineString<f64> = linestring.convert();
geo::Geometry::from(ls_f64).to_wkt().unwrap_or_default()
}
)
})?;
Ok(point)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use geo::{Geometry, MapCoords, TryConvert};
use geo_traits::to_geo::ToGeoGeometry;
use geojson;
use geozero::{
geojson::GeoJsonString, wkt::Wkt as WktReader, CoordDimensions, ToGeo, ToJson, ToWkb, ToWkt,
};
use routee_compass::plugin::output::OutputPluginError;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use wkb;
use wkt::{ToWkt, TryFromWkt};

#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
Expand All @@ -29,8 +28,12 @@ impl IsochroneOutputFormat {
"expected WKT string for geometry deserialization, found: {value:?}"
))
})?;
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}\"")))?;
Ok(g)
let geometry_f64 = WktReader(wkt).to_geo().map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"failure deserializing WKT geometry from output row due to: {e} - WKT string: \"{wkt}\""
))
})?;
try_convert_f32(&geometry_f64)
}
IsochroneOutputFormat::Wkb => {
let wkb_str = value.as_str().ok_or_else(|| {
Expand All @@ -44,31 +47,41 @@ impl IsochroneOutputFormat {
"failed to decode WKB hex string: {e} - WKB string: \"{wkb_str}\""
))
})?;
// Read geometry as f64, then convert to f32
let geom_trait = wkb::reader::read_wkb(&wkb_bytes).map_err(|e| OutputPluginError::OutputPluginFailed(format!(
"failure deserializing WKB geometry from output row due to: {e} - WKB string: \"{wkb_str}\""
)))?;
let geometry_f64 = geom_trait.to_geometry();
let geometry_f32 = try_convert_f32(&geometry_f64)?;
Ok(geometry_f32)
// Read geometry as f64 via geozero, then convert to f32
let geometry_f64 = geozero::wkb::Wkb(wkb_bytes).to_geo().map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"failure deserializing WKB geometry from output row due to: {e} - WKB string: \"{wkb_str}\""
))
})?;
try_convert_f32(&geometry_f64)
}
IsochroneOutputFormat::GeoJson => {
let geojson_str = value.as_str().ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!(
"expected string for geometry deserialization, found: {value:?}"
))
})?;
let geojson_obj = geojson_str.parse::<geojson::GeoJson>().map_err(|e| {
// Parse the JSON and extract geometry, handling both raw geometry and Feature format
let parsed: serde_json::Value = serde_json::from_str(geojson_str).map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"failure parsing GeoJSON from geometry string due to: {e}, found: {value:?}"
"failure parsing GeoJSON string: {e}, found: {value:?}"
))
})?;
let geometry = geo_types::Geometry::<f32>::try_from(geojson_obj).map_err(|e| {
let geom_json = if parsed["type"] == "Feature" {
serde_json::to_string(&parsed["geometry"]).map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"failure extracting geometry from GeoJSON Feature: {e}"
))
})?
} else {
geojson_str.to_string()
};
let geometry_f64 = GeoJsonString(geom_json).to_geo().map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"failure converting GeoJSON to Geometry due to: {e}"
"failure parsing GeoJSON geometry due to: {e}, found: {value:?}"
))
})?;
Ok(geometry)
try_convert_f32(&geometry_f64)
}
}
}
Expand All @@ -78,42 +91,55 @@ impl IsochroneOutputFormat {
geometry: &Geometry<f32>,
) -> Result<String, OutputPluginError> {
match self {
IsochroneOutputFormat::Wkt => Ok(geometry.wkt_string()),
IsochroneOutputFormat::Wkt => {
let geom: Geometry<f64> = geometry.try_convert().map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"unable to convert geometry from f32 to f64: {e}"
))
})?;
geom.to_wkt().map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"failed to write geometry as WKT: {e}"
))
})
}
IsochroneOutputFormat::Wkb => {
let mut out_bytes = vec![];
let geom: Geometry<f64> = geometry.try_convert().map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"unable to convert geometry from f32 to f64: {e}"
))
})?;
let write_options = wkb::writer::WriteOptions {
endianness: wkb::Endianness::BigEndian,
};
wkb::writer::write_geometry(&mut out_bytes, &geom, &write_options).map_err(
|e| {
OutputPluginError::OutputPluginFailed(format!(
"failed to write geometry as WKB: {e}"
))
},
)?;

let out_bytes = geom.to_wkb(CoordDimensions::xy()).map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"failed to write geometry as WKB: {e}"
))
})?;
Ok(out_bytes
.iter()
.map(|b| format!("{b:02X?}"))
.collect::<Vec<String>>()
.join(""))
}
IsochroneOutputFormat::GeoJson => {
let geometry = geojson::Geometry::from(geometry);
let feature = geojson::Feature {
bbox: None,
geometry: Some(geometry),
id: None,
properties: None,
foreign_members: None,
};
let result = serde_json::to_value(feature)?;
Ok(result.to_string())
let geom: Geometry<f64> = geometry.try_convert().map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"unable to convert geometry from f32 to f64: {e}"
))
})?;
let geom_json_str = geom.to_json().map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"failed to serialize geometry as GeoJSON: {e}"
))
})?;
let geom_json: serde_json::Value = serde_json::from_str(&geom_json_str)?;
let feature = serde_json::json!({
"type": "Feature",
"bbox": null,
"geometry": geom_json,
"id": null,
"properties": null
});
Ok(feature.to_string())
}
}
}
Expand All @@ -137,3 +163,94 @@ fn try_convert_f32(g: &Geometry<f64>) -> Result<Geometry<f32>, OutputPluginError
}
})
}

#[cfg(test)]
mod tests {
use super::*;
use geo::polygon;
use serde_json::json;

fn sample_polygon_f32() -> Geometry<f32> {
Geometry::Polygon(polygon![
(x: 0.0f32, y: 0.0f32),
(x: 1.0f32, y: 0.0f32),
(x: 1.0f32, y: 1.0f32),
(x: 0.0f32, y: 1.0f32),
(x: 0.0f32, y: 0.0f32),
])
}

#[test]
fn test_serialize_wkt_roundtrip() {
let fmt = IsochroneOutputFormat::Wkt;
let geom = sample_polygon_f32();
let serialized = fmt
.serialize_geometry(&geom)
.expect("wkt serialization failed");
assert!(
serialized.starts_with("POLYGON"),
"expected WKT POLYGON, got: {serialized}"
);
let deserialized = fmt
.deserialize_geometry(&json!(serialized))
.expect("wkt deserialization failed");
// verify the geometry type is preserved
assert!(matches!(deserialized, Geometry::Polygon(_)));
}

#[test]
fn test_serialize_wkb_roundtrip() {
let fmt = IsochroneOutputFormat::Wkb;
let geom = sample_polygon_f32();
let serialized = fmt
.serialize_geometry(&geom)
.expect("wkb serialization failed");
// WKB is hex-encoded
assert!(serialized.len() > 0, "expected non-empty WKB hex string");
let deserialized = fmt
.deserialize_geometry(&json!(serialized))
.expect("wkb deserialization failed");
assert!(matches!(deserialized, Geometry::Polygon(_)));
}

#[test]
fn test_serialize_geojson_roundtrip() {
let fmt = IsochroneOutputFormat::GeoJson;
let geom = sample_polygon_f32();
let serialized = fmt
.serialize_geometry(&geom)
.expect("geojson serialization failed");
let parsed: serde_json::Value =
serde_json::from_str(&serialized).expect("result should be valid json");
assert_eq!(parsed["type"], "Feature");
assert_eq!(parsed["geometry"]["type"], "Polygon");
let deserialized = fmt
.deserialize_geometry(&json!(serialized))
.expect("geojson deserialization failed");
assert!(matches!(deserialized, Geometry::Polygon(_)));
}

#[test]
fn test_empty_geometry_wkt() {
let fmt = IsochroneOutputFormat::Wkt;
let result = fmt.empty_geometry().expect("empty geometry wkt failed");
assert!(
result.contains("POLYGON"),
"expected WKT POLYGON, got: {result}"
);
}

#[test]
fn test_deserialize_wkt_invalid_input() {
let fmt = IsochroneOutputFormat::Wkt;
let result = fmt.deserialize_geometry(&json!("NOT VALID WKT!!"));
assert!(result.is_err(), "expected error for invalid WKT");
}

#[test]
fn test_deserialize_wkb_invalid_hex() {
let fmt = IsochroneOutputFormat::Wkb;
let result = fmt.deserialize_geometry(&json!("ZZZNOTVALIDHEX"));
assert!(result.is_err(), "expected error for invalid WKB hex");
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::sync::Arc;

use geo::{Centroid, Convert};
use geozero::ToWkt;
use routee_compass::plugin::output::OutputPluginError;
use routee_compass_core::{
algorithm::search::{SearchInstance, SearchTreeNode},
Expand All @@ -12,7 +13,6 @@ use routee_compass_core::{
};
use rstar::{RTreeObject, AABB};
use serde::Serialize;
use wkt::ToWkt;

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

Expand Down Expand Up @@ -139,7 +139,9 @@ impl OpportunityRowId {
let centroid = linestring.centroid().ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!(
"could not get centroid of LINESTRING {}",
linestring.to_wkt()
geo::Geometry::from(linestring.clone())
.to_wkt()
.unwrap_or_default()
))
})?;
Ok(centroid)
Expand Down
4 changes: 1 addition & 3 deletions rust/bambam-gtfs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ downloader = { workspace = true }
env_logger = { workspace = true }
flate2 = { workspace = true }
geo = { workspace = true }
geojson = { workspace = true }
geozero = { workspace = true }
gtfs-structures = { workspace = true }
h3o = { workspace = true }
itertools = { workspace = true }
Expand All @@ -35,6 +35,4 @@ skiplist = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
uom = { workspace = true }
wkb = { workspace = true }
wkt = { workspace = true }
zip = { workspace = true }
Loading
Loading