API Design
API Design
qgis-rs follows a set of design principles to provide an idiomatic, safe, and ergonomic Rust API.
Core Principles
1. Builder Pattern for Configuration
Complex configuration objects use the builder pattern:
let settings = RenderSettings::new(1920, 1080) .extent(Extent::new(14.0, 50.0, 15.0, 51.0)) .crs(Crs::from_epsg(3857)?) .dpi(150) .background(Color::WHITE) .layers(&["buildings", "roads"]) .antialiasing(true);Benefits:
- Clear, readable configuration
- Method chaining
- Optional parameters with sensible defaults
- Type-safe at compile time
2. Lazy Iteration
Feature iteration is lazy — data is fetched on-demand:
// No features fetched yetlet features = layer.features();
// Fetches features one at a time as we iteratefor feature in features { println!("{}", feature.get("name")?);
// Stop early — remaining features never fetched if some_condition { break; }}Benefits:
- Memory efficient (no
Vec<Feature>allocation) - Works with large datasets (millions of features)
- Early termination saves time
3. Result Everywhere
All fallible operations return Result<T, QgisError>:
match Project::open("map.qgs") { Ok(project) => println!("Loaded: {}", project.title()), Err(QgisError::FileNotFound(path)) => eprintln!("File not found: {}", path), Err(QgisError::InvalidProject { message }) => eprintln!("Invalid project: {}", message), Err(e) => eprintln!("Error: {}", e),}Benefits:
- Explicit error handling
- No panics in library code
- Exhaustive error types for pattern matching
4. Sensible Defaults
Methods have reasonable defaults so simple cases are one-liners:
// Minimal configurationlet image = project.render_to_image(RenderSettings::new(1024, 768))?;image.save("output.png")?;
// Defaults used:// - Extent: project.extent()// - CRS: project.crs()// - DPI: 96// - Background: transparent// - Layers: all visible layers5. Consistent Naming
Verbs and patterns are consistent across the API:
| Pattern | Example |
|---|---|
open() | Project::open("map.qgs") |
render() | project.render(&settings) |
save() | image.save("out.png") |
to_*() | geometry.to_wkt(), image.to_png_bytes() |
from_*() | Crs::from_epsg(4326), Geometry::from_wkt(...) |
as_*() | layer.as_vector(), feature.as_json() |
Type System
Core Types
// Projects and layerspub struct Project { /* ... */ }pub struct Layer { /* ... */ }pub struct VectorLayer { /* ... */ }pub struct RasterLayer { /* ... */ }
// Data accesspub struct Feature { /* ... */ }pub struct Field { /* ... */ }pub struct Geometry { /* ... */ }
// Configurationpub struct RenderSettings { /* builder */ }pub struct MapSettings { /* builder */ }pub struct FeatureRequest { /* builder */ }
// Spatial typespub struct Extent { pub min_x: f64, pub min_y: f64, pub max_x: f64, pub max_y: f64 }pub struct Crs { /* opaque */ }pub struct CoordTransform { /* opaque */ }
// Outputpub struct Image { /* pixel buffer */ }pub struct TileCoord { pub z: u8, pub x: u32, pub y: u32 }Enums
pub enum LayerType { Vector, Raster, Mesh, VectorTile, PointCloud,}
pub enum GeometryType { Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection,}
pub enum BlendMode { Normal, Multiply, Screen, Overlay, // ... 12 blend modes total}Error Types
pub enum QgisError { FileNotFound(PathBuf), InvalidProject { message: String }, BadLayer { name: String, uri: String }, UnsupportedFormat(String), RenderError(String), ExpressionError { expr: String, message: String }, CrsError(String), IoError(std::io::Error), // ...}
impl std::error::Error for QgisError {}impl std::fmt::Display for QgisError { /* ... */ }Ownership and Lifetimes
Owned vs Borrowed
// Owned — you control the lifetimelet project = Project::open("map.qgs")?;let geometry = Geometry::from_wkt("POINT(0 0)")?;
// Borrowed — lifetime tied to parentlet layer: &Layer = project.layer("buildings")?;let field: &Field = &layer.fields()[0];
// Transferred — ownership moves to youlet layer = project.take_layer("buildings")?; // Project no longer owns itThread Safety
QGIS objects are !Send + !Sync (not thread-safe):
// ✗ Compile errorstd::thread::spawn(move || { project.render(...); // ERROR: Project is !Send});
// ✓ Use a render pool with thread affinitylet pool = RenderPool::new(4);pool.render(request).await;Advanced Patterns
Filtering and Selection
let request = FeatureRequest::new() .filter(Expression::gt("population", 1_000_000)) .bounding_box(Extent::new(14.0, 50.0, 15.0, 51.0)) .select(["name", "population"]) // Only fetch these columns .limit(1000) .no_geometry(); // Skip geometry if not needed
for feature in layer.features_with(request) { // Fast: only fetched name and population println!("{}: {}", feature.get("name")?, feature.get("population")?);}Spatial Queries
let polygon = Geometry::from_wkt("POLYGON((...))")?;
let request = FeatureRequest::new() .intersects(&polygon) .select_all();
let count = layer.count_with(request)?;println!("{} features intersect the polygon", count);Coordinate Transforms
let wgs84 = Crs::from_epsg(4326)?;let mercator = Crs::from_epsg(3857)?;
let transform = CoordTransform::new(&wgs84, &mercator)?;
// Single pointlet (x, y) = transform.transform(14.0, 50.0)?;
// Batch transform (more efficient)let points = [(14.0, 50.0), (15.0, 51.0), (16.0, 52.0)];let transformed = transform.transform_batch(&points)?;Next Steps
- QGIS Integration — How qgis-rs maps to QGIS concepts
- API Reference — Complete API documentation
- Rendering Projects — Practical examples