Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ATTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ which every package ships next to LICENSE. They are generated from `Cargo.lock`
|---|---|---|
| Sample data (`countries`, `countries_chn`, `cities`, `rivers`, `lakes` GeoJSON), embedded at build time from [rustgis-assets](https://github.com/opengeos/rustgis-assets/tree/main/data) | Natural Earth 1:110m, attributes trimmed; `countries_chn` is Natural Earth's China point of view (1:10m, simplified) | Public domain |
| QGIS screenshot (`qgis-world-atlas.webp` in [rustgis-assets](https://github.com/opengeos/rustgis-assets/tree/main/images)), on the [RustGIS and QGIS](docs/comparison/qgis.md) page | Taken by the RustGIS project: [QGIS](https://qgis.org) 4.2 showing the Natural Earth sample data above | QGIS is GPL-2.0-or-later; the data is public domain |
| QGIS test corpus (`crates/map/testdata/qgis/generated/`: styles, projects and their sample data) | Dominik Strebel ([@pick2510](https://github.com/pick2510)) for the RustGIS project: written by [QGIS](https://qgis.org) 4.2.3 through its API, from `generate.py` there; the data is made up | Apache-2.0 |

Raster coordinate systems are looked up by EPSG code in the PROJ definitions of the
[crs-definitions](https://crates.io/crates/crs-definitions) crate (CC0-1.0), compiled into the
Expand Down
2 changes: 2 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ authors:
family-names: Hofbauer
- given-names: Mavrick
family-names: Fitzgerald
- given-names: Dominik
family-names: Strebel
version: 0.4.0
date-released: 2026-10-25
doi: "10.5281/zenodo.23633281"
Expand Down
37 changes: 37 additions & 0 deletions crates/map/src/qgis/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,3 +535,40 @@ fn raster_renderers_read_as_rustgis_raster_styles() {
assert!(s.raster.is_none() && report.notes.iter().any(|n| n.text.contains("couldn't read")), "{bad}: {report:?}");
}
}

/// Every generated style and project in `testdata/qgis` reads, each project with its layers.
#[test]
fn every_corpus_file_reads() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata/qgis");
let files = |dir: &str, extensions: &[&str]| {
let mut paths: Vec<_> = std::fs::read_dir(root.join(dir)).unwrap().map(|e| e.unwrap().path()).collect();
paths.retain(|path| path.is_file() && path.extension().and_then(|e| e.to_str()).is_some_and(|e| extensions.contains(&e)));
paths.sort();
paths
};
// Pin the corpus sizes so a regeneration cannot silently drop fixtures.
let styles = files("generated/qml", &["qml"]);
assert_eq!(styles.len(), 90, "style corpus changed: {styles:?}");
for path in &styles {
let text = std::fs::read_to_string(path).unwrap();
let mut report = Report::default();
read_style(&text, "layer", &mut report).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
}
let projects = files("generated/qgs", &["qgs", "qgz"]);
assert_eq!(projects.len(), 21, "project corpus changed: {projects:?}");
for path in &projects {
let bytes = std::fs::read(path).unwrap();
let text = if path.extension().is_some_and(|e| e == "qgz") {
let entries = rustgis_data::zip::read_matching_within(&bytes, |n| n.ends_with(".qgs"), 64 << 20).unwrap();
String::from_utf8(entries.into_iter().next().unwrap().1).unwrap()
} else {
String::from_utf8(bytes).unwrap()
};
let dir = path.parent().unwrap().to_string_lossy().to_string();
let mut report = Report::default();
let project = read_project(&text, &dir, &mut report).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
assert!(!project.layers.is_empty(), "{}: no layers", path.display());
// The PostGIS and SQL Server projects' made-up password reaches neither the layers nor the notes.
assert!(!format!("{project:?} {report:?}").contains("secret"), "{}: the password came through", path.display());
}
}
51 changes: 51 additions & 0 deletions crates/map/testdata/qgis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# QGIS file corpus

By Dominik Strebel ([@pick2510](https://github.com/pick2510)), for the RustGIS project.

QGIS projects (`.qgs`, `.qgz`) and layer styles (`.qml`) for testing the QGIS importer
(`crates/map/src/qgis/`, the `project.importQgis` and `style.importQgis` commands). QGIS publishes
no specification of these formats, so the corpus stands in for one: `every_corpus_file_reads` in
`src/qgis/tests.rs` reads each file, and the files show what real projects contain.

| Folder | What | Licence |
|---|---|---|
| `generated/qml/` | 90 styles written by QGIS 4.2.3 through its API, each varying one thing: every symbol layer type with its defaults, every renderer (categorized and graduated by expression, rule-based with nested and else rules), every paint effect, data-defined size, angle and colour, labels (every placement, buffer, background, shadow, font, wrap, expression text, offsets, data-defined settings, scale ranges, curved labels, each callout, rule-based labels), layer settings (opacity, blend mode, scale range, reference scale, subset) and raster renderers (grey, pseudocolour interpolated, discrete and exact, multiband, paletted, hillshade, contour) | Apache-2.0, like RustGIS: made by Dominik Strebel ([@pick2510](https://github.com/pick2510)) for the RustGIS project |
| `generated/qgs/` | 21 projects: one per provider (OGR, GDAL, memory, delimited text by x/y and WKT, SpatiaLite, virtual, WMS, XYZ, WFS, WCS, ArcGIS feature and map server, vector tiles, PostGIS, SQL Server, GPX); project structure (groups, a mutually exclusive group, a map theme, a bookmark, a variable, a relation, a join, a virtual field, time from a field, two styles on a layer); a layout with every item and an atlas; the annotation layer with every item; `all-styles.qgz`, every style above on its own layer | Apache-2.0 |
| `generated/data/` | The data those files use: `sample.gpkg` (layers `points`, `lines` and `polygons`, 9 made-up features each, fields `id`, `name`, `kind`, `value`, `angle`, `day`), `dem.tif` (3 bands of a linear surface), two CSVs, a GPX file | Apache-2.0 |
| `generated/generate.py`, `scrub.py` | The generator, and the step that removes the user's name and absolute paths from what QGIS writes | Apache-2.0 |
| `generated/types.txt` | The element types QGIS 4.2.3 knows (symbol layers, renderers, providers, label placements, callouts, annotation and layout items, paint effects): the checklist the corpus was made from. A few have no file yet: the single-colour raster renderer, and the text table, fixed table, elevation profile and chart layout items | Apache-2.0 |

## Not included

- **The SMV25 project and its data.** The repository does not bundle third-party vendor styles.
For a large real-world project to test manually, download the GeoPackage version and its
QGIS project from
[swisstopo](https://www.swisstopo.admin.ch/en/national-map-swiss-map-vector-25) (also on
[opendata.swiss](https://opendata.swiss/en/dataset/swiss-map-vector-25-vektorielle-landeskarte-der-schweiz-1-25-000))
and place the project beside `SMV25_CHLV95LN02.gpkg` and the four rasters in
`SMV25_CHLV95LN02_RASTER/` (about 760 MB) to open the whole map. These external files are
outside the automated corpus test.
- **The QGIS Training Manual's projects and styles**, which were part of the audit: they are
GPL-2.0, so they live in [qgis/QGIS-Training-Data](https://github.com/qgis/QGIS-Training-Data)
(`exercise_data/`, 36 `.qgs`, `.qgz` and `.qml` files from QGIS 1.8 to 3.x).
- **`sample.sqlite`**, the SpatiaLite copy of `sample.gpkg` the SpatiaLite project reads (7 MB,
mostly SpatiaLite's table of coordinate systems): `generate.py` writes it.
- The service and database layers (WFS, WCS, ArcGIS, PostGIS, SQL Server) point at
`example.invalid` or `db.invalid`; QGIS writes them as configured, without data. The PostGIS
and SQL Server sources carry a made-up password (`secret`), to test that the importer drops it.

## Regenerating

QGIS 4.2.3 (here the Flathub Flatpak, whose `/tmp` is private, so it writes under `~/.cache`):

```sh
mkdir -p ~/.cache/qgis-corpus
flatpak run --env=PYTHONPATH=/app/share/qgis/python --env=QT_QPA_PLATFORM=offscreen \
--command=python3 org.qgis.qgis generated/generate.py ~/.cache/qgis-corpus/out
```

It writes `data/`, `qml/`, `qgs/`, `types.txt` and `log.txt` (what was written, and what this
QGIS couldn't write) into the output folder, then runs `scrub.py` on the projects. QGIS may crash
on exit, after writing everything. Copy the files here without `log.txt`, `sample.sqlite`,
`dem.tif.aux.xml`, `all-styles.qgs` (the `.qgz` holds it) and the `_attachments.zip` files QGIS
writes beside each project.
Binary file added crates/map/testdata/qgis/generated/data/dem.tif
Binary file not shown.
3 changes: 3 additions & 0 deletions crates/map/testdata/qgis/generated/data/points-wkt.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name,wkt
A,POINT(2697000 1261800)
B,"LINESTRING(2697000 1261800, 2697100 1261900)"
3 changes: 3 additions & 0 deletions crates/map/testdata/qgis/generated/data/points-xy.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name,x,y
A,2697000,1261800
B,2697100,1261900
Binary file not shown.
1 change: 1 addition & 0 deletions crates/map/testdata/qgis/generated/data/waypoints.gpx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?xml version="1.0"?><gpx version="1.1" creator="corpus" xmlns="http://www.topografix.com/GPX/1/1"><wpt lat="47.5" lon="8.73"><name>A</name></wpt></gpx>
Loading
Loading