use crate::{
ArcGISClient, ArcGISPoint, GeocodeResponse, LocationType, Result, ReverseGeocodeResponse,
SuggestResponse,
};
use tracing::instrument;
pub struct GeocodeServiceClient<'a> {
base_url: String,
client: &'a ArcGISClient,
}
impl<'a> GeocodeServiceClient<'a> {
#[instrument(skip(base_url, client))]
pub fn new(base_url: impl Into<String>, client: &'a ArcGISClient) -> Self {
let base_url = base_url.into();
tracing::debug!(base_url = %base_url, "Creating GeocodeServiceClient");
Self { base_url, client }
}
#[instrument(skip(self, address), fields(base_url = %self.base_url))]
pub async fn find_address_candidates(
&self,
address: impl Into<String>,
) -> Result<GeocodeResponse> {
let address = address.into();
tracing::debug!(address = %address, "Finding address candidates");
let url = format!("{}/findAddressCandidates", self.base_url);
tracing::debug!(url = %url, "Sending findAddressCandidates request");
let mut request = self.client.http().get(&url).query(&[
("SingleLine", address.as_str()),
("f", "json"),
("outFields", "*"),
("maxLocations", "50"),
]);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "findAddressCandidates failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let geocode_response: GeocodeResponse = response.json().await?;
tracing::info!(
candidate_count = geocode_response.candidates().len(),
"findAddressCandidates completed"
);
Ok(geocode_response)
}
#[instrument(skip(self, address), fields(base_url = %self.base_url))]
pub async fn find_address_candidates_with_options(
&self,
address: impl Into<String>,
max_locations: Option<u32>,
location_type: Option<LocationType>,
) -> Result<GeocodeResponse> {
let address = address.into();
tracing::debug!(address = %address, "Finding address candidates with options");
let url = format!("{}/findAddressCandidates", self.base_url);
let max_locs = max_locations.unwrap_or(50).to_string();
let mut params = vec![
("SingleLine", address.as_str()),
("f", "json"),
("outFields", "*"),
("maxLocations", max_locs.as_str()),
];
let loc_type_str;
if let Some(lt) = location_type {
loc_type_str = match lt {
LocationType::Rooftop => "rooftop",
LocationType::Street => "street",
};
params.push(("locationType", loc_type_str));
}
tracing::debug!(url = %url, "Sending findAddressCandidates request");
let mut request = self.client.http().get(&url).query(¶ms);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "findAddressCandidates failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let geocode_response: GeocodeResponse = response.json().await?;
tracing::info!(
candidate_count = geocode_response.candidates().len(),
"findAddressCandidates completed"
);
Ok(geocode_response)
}
#[instrument(skip(self, address), fields(out_sr = out_sr))]
pub async fn find_address_candidates_with_sr(
&self,
address: impl Into<String>,
out_sr: i32,
) -> Result<GeocodeResponse> {
let address = address.into();
tracing::debug!(address = %address, out_sr = out_sr, "Finding address candidates with custom SR");
let url = format!("{}/findAddressCandidates", self.base_url);
tracing::debug!(url = %url, "Sending findAddressCandidates request");
let mut request = self.client.http().get(&url).query(&[
("SingleLine", address.as_str()),
("outSR", out_sr.to_string().as_str()),
("f", "json"),
("outFields", "*"),
("maxLocations", "50"),
]);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "findAddressCandidates with SR failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let geocode_response: GeocodeResponse = response.json().await?;
tracing::info!(
candidate_count = geocode_response.candidates().len(),
out_sr = out_sr,
"findAddressCandidates with SR completed"
);
Ok(geocode_response)
}
#[instrument(skip(self, location), fields(base_url = %self.base_url, x = *location.x(), y = *location.y()))]
pub async fn reverse_geocode(&self, location: &ArcGISPoint) -> Result<ReverseGeocodeResponse> {
tracing::debug!(
x = *location.x(),
y = *location.y(),
"Reverse geocoding location"
);
let url = format!("{}/reverseGeocode", self.base_url);
let location_str = format!("{},{}", location.x(), location.y());
tracing::debug!(url = %url, location = %location_str, "Sending reverseGeocode request");
let mut request = self
.client
.http()
.get(&url)
.query(&[("location", location_str.as_str()), ("f", "json")]);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "reverseGeocode failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let reverse_response: ReverseGeocodeResponse = response.json().await?;
tracing::info!(
address = ?reverse_response.address().match_addr(),
"reverseGeocode completed"
);
Ok(reverse_response)
}
#[instrument(skip(self, location), fields(x = *location.x(), y = *location.y(), out_sr = out_sr))]
pub async fn reverse_geocode_with_sr(
&self,
location: &ArcGISPoint,
out_sr: i32,
) -> Result<ReverseGeocodeResponse> {
tracing::debug!(
x = *location.x(),
y = *location.y(),
out_sr = out_sr,
"Reverse geocoding location with custom SR"
);
let url = format!("{}/reverseGeocode", self.base_url);
let location_str = format!("{},{}", location.x(), location.y());
tracing::debug!(url = %url, location = %location_str, "Sending reverseGeocode request");
let mut request = self.client.http().get(&url).query(&[
("location", location_str.as_str()),
("outSR", out_sr.to_string().as_str()),
("f", "json"),
]);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "reverseGeocode with SR failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let reverse_response: ReverseGeocodeResponse = response.json().await?;
tracing::info!(
address = ?reverse_response.address().match_addr(),
out_sr = out_sr,
"reverseGeocode with SR completed"
);
Ok(reverse_response)
}
#[instrument(skip(self, text), fields(base_url = %self.base_url))]
pub async fn suggest(&self, text: impl Into<String>) -> Result<SuggestResponse> {
let text = text.into();
tracing::debug!(text = %text, "Getting autocomplete suggestions");
let url = format!("{}/suggest", self.base_url);
tracing::debug!(url = %url, "Sending suggest request");
let mut request = self
.client
.http()
.get(&url)
.query(&[("text", text.as_str()), ("f", "json")]);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "suggest failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let suggest_response: SuggestResponse = response.json().await?;
tracing::info!(
suggestion_count = suggest_response.suggestions().len(),
"suggest completed"
);
Ok(suggest_response)
}
#[instrument(skip(self, text, category), fields(text_len = text.as_ref().len()))]
pub async fn suggest_with_category(
&self,
text: impl Into<String> + AsRef<str>,
category: crate::Category,
) -> Result<SuggestResponse> {
let text = text.into();
tracing::debug!(text = %text, category = ?category, "Getting category-filtered suggestions");
let url = format!("{}/suggest", self.base_url);
tracing::debug!(url = %url, "Sending suggest request with category filter");
let mut request = self.client.http().get(&url).query(&[
("text", text.as_str()),
("category", category.as_str()),
("f", "json"),
]);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "suggest with category failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let suggest_response: SuggestResponse = response.json().await?;
tracing::info!(
suggestion_count = suggest_response.suggestions().len(),
category = ?category,
"suggest with category completed"
);
Ok(suggest_response)
}
#[instrument(skip(self, addresses), fields(count = addresses.len()))]
pub async fn geocode_addresses(
&self,
addresses: Vec<crate::BatchGeocodeRecord>,
) -> Result<crate::BatchGeocodeResponse> {
tracing::debug!("Batch geocoding addresses");
let url = format!("{}/geocodeAddresses", self.base_url);
let records: Vec<serde_json::Value> = addresses
.iter()
.map(|addr| {
serde_json::json!({
"attributes": addr
})
})
.collect();
let addresses_json = serde_json::json!({
"records": records
});
tracing::debug!(url = %url, count = addresses.len(), "Sending geocodeAddresses request");
let addresses_str = addresses_json.to_string();
let mut form = vec![("addresses", addresses_str.as_str()), ("f", "json")];
let token_opt = self.client.get_token_if_required().await?;
let token_str;
if let Some(token) = token_opt {
token_str = token;
form.push(("token", token_str.as_str()));
}
let response = self.client.http().post(&url).form(&form).send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "geocodeAddresses failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let batch_response: crate::BatchGeocodeResponse = response.json().await?;
tracing::info!(
location_count = batch_response.locations().len(),
"geocodeAddresses completed"
);
Ok(batch_response)
}
}