use anyhow::Result;
use arcgis::example_tracker::ExampleTracker;
use arcgis::{
ArcGISClient, ExportTarget, GenerateKmlParams, GenerateRendererParams, GeometryType,
IdentifyParams, ImageFormat, LayerSelection, MapServiceClient, NoAuth, TileCoordinate,
};
const USA_MAP_SERVER: &str =
"https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer";
const WORLD_STREET_MAP: &str =
"https://sampleserver6.arcgisonline.com/arcgis/rest/services/World_Street_Map/MapServer";
#[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("map_service_basics")
.service_type("ExampleClient")
.start();
tracing::info!("πΊοΈ Map Service Examples");
tracing::info!("Using ESRI's public USA MapServer service");
let client = ArcGISClient::new(NoAuth);
let map_service = MapServiceClient::new(USA_MAP_SERVER, &client);
demonstrate_basic_map_export(&map_service).await?;
demonstrate_transparent_export(&map_service).await?;
demonstrate_high_dpi_export(&map_service).await?;
demonstrate_identify_features(&map_service).await?;
demonstrate_find_by_text(&map_service).await?;
demonstrate_legend_retrieval(&map_service).await?;
demonstrate_metadata_retrieval(&map_service).await?;
let cached_service = MapServiceClient::new(WORLD_STREET_MAP, &client);
demonstrate_tile_export(&cached_service).await?;
demonstrate_query_domains(&map_service).await?;
demonstrate_generate_kml(&map_service).await?;
demonstrate_generate_renderer(&map_service).await?;
tracing::info!("\nβ
All map service examples completed successfully!");
print_best_practices();
tracker.success();
Ok(())
}
async fn demonstrate_basic_map_export(service: &MapServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 1: Basic Map Export ===");
tracing::info!("Export map of San Francisco Bay Area");
let sf_extent = "-122.5,37.6,-122.3,37.9";
let result = service
.export()
.bbox(sf_extent)
.size(800, 600)
.format(ImageFormat::Png)
.execute(ExportTarget::to_path("map_basic.png"))
.await?;
assert!(
result.path().is_some(),
"Export should create a file and return path"
);
if let Some(path) = result.path() {
assert!(
path.exists(),
"Exported file should exist at {}",
path.display()
);
let metadata = std::fs::metadata(path)?;
assert!(metadata.len() > 0, "Exported file should not be empty");
tracing::info!(
path = %path.display(),
size_bytes = metadata.len(),
"β
Map exported successfully"
);
tracing::info!(" Extent: San Francisco Bay Area");
tracing::info!(" Size: 800x600 pixels");
tracing::info!(" Format: PNG");
}
Ok(())
}
async fn demonstrate_transparent_export(service: &MapServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 2: Transparent Background Export ===");
tracing::info!("Export map with transparency for overlay use");
let la_extent = "-118.3,34.0,-118.2,34.1";
let result = service
.export()
.bbox(la_extent)
.size(1024, 768)
.format(ImageFormat::Png32) .transparent(true) .execute(ExportTarget::to_path("map_transparent.png"))
.await?;
assert!(
result.path().is_some(),
"Transparent export should create a file and return path"
);
if let Some(path) = result.path() {
assert!(path.exists(), "Transparent map file should exist");
let metadata = std::fs::metadata(path)?;
assert!(metadata.len() > 0, "Transparent map should not be empty");
tracing::info!(
path = %path.display(),
size_bytes = metadata.len(),
"β
Transparent map exported"
);
tracing::info!(" Background: Transparent (alpha channel)");
tracing::info!(" Use case: Perfect for overlaying on other imagery");
}
Ok(())
}
async fn demonstrate_high_dpi_export(service: &MapServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 3: High DPI Export ===");
tracing::info!("Export high-resolution map for print");
let seattle_extent = "-122.35,47.58,-122.30,47.63";
let result = service
.export()
.bbox(seattle_extent)
.size(1600, 1200) .format(ImageFormat::Png32)
.dpi(300) .execute(ExportTarget::to_path("map_high_dpi.png"))
.await?;
assert!(
result.path().is_some(),
"High-DPI export should create a file and return path"
);
if let Some(path) = result.path() {
assert!(path.exists(), "High-DPI map file should exist");
let metadata = std::fs::metadata(path)?;
assert!(metadata.len() > 0, "High-DPI map should not be empty");
assert!(
metadata.len() > 10000,
"High-DPI map should be substantial, got {} bytes",
metadata.len()
);
tracing::info!(
path = %path.display(),
size_bytes = metadata.len(),
"β
High-DPI map exported"
);
tracing::info!(" Resolution: 300 DPI (print quality)");
tracing::info!(" Size: 1600x1200 pixels");
tracing::info!(" Use case: Professional printing, large displays");
}
Ok(())
}
async fn demonstrate_identify_features(service: &MapServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 4: Identify Features at Point ===");
tracing::info!("Identify features at clicked location (Los Angeles area)");
let point_x = -118.25;
let point_y = 34.05;
let geometry = format!("{{\"x\":{},\"y\":{}}}", point_x, point_y);
let map_extent = "-120.0,32.0,-116.0,36.0";
let image_display = "800,600,96";
let params = IdentifyParams::builder()
.geometry(geometry)
.geometry_type(GeometryType::Point)
.map_extent(map_extent.to_string())
.image_display(image_display.to_string())
.layers(LayerSelection::Visible) .tolerance(5) .return_geometry(true)
.build()
.expect("Valid identify params");
let response = service.identify(params).await?;
tracing::info!(
result_count = response.results().len(),
"β
Identify completed"
);
if response.results().is_empty() {
tracing::info!(" No features found at this location");
tracing::info!(" Try different coordinates or increase tolerance");
} else {
for result in response.results().iter() {
assert!(
!result.layer_name().is_empty(),
"Identified feature should have layer name"
);
assert!(
!result.attributes().is_empty(),
"Identified feature should have attributes"
);
}
tracing::info!("π Features identified:");
for (i, result) in response.results().iter().take(5).enumerate() {
tracing::info!(
" {}. Layer {}: {}",
i + 1,
result.layer_id(),
result.layer_name()
);
if !result.attributes().is_empty() {
let attrs: Vec<String> = result
.attributes()
.iter()
.take(3)
.map(|(k, v)| format!("{}: {:?}", k, v))
.collect();
tracing::info!(" Attributes: {}", attrs.join(", "));
}
}
}
Ok(())
}
async fn demonstrate_find_by_text(service: &MapServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 5: Find Features by Text ===");
tracing::info!("Search for cities containing 'Los' in their name");
let params = arcgis::FindParams::builder()
.search_text("Los")
.layers(vec![0]) .search_fields(vec!["AREANAME".to_string()]) .sr(4326) .contains(true) .return_geometry(false) .build()
.expect("Valid find params");
let response = service.find(params).await?;
assert!(
!response.results().is_empty(),
"Should find cities containing 'Los' (e.g., Los Angeles)"
);
assert!(
!response.results().is_empty(),
"Find should return at least one result"
);
for result in response.results().iter() {
assert!(
!result.layer_name().is_empty(),
"Found feature should have layer name"
);
assert!(
!result.found_field_name().is_empty(),
"Found feature should have field name"
);
assert!(
!result.value().is_null(),
"Found feature should have non-null value"
);
}
tracing::info!(result_count = response.results().len(), "β
Find completed");
if response.results().is_empty() {
tracing::info!(" No cities found containing 'Los'");
} else {
tracing::info!("π Cities found:");
for (i, result) in response.results().iter().take(5).enumerate() {
tracing::info!(
" {}. Layer {}: {}",
i + 1,
result.layer_id(),
result.layer_name()
);
if !result.found_field_name().is_empty() {
tracing::info!(
" Match in field '{}': {}",
result.found_field_name(),
result.value()
);
}
}
if response.results().len() > 5 {
tracing::info!(" ... and {} more results", response.results().len() - 5);
}
}
Ok(())
}
async fn demonstrate_legend_retrieval(service: &MapServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 6: Legend Retrieval ===");
tracing::info!("Get legend symbols and labels for all layers");
let legend = service.get_legend().await?;
assert!(
!legend.layers().is_empty(),
"Legend should have at least one layer"
);
assert!(
legend.layers().len() >= 3,
"USA MapServer should have at least 3 layers, got {}",
legend.layers().len()
);
for layer in legend.layers().iter() {
assert!(
!layer.layer_name().is_empty(),
"Legend layer should have a name"
);
}
tracing::info!(layer_count = legend.layers().len(), "β
Legend retrieved");
tracing::info!("π¨ Legend information:");
for layer in legend.layers().iter().take(5) {
tracing::info!(" Layer {}: {}", layer.layer_id(), layer.layer_name());
if !layer.legend().is_empty() {
tracing::info!(" Symbols:");
for (i, symbol) in layer.legend().iter().take(3).enumerate() {
tracing::info!(" {}. {}", i + 1, symbol.label());
if let Some(url) = symbol.url() {
tracing::debug!(" Icon URL: {}", url);
}
}
if layer.legend().len() > 3 {
tracing::info!(" ... and {} more symbols", layer.legend().len() - 3);
}
}
}
if legend.layers().len() > 5 {
tracing::info!(" ... and {} more layers", legend.layers().len() - 5);
}
Ok(())
}
async fn demonstrate_metadata_retrieval(service: &MapServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 7: Service Metadata Retrieval ===");
tracing::info!("Get comprehensive service information and capabilities");
let metadata = service.get_metadata().await?;
assert!(
!metadata.layers().is_empty(),
"Metadata should include layer information"
);
assert!(
metadata.spatial_reference().is_some(),
"Metadata should include spatial reference"
);
tracing::info!(
layer_count = metadata.layers().len(),
"β
Metadata retrieved"
);
tracing::info!("π Service Information:");
if let Some(desc) = metadata.description() {
let preview = desc.chars().take(100).collect::<String>();
tracing::info!(" Description: {}...", preview);
}
if let Some(sr) = metadata.spatial_reference() {
if let Some(wkid) = sr.wkid() {
tracing::info!(" Spatial Reference: WKID {}", wkid);
}
}
if let Some(extent) = metadata.full_extent() {
tracing::debug!(" Extent: {:?}", extent);
}
tracing::info!(" Layers:");
for layer in metadata.layers().iter().take(5) {
tracing::info!(" - Layer {}: {}", layer.id(), layer.name());
}
if let Some(capabilities) = metadata.capabilities() {
tracing::info!(" Capabilities: {}", capabilities);
}
Ok(())
}
async fn demonstrate_tile_export(service: &MapServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 8: Cached Tile Export ===");
tracing::info!("Export pre-generated tiles from cached service");
let tile_coord = TileCoordinate::new(5, 12, 5);
tracing::info!(" Tile coordinates:");
tracing::info!(" Zoom level: {}", tile_coord.level());
tracing::info!(" Row: {}", tile_coord.row());
tracing::info!(" Column: {}", tile_coord.col());
let result = service
.export_tile(tile_coord, ExportTarget::to_path("tile_cached.jpg"))
.await?;
assert!(
result.path().is_some(),
"Tile export should create a file and return path"
);
if let Some(path) = result.path() {
assert!(
path.exists(),
"Exported tile should exist at {}",
path.display()
);
let metadata = std::fs::metadata(path)?;
assert!(metadata.len() > 0, "Exported tile should not be empty");
tracing::info!(
path = %path.display(),
size_bytes = metadata.len(),
"β
Cached tile exported"
);
tracing::info!(" Use case: Offline mapping, custom tile caches");
}
Ok(())
}
async fn demonstrate_query_domains(service: &MapServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 9: Query Domains ===");
tracing::info!("Retrieve domain and subtype information for layers");
let domains_result = service.query_domains(vec![]).await;
match domains_result {
Ok(domains) => {
tracing::info!(
layer_count = domains.layers().len(),
"β
Query domains completed"
);
if domains.layers().is_empty() {
tracing::info!(" No domain information available for this service");
tracing::info!(" (Many services don't use coded value domains)");
} else {
tracing::info!("π Domain information:");
for layer_domain in domains.layers().iter().take(3) {
if let Some(name) = layer_domain.name() {
tracing::info!(" Layer: {}", name);
}
if !layer_domain.domains().is_empty() {
tracing::info!(" {} domain(s) defined", layer_domain.domains().len());
}
}
}
}
Err(e) => {
tracing::warn!(
error = %e,
"Query domains not supported by this service"
);
tracing::info!(" This is expected - not all services support this operation");
}
}
Ok(())
}
async fn demonstrate_generate_kml(service: &MapServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 10: Generate KML ===");
tracing::info!("Export map service as KML for Google Earth");
let params = GenerateKmlParams::builder()
.doc_name("USA_Map")
.layers(vec![0, 2]) .build()
.expect("Valid KML params");
let kml_result = service.generate_kml(params).await;
match kml_result {
Ok(kml) => {
assert!(!kml.is_empty(), "Generated KML should not be empty");
assert!(
kml.len() > 100,
"KML should have substantial content, got {} bytes",
kml.len()
);
tracing::info!(kml_length = kml.len(), "β
KML generated successfully");
tracing::info!(" Use case: Integration with Google Earth, KML viewers");
tracing::info!(
" KML preview: {}...",
&kml.chars().take(100).collect::<String>()
);
}
Err(e) => {
tracing::warn!(
error = %e,
"KML generation not supported by this service"
);
tracing::info!(" This is expected - not all services support KML export");
tracing::info!(" Services need KmlServer extension enabled");
}
}
Ok(())
}
async fn demonstrate_generate_renderer(service: &MapServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 11: Generate Renderer ===");
tracing::info!("Create dynamic classification renderer for data visualization");
let params = GenerateRendererParams::builder()
.classification_field("POP2000") .classification_method("natural-breaks")
.break_count(5)
.build()
.expect("Valid renderer params");
let renderer_result = service.generate_renderer(2, params).await;
match renderer_result {
Ok(renderer) => {
assert!(
!renderer.renderer_type().is_empty(),
"Generated renderer should have renderer type"
);
tracing::info!("β
Renderer generated successfully");
tracing::info!(" Renderer type: {}", renderer.renderer_type());
tracing::info!(" Use case: Dynamic choropleth maps, data visualization");
if let Some(field) = renderer.field() {
tracing::info!(" Classification field: {}", field);
}
if let Some(breaks) = renderer.class_break_infos() {
tracing::info!(" Class breaks: {} classes", breaks.len());
}
}
Err(e) => {
tracing::warn!(
error = %e,
"Renderer generation not supported by this service"
);
tracing::info!(" This is expected - not all services support dynamic renderers");
tracing::info!(" Requires specific service configuration and field availability");
}
}
Ok(())
}
fn print_best_practices() {
tracing::info!("\nπ‘ Map Service Best Practices:");
tracing::info!(" - Use appropriate image formats (PNG for transparency, JPG for photos)");
tracing::info!(" - Request only the extent you need to minimize data transfer");
tracing::info!(" - Use DPI setting (96 for screen, 300 for print)");
tracing::info!(" - Cache exported maps when extent/params don't change");
tracing::info!(" - Set appropriate tolerance for identify operations");
tracing::info!("");
tracing::info!("π― Format Selection:");
tracing::info!(" - PNG32: Transparency support, best for overlays");
tracing::info!(" - PNG24: No transparency, smaller than PNG32");
tracing::info!(" - JPG: Smallest file size, no transparency, best for photos");
tracing::info!(" - PDF/SVG: Vector formats for scalable graphics");
tracing::info!("");
tracing::info!("β‘ Performance Tips:");
tracing::info!(" - Stream large exports to files instead of loading into memory");
tracing::info!(" - Use cached tile services when available (export_tile)");
tracing::info!(" - Limit identify tolerance to reduce processing time");
tracing::info!(" - Use contains=true for partial text matching in find operations");
tracing::info!("");
tracing::info!("π Extent Format:");
tracing::info!(" - Bbox format: 'xmin,ymin,xmax,ymax' (west,south,east,north)");
tracing::info!(" - Use WGS84 (EPSG:4326) for lat/lon coordinates");
tracing::info!(" - Use Web Mercator (EPSG:3857) for web mapping");
tracing::info!(" - Match service spatial reference for best results");
}