Skip to content

Commit 68e61a9

Browse files
crumplecupclaude
andcommitted
feat(routing): add comprehensive examples and fix type mismatches
Extended routing_navigation.rs example to test all 4 routing methods with comprehensive assertions and minimal API credit usage. Fixed multiple type mismatches discovered during testing. Key changes: - Enhanced service_area example with polygon/geometry validation assertions - Enhanced closest_facility example with route/facility/incident assertions - Added new od_cost_matrix example (minimal 2×2 matrix for credit conservation) - Fixed Route field names to match API (Total_Miles, Total_TravelTime) - Fixed ClosestFacilityResult to use custom deserializer with FeatureSet conversion - Fixed ODCostMatrixResult to handle nested map response format - Fixed OD cost matrix endpoint from /generateOriginDestinationCostMatrix to /solveODCostMatrix - Added NALocation::from_feature() for deserializing facilities/incidents - Added impedance/accumulate attribute parameters to client methods - Removed unused ClosestFacilityFeatureSet type Testing: - All 4 routing examples pass with real API - Service area: 1 facility, 3 breaks (✅ 3 polygons) - Closest facility: 1 incident, 3 facilities (✅ 1 route + facilities/incidents) - OD cost matrix: 2×2 matrix (✅ 4 cost lines) - Comprehensive assertions validate response structure and values Files modified: - examples/enterprise/routing_navigation.rs - Added OD matrix example, enhanced assertions - src/services/routing/client.rs - Fixed endpoints, added accumulate parameters - src/services/routing/types.rs - Fixed Route field mappings, custom deserializers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 5c9168d commit 68e61a9

3 files changed

Lines changed: 438 additions & 35 deletions

File tree

examples/enterprise/routing_navigation.rs

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
//! # What You'll Learn
88
//!
99
//! - **Multi-stop routing**: Optimize routes through multiple cities
10-
//! - **Service areas**: Generate drive-time polygons (30-minute zones)
10+
//! - **Service areas**: Generate drive-time polygons (15, 30, 45-minute zones)
1111
//! - **Closest facility**: Find nearest services (gas stations, rest stops)
12+
//! - **OD cost matrix**: Calculate travel times between multiple origins/destinations
1213
//! - **Error handling**: Graceful handling of routing failures
1314
//! - **Builder patterns**: Construct complex routing parameters
15+
//! - **Comprehensive assertions**: Validate all API responses
1416
//!
1517
//! # Prerequisites
1618
//!
@@ -42,14 +44,16 @@
4244
use anyhow::Result;
4345
use arcgis::{
4446
ApiKeyAuth, ApiKeyTier, ArcGISClient, ArcGISGeometry, ArcGISPoint, ClosestFacilityParameters,
45-
NALocation, RouteParameters, RoutingServiceClient, ServiceAreaParameters, TravelDirection,
47+
NALocation, ODCostMatrixParameters, RouteParameters, RoutingServiceClient,
48+
ServiceAreaParameters, TravelDirection,
4649
};
4750

4851
/// World Routing Service endpoints
4952
const ROUTE_SERVICE: &str =
5053
"https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World";
5154
const SERVICE_AREA_SERVICE: &str = "https://route-api.arcgis.com/arcgis/rest/services/World/ServiceAreas/NAServer/ServiceArea_World";
5255
const CLOSEST_FACILITY_SERVICE: &str = "https://route-api.arcgis.com/arcgis/rest/services/World/ClosestFacility/NAServer/ClosestFacility_World";
56+
const OD_COST_MATRIX_SERVICE: &str = "https://route-api.arcgis.com/arcgis/rest/services/World/OriginDestinationCostMatrix/NAServer/OriginDestinationCostMatrix_World";
5357

5458
#[tokio::main]
5559
async fn main() -> Result<()> {
@@ -73,6 +77,7 @@ async fn main() -> Result<()> {
7377
demonstrate_multi_stop_route(&client).await?;
7478
demonstrate_service_area(&client).await?;
7579
demonstrate_closest_facility(&client).await?;
80+
demonstrate_od_cost_matrix(&client).await?;
7681

7782
tracing::info!("\n✅ All routing examples completed successfully!");
7883
print_best_practices();
@@ -163,14 +168,39 @@ async fn demonstrate_service_area(client: &ArcGISClient) -> Result<()> {
163168
"✅ Service area polygons generated"
164169
);
165170

171+
// Validate results
172+
anyhow::ensure!(
173+
!service_area_result.service_area_polygons().is_empty(),
174+
"Should generate at least one service area polygon"
175+
);
176+
177+
anyhow::ensure!(
178+
service_area_result.service_area_polygons().len() == 3,
179+
"Should generate 3 polygons (for 15, 30, 45 min breaks), got {}",
180+
service_area_result.service_area_polygons().len()
181+
);
182+
166183
tracing::info!("📊 Drive-time zones from San Francisco:");
167184
for (i, polygon) in service_area_result
168185
.service_area_polygons()
169186
.iter()
170187
.enumerate()
171188
{
189+
anyhow::ensure!(
190+
polygon.geometry().is_some(),
191+
"Polygon {} should have geometry",
192+
i + 1
193+
);
194+
172195
if let Some(from_break) = polygon.from_break() {
173196
if let Some(to_break) = polygon.to_break() {
197+
anyhow::ensure!(
198+
from_break < to_break,
199+
"from_break ({}) should be less than to_break ({})",
200+
from_break,
201+
to_break
202+
);
203+
174204
tracing::info!(
175205
" Zone {}: {}-{} minute drive time",
176206
i + 1,
@@ -207,6 +237,7 @@ async fn demonstrate_closest_facility(client: &ArcGISClient) -> Result<()> {
207237
.default_target_facility_count(1) // Find closest 1
208238
.return_routes(true)
209239
.travel_direction(TravelDirection::ToFacility) // From incident to facility
240+
.accumulate_attribute_names(vec!["Miles".to_string()]) // Request distance attribute
210241
.build()?;
211242

212243
tracing::debug!("Finding nearest gas station");
@@ -219,10 +250,46 @@ async fn demonstrate_closest_facility(client: &ArcGISClient) -> Result<()> {
219250
"✅ Found closest facility route"
220251
);
221252

253+
// Validate results
254+
anyhow::ensure!(
255+
!closest_result.routes().is_empty(),
256+
"Should find at least one route to closest facility"
257+
);
258+
259+
anyhow::ensure!(
260+
closest_result.routes().len() == 1,
261+
"Should find exactly 1 route (defaultTargetFacilityCount=1), got {}",
262+
closest_result.routes().len()
263+
);
264+
265+
anyhow::ensure!(
266+
!closest_result.facilities().is_empty(),
267+
"Should have facilities in result"
268+
);
269+
270+
anyhow::ensure!(
271+
!closest_result.incidents().is_empty(),
272+
"Should have incidents in result"
273+
);
274+
222275
if let Some(route) = closest_result.routes().first() {
223276
let distance_miles = route.total_length().unwrap_or(0.0);
224277
let time_minutes = route.total_time().unwrap_or(0.0);
225278

279+
anyhow::ensure!(
280+
distance_miles > 0.0,
281+
"Route distance should be positive, got {}",
282+
distance_miles
283+
);
284+
285+
anyhow::ensure!(
286+
time_minutes > 0.0,
287+
"Route time should be positive, got {}",
288+
time_minutes
289+
);
290+
291+
anyhow::ensure!(route.geometry().is_some(), "Route should have geometry");
292+
226293
tracing::info!("⛽ Closest gas station:");
227294
tracing::info!(" Distance: {:.2} miles away", distance_miles);
228295
tracing::info!(" Drive time: {:.1} minutes", time_minutes);
@@ -246,6 +313,100 @@ async fn demonstrate_closest_facility(client: &ArcGISClient) -> Result<()> {
246313
Ok(())
247314
}
248315

316+
/// Demonstrates origin-destination cost matrix calculation.
317+
///
318+
/// MINIMAL API USAGE: Only 2 origins × 2 destinations = 4 cost calculations.
319+
async fn demonstrate_od_cost_matrix(client: &ArcGISClient) -> Result<()> {
320+
tracing::info!("\n=== Example 4: Travel Cost Matrix ===");
321+
tracing::info!("Calculate all travel times between offices (2 origins × 2 destinations)");
322+
tracing::info!("⚠️ Minimal usage: 2x2 matrix to conserve API credits");
323+
324+
let od_matrix_client = RoutingServiceClient::new(OD_COST_MATRIX_SERVICE, client);
325+
326+
// Origins: Company offices in Bay Area
327+
let origin_sf = create_location(-122.4194, 37.7749, "SF Office");
328+
let origin_oakland = create_location(-122.2711, 37.8044, "Oakland Office");
329+
330+
// Destinations: Client sites
331+
let dest_san_jose = create_location(-121.8863, 37.3382, "San Jose Client");
332+
let dest_palo_alto = create_location(-122.1430, 37.4419, "Palo Alto Client");
333+
334+
let od_params = ODCostMatrixParameters::builder()
335+
.origins(vec![origin_sf, origin_oakland])
336+
.destinations(vec![dest_san_jose, dest_palo_alto])
337+
.accumulate_attribute_names(vec!["Miles".to_string()]) // Request distance attribute
338+
.build()?;
339+
340+
tracing::debug!("Calculating OD cost matrix");
341+
let od_result = od_matrix_client.generate_od_cost_matrix(od_params).await?;
342+
343+
tracing::info!(
344+
od_line_count = od_result.od_lines().len(),
345+
"✅ Cost matrix calculated"
346+
);
347+
348+
// Validate results
349+
anyhow::ensure!(
350+
!od_result.od_lines().is_empty(),
351+
"Should generate OD cost matrix lines"
352+
);
353+
354+
anyhow::ensure!(
355+
od_result.od_lines().len() == 4,
356+
"Should generate 4 OD lines (2 origins × 2 destinations), got {}",
357+
od_result.od_lines().len()
358+
);
359+
360+
tracing::info!("📊 Travel time matrix:");
361+
tracing::info!(" From → To Time Distance");
362+
tracing::info!(" ================================================");
363+
364+
for od_line in od_result.od_lines() {
365+
let time_mins = od_line.total_time().unwrap_or(0.0);
366+
let distance_miles = od_line.total_distance().unwrap_or(0.0);
367+
368+
anyhow::ensure!(
369+
time_mins > 0.0,
370+
"Travel time should be positive, got {}",
371+
time_mins
372+
);
373+
374+
anyhow::ensure!(
375+
distance_miles > 0.0,
376+
"Distance should be positive, got {}",
377+
distance_miles
378+
);
379+
380+
let origin_name = od_result
381+
.origins()
382+
.get(od_line.origin_id().unwrap_or(0) as usize - 1)
383+
.and_then(|o| o.name().as_deref())
384+
.unwrap_or("Unknown");
385+
386+
let dest_name = od_result
387+
.destinations()
388+
.get(od_line.destination_id().unwrap_or(0) as usize - 1)
389+
.and_then(|d| d.name().as_deref())
390+
.unwrap_or("Unknown");
391+
392+
tracing::info!(
393+
" {} → {} {:>6.1} min {:>7.2} mi",
394+
origin_name,
395+
dest_name,
396+
time_mins,
397+
distance_miles
398+
);
399+
}
400+
401+
tracing::info!("");
402+
tracing::info!("💡 Use cases: Multi-location logistics, delivery route optimization");
403+
tracing::info!(" - Compare all origin-destination pairs efficiently");
404+
tracing::info!(" - No routes/directions - just travel costs (faster/cheaper)");
405+
tracing::info!(" - Perfect for fleet dispatching and territory analysis");
406+
407+
Ok(())
408+
}
409+
249410
/// Prints best practices for routing and navigation.
250411
fn print_best_practices() {
251412
tracing::info!("\n💡 Routing Best Practices:");
@@ -259,6 +420,7 @@ fn print_best_practices() {
259420
tracing::info!(" - Route: Multi-stop trip planning, delivery routes");
260421
tracing::info!(" - Service Area: Coverage analysis, accessibility zones");
261422
tracing::info!(" - Closest Facility: Emergency response, nearest service finder");
423+
tracing::info!(" - OD Cost Matrix: Multi-location logistics, fleet dispatching");
262424
tracing::info!("");
263425
tracing::info!("⚡ Performance Tips:");
264426
tracing::info!(" - Batch multiple route calculations when possible");
@@ -271,6 +433,7 @@ fn print_best_practices() {
271433
tracing::info!(" - Optimized route (10+ stops): ~1.0 credits");
272434
tracing::info!(" - Service area: ~0.5 credits per facility");
273435
tracing::info!(" - Closest facility: ~0.5 credits");
436+
tracing::info!(" - OD cost matrix (2×2): ~0.5 credits");
274437
tracing::info!(" ⚠️ Monitor your ArcGIS Online quota!");
275438
}
276439

src/services/routing/client.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,10 @@ impl<'a> RoutingServiceClient<'a> {
393393
));
394394
}
395395

396+
// Always request facilities and incidents in response for validation
397+
form.push(("returnFacilities", "true"));
398+
form.push(("returnIncidents", "true"));
399+
396400
if let Some(travel_direction) = params.travel_direction() {
397401
let direction_str = match travel_direction {
398402
crate::TravelDirection::FromFacility => "esriNATravelDirectionFromFacility",
@@ -401,6 +405,21 @@ impl<'a> RoutingServiceClient<'a> {
401405
form.push(("travelDirection", direction_str));
402406
}
403407

408+
// Handle impedance attribute
409+
let impedance_str;
410+
if let Some(impedance) = params.impedance_attribute() {
411+
impedance_str = serde_json::to_string(impedance)?;
412+
let impedance_value = impedance_str.trim_matches('"');
413+
form.push(("impedanceAttributeName", impedance_value));
414+
}
415+
416+
// Handle accumulate attributes
417+
let accumulate_str;
418+
if let Some(accumulate) = params.accumulate_attribute_names() {
419+
accumulate_str = accumulate.join(",");
420+
form.push(("accumulateAttributeNames", accumulate_str.as_str()));
421+
}
422+
404423
// Add token if required by auth provider
405424
let token_opt = self.client.get_token_if_required().await?;
406425
let token_str;
@@ -428,6 +447,8 @@ impl<'a> RoutingServiceClient<'a> {
428447

429448
tracing::info!(
430449
route_count = result.routes().len(),
450+
facility_count = result.facilities().len(),
451+
incident_count = result.incidents().len(),
431452
"solve closest facility completed"
432453
);
433454

@@ -482,7 +503,7 @@ impl<'a> RoutingServiceClient<'a> {
482503
) -> Result<ODCostMatrixResult> {
483504
tracing::debug!("Generating OD cost matrix");
484505

485-
let url = format!("{}/generateOriginDestinationCostMatrix", self.base_url);
506+
let url = format!("{}/solveODCostMatrix", self.base_url);
486507

487508
tracing::debug!(url = %url, "Sending OD cost matrix request");
488509

@@ -521,6 +542,21 @@ impl<'a> RoutingServiceClient<'a> {
521542
form.push(("outSR", out_sr.as_str()));
522543
}
523544

545+
// Handle impedance attribute
546+
let impedance_str;
547+
if let Some(impedance) = params.impedance_attribute() {
548+
impedance_str = serde_json::to_string(impedance)?;
549+
let impedance_value = impedance_str.trim_matches('"');
550+
form.push(("impedanceAttributeName", impedance_value));
551+
}
552+
553+
// Handle accumulate attributes
554+
let accumulate_str;
555+
if let Some(accumulate) = params.accumulate_attribute_names() {
556+
accumulate_str = accumulate.join(",");
557+
form.push(("accumulateAttributeNames", accumulate_str.as_str()));
558+
}
559+
524560
// Add token if required by auth provider
525561
let token_opt = self.client.get_token_if_required().await?;
526562
let token_str;

0 commit comments

Comments
 (0)