use anyhow::Result;
use arcgis::example_tracker::ExampleTracker;
use arcgis::{
ApiKeyAuth, ApiKeyTier, ArcGISClient, ArcGISGeometry, ArcGISPoint, ClosestFacilityParameters,
NALocation, ODCostMatrixParameters, RouteParameters, RoutingServiceClient,
ServiceAreaParameters, TravelDirection,
};
const ROUTE_SERVICE: &str =
"https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World";
const SERVICE_AREA_SERVICE: &str = "https://route-api.arcgis.com/arcgis/rest/services/World/ServiceAreas/NAServer/ServiceArea_World";
const CLOSEST_FACILITY_SERVICE: &str = "https://route-api.arcgis.com/arcgis/rest/services/World/ClosestFacility/NAServer/ClosestFacility_World";
const OD_COST_MATRIX_SERVICE: &str = "https://route-api.arcgis.com/arcgis/rest/services/World/OriginDestinationCostMatrix/NAServer/OriginDestinationCostMatrix_World";
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let tracker = ExampleTracker::new("routing_navigation")
.service_type("ExampleClient")
.start();
tracing::info!("πΊοΈ ArcGIS Routing & Navigation Examples");
tracing::info!("Pacific Coast Road Trip: San Francisco β Seattle");
tracing::debug!("Creating authenticated client");
let auth = ApiKeyAuth::from_env(ApiKeyTier::Location)?;
let client = ArcGISClient::new(auth);
demonstrate_multi_stop_route(&client).await?;
demonstrate_service_area(&client).await?;
demonstrate_closest_facility(&client).await?;
demonstrate_od_cost_matrix(&client).await?;
tracing::info!("\nβ
All routing examples completed successfully!");
print_best_practices();
tracker.success();
Ok(())
}
async fn demonstrate_multi_stop_route(client: &ArcGISClient) -> Result<()> {
tracing::info!("\n=== Example 1: Planning Your Road Trip Route ===");
tracing::info!("Calculate optimal route: SF β Portland β Seattle");
let route_service = RoutingServiceClient::new(ROUTE_SERVICE, client);
let san_francisco = create_stop(-122.4194, 37.7749, "San Francisco, CA");
let portland = create_stop(-122.6765, 45.5231, "Portland, OR");
let seattle = create_stop(-122.3321, 47.6062, "Seattle, WA");
let route_params = RouteParameters::builder()
.stops(vec![
san_francisco.clone(),
portland.clone(),
seattle.clone(),
])
.return_directions(true)
.return_routes(true)
.return_stops(true)
.build()?;
tracing::debug!("Sending route request to ArcGIS");
let route_result = route_service.solve_route(route_params).await?;
if let Some(route) = route_result.routes().first() {
let distance_miles = route.total_length().unwrap_or(0.0);
let time_minutes = route.total_time().unwrap_or(0.0);
tracing::info!(
distance_miles = format!("{:.1}", distance_miles),
drive_time_hours = format!("{:.1}", time_minutes / 60.0),
"β
Route calculated successfully!"
);
tracing::info!("π Route summary:");
tracing::info!(" Total distance: {:.1} miles", distance_miles);
tracing::info!(" Estimated drive time: {:.1} hours", time_minutes / 60.0);
let directions = route.directions();
if !directions.is_empty() {
tracing::info!(" Turn-by-turn directions: {} steps", directions.len());
tracing::debug!("First few directions:");
for (i, direction) in directions.iter().take(3).enumerate() {
if let Some(text) = direction.text() {
tracing::debug!(" {}. {}", i + 1, text);
}
}
}
} else {
tracing::warn!("β οΈ No route found in result");
}
Ok(())
}
async fn demonstrate_service_area(client: &ArcGISClient) -> Result<()> {
tracing::info!("\n=== Example 2: Drive-Time Analysis ===");
tracing::info!("Generate 15, 30, and 45-minute drive zones from San Francisco");
let service_area_client = RoutingServiceClient::new(SERVICE_AREA_SERVICE, client);
let san_francisco = create_stop(-122.4194, 37.7749, "San Francisco, CA");
let service_area_params = ServiceAreaParameters::builder()
.facilities(vec![san_francisco])
.default_breaks(vec![15.0, 30.0, 45.0]) .return_polygons(true) .build()?;
tracing::debug!("Calculating service area polygons");
let service_area_result = service_area_client
.solve_service_area(service_area_params)
.await?;
tracing::info!(
polygon_count = service_area_result.service_area_polygons().len(),
"β
Service area polygons generated"
);
anyhow::ensure!(
!service_area_result.service_area_polygons().is_empty(),
"Should generate at least one service area polygon"
);
anyhow::ensure!(
service_area_result.service_area_polygons().len() == 3,
"Should generate 3 polygons (for 15, 30, 45 min breaks), got {}",
service_area_result.service_area_polygons().len()
);
tracing::info!("π Drive-time zones from San Francisco:");
for (i, polygon) in service_area_result
.service_area_polygons()
.iter()
.enumerate()
{
anyhow::ensure!(
polygon.geometry().is_some(),
"Polygon {} should have geometry",
i + 1
);
if let Some(from_break) = polygon.from_break() {
if let Some(to_break) = polygon.to_break() {
anyhow::ensure!(
from_break < to_break,
"from_break ({}) should be less than to_break ({})",
from_break,
to_break
);
tracing::info!(
" Zone {}: {}-{} minute drive time",
i + 1,
from_break,
to_break
);
}
}
}
tracing::info!("π‘ Pro tip: Service areas show reachable regions for delivery planning");
Ok(())
}
async fn demonstrate_closest_facility(client: &ArcGISClient) -> Result<()> {
tracing::info!("\n=== Example 3: Finding Nearest Services ===");
tracing::info!("Scenario: Road trip on I-5 - find closest gas station near San Jose");
let closest_facility_client = RoutingServiceClient::new(CLOSEST_FACILITY_SERVICE, client);
let current_location = create_location(-121.8863, 37.3382, "Downtown San Jose");
let gas_station_1 = create_location(-121.8947, 37.3688, "North San Jose Station");
let gas_station_2 = create_location(-121.8772, 37.3088, "South San Jose Station");
let gas_station_3 = create_location(-121.9025, 37.2893, "Campbell Station");
let closest_facility_params = ClosestFacilityParameters::builder()
.incidents(vec![current_location])
.facilities(vec![gas_station_1, gas_station_2, gas_station_3])
.default_target_facility_count(1) .return_routes(true)
.travel_direction(TravelDirection::ToFacility) .accumulate_attribute_names(vec!["Miles".to_string()]) .build()?;
tracing::debug!("Finding nearest gas station");
let closest_result = closest_facility_client
.solve_closest_facility(closest_facility_params)
.await?;
tracing::info!(
route_count = closest_result.routes().len(),
"β
Found closest facility route"
);
anyhow::ensure!(
!closest_result.routes().is_empty(),
"Should find at least one route to closest facility"
);
anyhow::ensure!(
closest_result.routes().len() == 1,
"Should find exactly 1 route (defaultTargetFacilityCount=1), got {}",
closest_result.routes().len()
);
anyhow::ensure!(
!closest_result.facilities().is_empty(),
"Should have facilities in result"
);
anyhow::ensure!(
!closest_result.incidents().is_empty(),
"Should have incidents in result"
);
if let Some(route) = closest_result.routes().first() {
let distance_miles = route.total_length().unwrap_or(0.0);
let time_minutes = route.total_time().unwrap_or(0.0);
anyhow::ensure!(
distance_miles > 0.0,
"Route distance should be positive, got {}",
distance_miles
);
anyhow::ensure!(
time_minutes > 0.0,
"Route time should be positive, got {}",
time_minutes
);
anyhow::ensure!(route.geometry().is_some(), "Route should have geometry");
tracing::info!("β½ Closest gas station:");
tracing::info!(" Distance: {:.2} miles away", distance_miles);
tracing::info!(" Drive time: {:.1} minutes", time_minutes);
tracing::info!(
" Route geometry points: {}",
if let Some(geom) = route.geometry() {
format!(
"{} points",
match geom {
ArcGISGeometry::Polyline(line) =>
line.paths().iter().map(|p| p.len()).sum::<usize>(),
_ => 0,
}
)
} else {
"No geometry".to_string()
}
);
}
Ok(())
}
async fn demonstrate_od_cost_matrix(client: &ArcGISClient) -> Result<()> {
tracing::info!("\n=== Example 4: Travel Cost Matrix ===");
tracing::info!("Calculate all travel times between offices (2 origins Γ 2 destinations)");
tracing::info!("β οΈ Minimal usage: 2x2 matrix to conserve API credits");
let od_matrix_client = RoutingServiceClient::new(OD_COST_MATRIX_SERVICE, client);
let origin_sf = create_location(-122.4194, 37.7749, "SF Office");
let origin_oakland = create_location(-122.2711, 37.8044, "Oakland Office");
let dest_san_jose = create_location(-121.8863, 37.3382, "San Jose Client");
let dest_palo_alto = create_location(-122.1430, 37.4419, "Palo Alto Client");
let od_params = ODCostMatrixParameters::builder()
.origins(vec![origin_sf, origin_oakland])
.destinations(vec![dest_san_jose, dest_palo_alto])
.accumulate_attribute_names(vec!["Miles".to_string()]) .build()?;
tracing::debug!("Calculating OD cost matrix");
let od_result = od_matrix_client.generate_od_cost_matrix(od_params).await?;
tracing::info!(
od_line_count = od_result.od_lines().len(),
"β
Cost matrix calculated"
);
anyhow::ensure!(
!od_result.od_lines().is_empty(),
"Should generate OD cost matrix lines"
);
anyhow::ensure!(
od_result.od_lines().len() == 4,
"Should generate 4 OD lines (2 origins Γ 2 destinations), got {}",
od_result.od_lines().len()
);
tracing::info!("π Travel time matrix:");
tracing::info!(" From β To Time Distance");
tracing::info!(" ================================================");
for od_line in od_result.od_lines() {
let time_mins = od_line.total_time().unwrap_or(0.0);
let distance_miles = od_line.total_distance().unwrap_or(0.0);
anyhow::ensure!(
time_mins > 0.0,
"Travel time should be positive, got {}",
time_mins
);
anyhow::ensure!(
distance_miles > 0.0,
"Distance should be positive, got {}",
distance_miles
);
let origin_name = od_result
.origins()
.get(od_line.origin_id().unwrap_or(0) as usize - 1)
.and_then(|o| o.name().as_deref())
.unwrap_or("Unknown");
let dest_name = od_result
.destinations()
.get(od_line.destination_id().unwrap_or(0) as usize - 1)
.and_then(|d| d.name().as_deref())
.unwrap_or("Unknown");
tracing::info!(
" {} β {} {:>6.1} min {:>7.2} mi",
origin_name,
dest_name,
time_mins,
distance_miles
);
}
tracing::info!("");
tracing::info!("π‘ Use cases: Multi-location logistics, delivery route optimization");
tracing::info!(" - Compare all origin-destination pairs efficiently");
tracing::info!(" - No routes/directions - just travel costs (faster/cheaper)");
tracing::info!(" - Perfect for fleet dispatching and territory analysis");
Ok(())
}
fn print_best_practices() {
tracing::info!("\nπ‘ Routing Best Practices:");
tracing::info!(" - Cache route results to minimize API calls and costs");
tracing::info!(" - Use service areas for coverage/accessibility analysis");
tracing::info!(" - Closest facility is perfect for emergency response planning");
tracing::info!(" - Always check total_miles/total_minutes for route validation");
tracing::info!(" - Consider traffic patterns with time-of-day routing (premium feature)");
tracing::info!("");
tracing::info!("π― When to Use Each Service:");
tracing::info!(" - Route: Multi-stop trip planning, delivery routes");
tracing::info!(" - Service Area: Coverage analysis, accessibility zones");
tracing::info!(" - Closest Facility: Emergency response, nearest service finder");
tracing::info!(" - OD Cost Matrix: Multi-location logistics, fleet dispatching");
tracing::info!("");
tracing::info!("β‘ Performance Tips:");
tracing::info!(" - Batch multiple route calculations when possible");
tracing::info!(" - Request only needed attributes (directions, geometry)");
tracing::info!(" - Use straight-line distance for rough estimates first");
tracing::info!(" - Consider caching frequently-requested routes");
tracing::info!("");
tracing::info!("π° Credit Usage:");
tracing::info!(" - Simple route (2 stops): ~0.5 credits");
tracing::info!(" - Optimized route (10+ stops): ~1.0 credits");
tracing::info!(" - Service area: ~0.5 credits per facility");
tracing::info!(" - Closest facility: ~0.5 credits");
tracing::info!(" - OD cost matrix (2Γ2): ~0.5 credits");
tracing::info!(" β οΈ Monitor your ArcGIS Online quota!");
}
fn create_stop(lon: f64, lat: f64, name: &str) -> NALocation {
NALocation::new(ArcGISGeometry::Point(
ArcGISPoint::new(lon, lat).with_spatial_reference(Some(arcgis::SpatialReference::wgs84())),
))
.with_name(name.to_string())
}
fn create_location(lon: f64, lat: f64, name: &str) -> NALocation {
NALocation::new(ArcGISGeometry::Point(
ArcGISPoint::new(lon, lat).with_spatial_reference(Some(arcgis::SpatialReference::wgs84())),
))
.with_name(name.to_string())
}