use crate::{ArcGISClient, ErrorKind, FeatureSet, GPExecuteResult, Result};
use tracing::instrument;
use super::types::{
ProfileParameters, ProfileResult, SummarizeElevationParameters, SummarizeElevationResult,
ViewshedParameters, ViewshedResult,
};
#[derive(Clone)]
pub struct ElevationClient<'a> {
url: String,
client: &'a ArcGISClient,
}
impl<'a> ElevationClient<'a> {
pub fn new(client: &'a ArcGISClient) -> Self {
ElevationClient {
url: "https://elevation.arcgis.com/arcgis/rest/services/Tools/ElevationSync/GPServer"
.to_string(),
client,
}
}
pub fn with_url(url: impl Into<String>, client: &'a ArcGISClient) -> Self {
ElevationClient {
url: url.into(),
client,
}
}
#[instrument(skip(self, params))]
pub async fn profile(&self, params: ProfileParameters) -> Result<ProfileResult> {
tracing::debug!("Generating elevation profile");
let profile_url = format!("{}/Profile/execute", self.url);
let mut request = self
.client
.http()
.get(&profile_url)
.query(&[("f", "json")])
.query(¶ms);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
tracing::debug!(url = %profile_url, "Sending profile request");
let response = request.send().await?;
let response_body = response.text().await?;
tracing::debug!(
response_length = response_body.len(),
response_body = %response_body,
"Received profile response"
);
let gp_result: GPExecuteResult = serde_json::from_str(&response_body)?;
tracing::debug!(
result_count = gp_result.results().len(),
message_count = gp_result.messages().len(),
"Parsed GP execute result"
);
let output_param = gp_result.results().first().ok_or_else(|| {
tracing::error!("GP result missing results array");
crate::Error::from(ErrorKind::Api {
code: 0,
message: "Elevation profile result missing results array".to_string(),
})
})?;
tracing::debug!(
param_name = ?output_param.param_name(),
data_type = ?output_param.data_type(),
"Extracting profile parameter"
);
let feature_set_value = output_param.value().as_ref().ok_or_else(|| {
tracing::error!("OutputProfile parameter missing value");
crate::Error::from(ErrorKind::Api {
code: 0,
message: "Elevation profile parameter missing value field".to_string(),
})
})?;
let feature_set: FeatureSet = serde_json::from_value(feature_set_value.clone())?;
tracing::debug!(
feature_count = feature_set.features().len(),
geometry_type = ?feature_set.geometry_type(),
"Extracted profile FeatureSet"
);
let result = ProfileResult::new(feature_set);
tracing::debug!("Profile generated");
Ok(result)
}
#[instrument(skip(self, params))]
pub async fn submit_summarize_elevation(
&self,
params: SummarizeElevationParameters,
) -> Result<crate::GPJobInfo> {
tracing::debug!("Submitting SummarizeElevation job");
let gp_service = crate::GeoprocessingServiceClient::new(
"https://elevation.arcgis.com/arcgis/rest/services/Tools/Elevation/GPServer/SummarizeElevation",
self.client,
);
let param_map = self.params_to_hashmap(¶ms)?;
let job = gp_service.submit_job(param_map).await?;
tracing::info!(
job_id = %job.job_id(),
status = ?job.job_status(),
"SummarizeElevation job submitted"
);
Ok(job)
}
#[instrument(skip(self), fields(job_id, timeout_ms))]
pub async fn poll_summarize_elevation(
&self,
job_id: &str,
timeout_ms: Option<u64>,
) -> Result<SummarizeElevationResult> {
tracing::debug!("Polling SummarizeElevation job");
let gp_service = crate::GeoprocessingServiceClient::new(
"https://elevation.arcgis.com/arcgis/rest/services/Tools/Elevation/GPServer/SummarizeElevation",
self.client,
);
let job_info = gp_service
.poll_until_complete(job_id, 2000, 5000, timeout_ms.or(Some(60000)))
.await?;
tracing::debug!(
job_id = %job_info.job_id(),
status = ?job_info.job_status(),
"Job completed"
);
self.extract_summarize_result(&job_info).await
}
fn params_to_hashmap<T: serde::Serialize>(
&self,
params: &T,
) -> Result<std::collections::HashMap<String, serde_json::Value>> {
use std::collections::HashMap;
let json_value = serde_json::to_value(params)?;
let map = json_value
.as_object()
.ok_or_else(|| crate::BuilderError::new("Failed to convert params to map"))?
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<HashMap<String, serde_json::Value>>();
Ok(map)
}
async fn extract_summarize_result(
&self,
job_info: &crate::GPJobInfo,
) -> Result<SummarizeElevationResult> {
tracing::debug!("Extracting SummarizeElevation result from GP response");
let results = job_info.results();
tracing::debug!(
result_count = results.len(),
result_keys = ?results.keys().collect::<Vec<_>>(),
"Available result parameters"
);
let output_summary_param = results.get("OutputSummary").ok_or_else(|| {
tracing::error!("Results missing OutputSummary parameter");
crate::Error::from(ErrorKind::Api {
code: 0,
message: "SummarizeElevation result missing OutputSummary parameter".to_string(),
})
})?;
let output_summary = if let Some(value) = output_summary_param.value() {
tracing::debug!("Using OutputSummary value from inline response");
value.clone()
} else if let Some(param_url) = output_summary_param.param_url() {
tracing::debug!(param_url = %param_url, "Fetching OutputSummary from paramUrl");
let gp_service = crate::GeoprocessingServiceClient::new(
"https://elevation.arcgis.com/arcgis/rest/services/Tools/Elevation/GPServer/SummarizeElevation",
self.client,
);
let result_json = gp_service
.get_result_data(job_info.job_id(), "OutputSummary")
.await?;
result_json
.get("value")
.ok_or_else(|| {
tracing::error!("Result data missing 'value' field");
crate::Error::from(ErrorKind::Api {
code: 0,
message: "OutputSummary result data missing 'value' field".to_string(),
})
})?
.clone()
} else {
tracing::error!("OutputSummary parameter has neither value nor paramUrl");
return Err(crate::Error::from(ErrorKind::Api {
code: 0,
message: "OutputSummary parameter has no value or paramUrl".to_string(),
}));
};
let feature_set: FeatureSet = serde_json::from_value(output_summary.clone())?;
tracing::debug!(
feature_count = feature_set.features().len(),
"Parsed OutputSummary FeatureSet"
);
let result = SummarizeElevationResult::from_feature_set(&feature_set).map_err(|e| {
crate::Error::from(ErrorKind::Api {
code: 0,
message: format!("Failed to parse elevation statistics: {}", e),
})
})?;
Ok(result)
}
#[instrument(skip(self, params))]
pub async fn submit_viewshed(&self, params: ViewshedParameters) -> Result<crate::GPJobInfo> {
tracing::debug!("Submitting Viewshed job");
let gp_service = crate::GeoprocessingServiceClient::new(
"https://elevation.arcgis.com/arcgis/rest/services/Tools/Elevation/GPServer/Viewshed",
self.client,
);
let param_map = self.params_to_hashmap(¶ms)?;
let job = gp_service.submit_job(param_map).await?;
tracing::info!(
job_id = %job.job_id(),
status = ?job.job_status(),
"Viewshed job submitted"
);
Ok(job)
}
#[instrument(skip(self), fields(job_id, timeout_ms))]
pub async fn poll_viewshed(
&self,
job_id: &str,
timeout_ms: Option<u64>,
) -> Result<ViewshedResult> {
tracing::debug!("Polling Viewshed job");
let gp_service = crate::GeoprocessingServiceClient::new(
"https://elevation.arcgis.com/arcgis/rest/services/Tools/Elevation/GPServer/Viewshed",
self.client,
);
let job_info = gp_service
.poll_until_complete(job_id, 2000, 5000, timeout_ms.or(Some(60000)))
.await?;
tracing::debug!(
job_id = %job_info.job_id(),
status = ?job_info.job_status(),
"Job completed"
);
self.extract_viewshed_result(&job_info).await
}
async fn extract_viewshed_result(&self, job_info: &crate::GPJobInfo) -> Result<ViewshedResult> {
tracing::debug!("Extracting Viewshed result from GP response");
let results = job_info.results();
let output_viewshed_param = results.get("OutputViewshed").ok_or_else(|| {
tracing::error!("Results missing OutputViewshed parameter");
crate::Error::from(ErrorKind::Api {
code: 0,
message: "Viewshed result missing OutputViewshed parameter".to_string(),
})
})?;
let output_viewshed = if let Some(value) = output_viewshed_param.value() {
tracing::debug!("Using OutputViewshed value from inline response");
value.clone()
} else if let Some(param_url) = output_viewshed_param.param_url() {
tracing::debug!(param_url = %param_url, "Fetching OutputViewshed from paramUrl");
let gp_service = crate::GeoprocessingServiceClient::new(
"https://elevation.arcgis.com/arcgis/rest/services/Tools/Elevation/GPServer/Viewshed",
self.client,
);
let result_json = gp_service
.get_result_data(job_info.job_id(), "OutputViewshed")
.await?;
result_json
.get("value")
.ok_or_else(|| {
tracing::error!("Result data missing 'value' field");
crate::Error::from(ErrorKind::Api {
code: 0,
message: "OutputViewshed result data missing 'value' field".to_string(),
})
})?
.clone()
} else {
tracing::error!("OutputViewshed parameter has neither value nor paramUrl");
return Err(crate::Error::from(ErrorKind::Api {
code: 0,
message: "OutputViewshed parameter has no value or paramUrl".to_string(),
}));
};
let feature_set: FeatureSet = serde_json::from_value(output_viewshed.clone())?;
tracing::debug!(
feature_count = feature_set.features().len(),
"Parsed OutputViewshed FeatureSet"
);
let result = ViewshedResult::new(feature_set);
Ok(result)
}
}