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//!
4244use anyhow:: Result ;
4345use 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
4952const ROUTE_SERVICE : & str =
5053 "https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World" ;
5154const SERVICE_AREA_SERVICE : & str = "https://route-api.arcgis.com/arcgis/rest/services/World/ServiceAreas/NAServer/ServiceArea_World" ;
5255const 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]
5559async 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.
250411fn 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
0 commit comments