use crate::{ArcGISGeometryError, ArcGISGeometryErrorKind, FeatureSet};
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use tracing::instrument;
#[derive(Debug, Clone, Serialize, derive_builder::Builder, Getters)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct ProfileParameters {
#[serde(rename = "InputLineFeatures")]
input_line_features: String,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "DEMResolution")]
dem_resolution: Option<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "ProfileIDField")]
profile_id_field: Option<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "MaximumSampleDistance")]
maximum_sample_distance: Option<f64>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "MaximumSampleDistanceUnits")]
maximum_sample_distance_units: Option<String>,
#[builder(default = "true")]
#[serde(rename = "returnZ")]
return_z: bool,
#[builder(default = "true")]
#[serde(rename = "returnM")]
return_m: bool,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "inSR")]
in_sr: Option<u32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "outSR")]
out_sr: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Getters)]
pub struct ProfileResult {
output_profile: FeatureSet,
}
impl ProfileResult {
pub fn new(output_profile: FeatureSet) -> Self {
Self { output_profile }
}
#[instrument(skip(self))]
pub fn first_point_z(&self) -> Result<f64, ArcGISGeometryError> {
let points = self.elevation_points()?;
points
.first()
.map(|p| *p.elevation_meters())
.ok_or_else(|| {
ArcGISGeometryError::new(ArcGISGeometryErrorKind::InvalidGeometry(
"Profile has no points".to_string(),
))
})
}
#[instrument(skip(self))]
pub fn last_point_z(&self) -> Result<f64, ArcGISGeometryError> {
let points = self.elevation_points()?;
points.last().map(|p| *p.elevation_meters()).ok_or_else(|| {
ArcGISGeometryError::new(ArcGISGeometryErrorKind::InvalidGeometry(
"Profile has no points".to_string(),
))
})
}
#[instrument(skip(self))]
pub fn elevation_points(&self) -> Result<Vec<ElevationPoint>, ArcGISGeometryError> {
use crate::ArcGISGeometry;
tracing::debug!("Extracting elevation points from profile result");
let feature_count = self.output_profile.features().len();
tracing::debug!(feature_count, "Processing profile features");
if self.output_profile.features().is_empty() {
let err = ArcGISGeometryError::new(ArcGISGeometryErrorKind::InvalidGeometry(
"Profile result has no features".to_string(),
));
tracing::error!(error = %err, "No features in profile");
return Err(err);
}
let feature = &self.output_profile.features()[0];
let geometry = feature.geometry().as_ref().ok_or_else(|| {
let err = ArcGISGeometryError::new(ArcGISGeometryErrorKind::InvalidGeometry(
"Profile feature missing geometry".to_string(),
));
tracing::error!(error = %err, "Missing geometry");
err
})?;
let polyline = match geometry {
ArcGISGeometry::Polyline(polyline) => polyline,
_ => {
let err = ArcGISGeometryError::new(ArcGISGeometryErrorKind::InvalidGeometry(
format!("Expected polyline geometry, got {:?}", geometry),
));
tracing::error!(error = %err, "Wrong geometry type");
return Err(err);
}
};
let paths = polyline.paths();
if paths.is_empty() {
let err = ArcGISGeometryError::new(ArcGISGeometryErrorKind::InvalidGeometry(
"Polyline has no paths".to_string(),
));
tracing::error!(error = %err, "No paths in polyline");
return Err(err);
}
let path = &paths[0];
tracing::debug!(coord_count = path.len(), "Processing path coordinates");
let points: Result<Vec<ElevationPoint>, ArcGISGeometryError> = path
.iter()
.enumerate()
.map(|(idx, coord)| {
if coord.len() < 4 {
let err = ArcGISGeometryError::new(ArcGISGeometryErrorKind::InvalidGeometry(
format!(
"Coordinate {} missing Z or M values (length: {}, expected 4)",
idx,
coord.len()
),
));
tracing::error!(
coord_index = idx,
coord_length = coord.len(),
error = %err,
"Invalid coordinate"
);
return Err(err);
}
let elevation = coord[2]; let distance = coord[3];
tracing::trace!(
coord_index = idx,
distance_m = distance,
elevation_m = elevation,
"Parsed elevation point"
);
Ok(ElevationPoint::new(distance, elevation))
})
.collect();
let points = points?;
tracing::debug!(
point_count = points.len(),
"Successfully extracted elevation points"
);
Ok(points)
}
}
#[derive(Debug, Clone, PartialEq, Getters)]
pub struct ElevationPoint {
distance_meters: f64,
elevation_meters: f64,
}
impl ElevationPoint {
pub fn new(distance_meters: f64, elevation_meters: f64) -> Self {
Self {
distance_meters,
elevation_meters,
}
}
}
#[derive(Debug, Clone, Serialize, derive_builder::Builder, Getters)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct SummarizeElevationParameters {
#[serde(rename = "InputFeatures")]
input_features: String,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "FeatureIDField")]
feature_id_field: Option<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "DEMResolution")]
dem_resolution: Option<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "IncludeSlopeAspect")]
include_slope_aspect: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters)]
#[serde(rename_all = "PascalCase")]
pub struct SummarizeElevationResult {
#[serde(skip_serializing_if = "Option::is_none")]
min_elevation: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
mean_elevation: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
max_elevation: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
min_slope: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
mean_slope: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
max_slope: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
mean_aspect: Option<f64>,
}
impl SummarizeElevationResult {
#[instrument(skip(feature_set))]
pub fn from_feature_set(feature_set: &FeatureSet) -> Result<Self, ArcGISGeometryError> {
tracing::debug!("Parsing SummarizeElevationResult from FeatureSet");
let features = feature_set.features();
if features.is_empty() {
let err = ArcGISGeometryError::new(ArcGISGeometryErrorKind::InvalidGeometry(
"SummarizeElevation result has no features".to_string(),
));
tracing::error!(error = %err, "No features in result");
return Err(err);
}
let feature = &features[0];
let attrs = feature.attributes();
let result = Self {
min_elevation: attrs
.get("MinElevation")
.and_then(|v: &serde_json::Value| v.as_f64()),
mean_elevation: attrs
.get("MeanElevation")
.and_then(|v: &serde_json::Value| v.as_f64()),
max_elevation: attrs
.get("MaxElevation")
.and_then(|v: &serde_json::Value| v.as_f64()),
min_slope: attrs
.get("MinSlope")
.and_then(|v: &serde_json::Value| v.as_f64()),
mean_slope: attrs
.get("MeanSlope")
.and_then(|v: &serde_json::Value| v.as_f64()),
max_slope: attrs
.get("MaxSlope")
.and_then(|v: &serde_json::Value| v.as_f64()),
mean_aspect: attrs
.get("MeanAspect")
.and_then(|v: &serde_json::Value| v.as_f64()),
};
tracing::debug!(
min_elevation = ?result.min_elevation,
mean_elevation = ?result.mean_elevation,
max_elevation = ?result.max_elevation,
"Parsed elevation statistics"
);
Ok(result)
}
}
#[derive(Debug, Clone, Serialize, derive_builder::Builder, Getters)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct ViewshedParameters {
#[serde(rename = "InputPoints")]
input_points: String,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "MaximumDistance")]
maximum_distance: Option<f64>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "MaximumDistanceUnits")]
maximum_distance_units: Option<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "DEMResolution")]
dem_resolution: Option<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "ObserverHeight")]
observer_height: Option<f64>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "ObserverHeightUnits")]
observer_height_units: Option<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "SurfaceOffset")]
surface_offset: Option<f64>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "SurfaceOffsetUnits")]
surface_offset_units: Option<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "GeneralizeViewshedPolygons")]
generalize_viewshed_polygons: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Getters)]
pub struct ViewshedResult {
output_viewshed: FeatureSet,
}
impl ViewshedResult {
pub fn new(output_viewshed: FeatureSet) -> Self {
Self { output_viewshed }
}
pub fn viewshed_count(&self) -> usize {
self.output_viewshed.features().len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DemResolution {
Finest,
#[serde(rename = "10m")]
TenMeter,
#[serde(rename = "30m")]
ThirtyMeter,
#[serde(rename = "90m")]
NinetyMeter,
}
impl DemResolution {
pub fn as_str(&self) -> &'static str {
match self {
DemResolution::Finest => "FINEST",
DemResolution::TenMeter => "10m",
DemResolution::ThirtyMeter => "30m",
DemResolution::NinetyMeter => "90m",
}
}
}