Repository navigation
Replies: 11 comments 11 replies
|
This is an important question. Standard GIS behaviour is a project file that links to its data. QGIS and the other desktop GIS work like this, and most users will expect it from RustGIS too. So why not both, but as a project and a package?
The zip with GeoParquet layers proposed here would be a good package format. This means:
The browser needs some care, since it has no file paths. But I think that is a smaller problem than making every project self-contained. |
|
One gap: GeoParquet has no rasters. A package would need them as separate files, Cloud-Optimized GeoTIFFs for example, which can also be read in place from the zip. |
|
Thanks @pick2510, this is a good challenge. We agree on a lot:
It helps to look at where today's mix came from. In v0.1 a project was one file holding everything. Every exception since, from streaming large files to linking them, was a workaround for memory, because storing data in the project meant loading all of it. Traditional desktop GIS separates the project from the package for a similar reason: until recently, keeping data in one file meant reading the whole file. GeoParquet and COG change that. A layer stored in the project can be read in place, a byte range at a time, from disk, from a browser I'd also question whether a small linked project is easier to share. Emailing a bare project works only if the recipient can reach the same data at the same paths. That's how projects broke in the Shapefile era, and why packages exist. A self-contained file is the one you can send without thinking, in the browser too. Shared and authoritative data still stays in its database or on its shared drive, linked as before; only the project's own work lives inside. Git is a fair point. Most GIS users don't keep projects in git, but the scripting and reproducible-analysis users RustGIS aims at do, and a zip doesn't diff or merge. A diff driver only helps locally: GitHub still shows "binary file changed", and merges don't work. The better answer is the folder form already in the design for cloud storage: the same The costs are real, and they're why the prototype comes first. Every save writes the whole file, and editing a large stored layer is less natural than in a GeoPackage. If the prototype can't open large projects with bounded memory and save them at a reasonable cost, your model is the better answer. The prototype is the same work under both models, so I'd start there: the codec, in-place reading including COGs, and measured open and save costs. Those numbers tell us whether the zip is the working file or only the package. |
|
Most of my work is with remote and fairly large data: COGs, GeoTIFFs on network drives, PostGIS and SQL Server. I like the idea of a self-contained project, but in my workflow almost every input stays linked under either model, because nobody copies a 20 GB COG or a production table into a project file. So what I mostly care about is how links, snapshots and handoffs work. I worked through that as a proposal. It sits between the two positions here: @pick2510's split, with @Mavrick-F 's zip as the package. It's a basis for discussion, not a spec. I've marked what I propose and what's still open. SummaryTwo formats that are easy to tell apart: a compact, mostly referencing The key UX point: you choose the format in the normal Save dialog, not only in a separate export step. RustGIS shows a quick estimated size for both options up front, clearly marked as an estimate. Deliberately undecided: where derived results physically live in the Goals and non-goalsGoals:
Not in the first step: real-time collaboration (WebSockets, distributed locks, automatic conflict resolution); a guarantee that every GIS source converts losslessly to a neutral file format; a promise that a RustGIS project opens in other GIS-Programs without conversion. Today versus the proposalThis is a change to what the formats mean, not just a new extension. Both extensions exist already:
One project model for both formatsBoth formats use the same logical project description. A layer has a stable ID, its style, optionally a reference to its original source and, once materialized, a reference to a snapshot. The container must not change what a layer means. Building blocks:
The manifest structure, relative paths, version migration, file naming and transaction boundaries are design questions. This is a model, not a JSON spec. The nine topicsThe proposal in depth, topic by topic1. Formats and architectureProposed: Open: May 2. Sources and snapshotsProposed: Every copy keeps the reference to where it came from. In a package, the snapshot and the original reference sit side by side. Add means reference, Import means copy on purpose. That avoids accidental copies, especially of large files on network drives and database tables. How that fits the existing import paths needs checking. Databases allow subsets, SQL filters or spatial extracts, and those must not be mistaken for a full copy. In the code today, though:
So SQL Server subsets would be new work. For WMS, only rendered images at a given extent and resolution can be copied; that copy isn't equivalent to the service. Offline areas already do part of this. Open: Which sources or layers may be copied by default? What happens when a dataset is too large or can't be reached? What provenance and filter information does a reliable refresh need? 3. Open data formatsPreference: GeoParquet for copied vectors, COG for larger rasters. GeoPackage stays important for import and export, but not as the general package container. GeoParquet allows column reads and skipping row groups you don't need, as long as the metadata, writer and reader support it. A COG allows efficient spatial reads through its internal tiles and HTTP byte ranges. Both only keep those properties if the container, compression and reader allow access by file offset. Converting every dragged-in GeoTIFF to a COG is not decided, and it shouldn't happen silently, especially for large files. "Use the file", "copy it into the package" and "optimize as COG" are three different actions. Open: Exceptions for small editable vector layers, Zarr, point clouds or special raster products? Keeping the CRS and data types? Should GeoParquet become the default for exported results? 4. Saving, packaging and data ownershipProposed UX: The normal Save dialog shows both formats with an estimated size, like the format choice in Office (but more elaborate explanation on benefits/disadvantages):
The numbers are illustrative.
Company data and permissions: a referenced database stays protected by its access control. A copied snapshot is a standalone copy that anyone with the file can read, regardless of those permissions. RustGIS can't judge whether a copy may be shared, legally or organizationally. So: a clear warning and a deliberate action, and never credentials in the project file. The last part is already the case today: saved addresses drop passwords, keys and tokens. Open: How are packages labelled when their WMS or service data is only partly reproducible? Is a "complete package" a hard promise, or a snapshot with documented limits? 5. Refresh and syncProposed: On opening, show the saved state first. Detect changes in the background where that can be done reliably. Refresh only on request, per layer. The status distinguishes four cases: change detected, no change after a reliable check, couldn't check, source unreachable. It shows two timestamps: "snapshot made / refreshed on …" and "source last checked on …". Unchecked doesn't mean out of date. For COGs and objects on S3, ETag, version ID, Last-Modified and size help. For database tables, general PostgreSQL statistics or The interface shows a subtle hint on each layer, plus a central overview with optional "Check all / Refresh all". A successful check doesn't refresh derived layers. Open: Check interval, offline behaviour, authentication on refresh, conflicts with locally edited snapshots, and what "automatic refresh" means for live layers versus derived layers. 6. Processing and derived layersProposed: Results survive reopening. Parameters and input dependencies are recorded too. An outdated result is marked first, not recomputed. Example: a 500 m buffer on a PostGIS layer shows as its own layer and can be used tomorrow on the state it was made from. If the input changes later, it shows "possibly out of date". Only a deliberate re-run makes a new state. That keeps traceability and reproducibility separate from automatic freshness. This doesn't need the full command log in the project. Per derived layer, defined provenance is enough: tool ID, parameters, input references and version indicators. That's different from a chronological session audit trail. Today, tool outputs carry no reloadable source. The closest existing thing is the results package ( Open: Where results live in 7. Remote storage and S3Goal / PoC: Open and read large A reader can fetch a zip package's central directory with an HTTP range request, then read uncompressed members directly. If a COG or GeoParquet inside the zip is DEFLATE-compressed, efficient random access to its contents is typically lost. So store already-compressed assets "stored" in the zip, and test the implementation for real. S3 objects can't be overwritten at arbitrary byte positions. Multipart upload with
For active editing, the comparison is a remote folder with the project JSON and separate COG and GeoParquet objects. A slim Open: A single zip as an editable cloud file versus a folder layout on S3; CORS and HTTP range support, network interruptions, costs, and performance when saving small changes in large packages. 8. Multi-user work and change detectionDeliberately limited: Real concurrent editing is a later step. What's needed early is detecting that an open project file was changed by someone else, and never overwriting that silently. On S3, HEAD, ETag, version ID and conditional writes ( I think we should reconsider persisting the project log in the project file. Today the session log from #947 lives only in memory, and a saved project or package carries none of it. #797 asked for a project log, and #947 shipped it as "part 1", leaving persistence open. Detecting a conflict is only half the job. Once B knows A saved first, B needs to know what A changed, and RustGIS needs a way to combine the two. Because everything in RustGIS is a command, a log of the commands each save added since the shared base version gives both:
What that needs, and why it isn't free:
Later options on top of that: presence, layer locks, event streaming and conflict merge in the interface. Real locks would need extra coordination; S3 alone offers no presence or WebSocket infrastructure. Open: A minimal conflict dialog when two saves compete. Whether the log lives in 9. Interoperability with QGIS and ArcGISGoal, deliberately limited: Open data formats make data exchange easier; full project interoperability is a separate task. A GeoParquet or COG inside the package doesn't mean another GIS can open the RustGIS file as a project with its layer tree, styles and layouts. For QGIS, a The minimum experience: the data in a package is reachable from QGIS without a proprietary extraction step. Ideally, also a project export that lists what it couldn't carry over. Dragging a Open: Is the minimum "data readable" or "project with styles and layout rebuilt"? Which target applications and style subsets should be supported explicitly? Example workflowsA–D: four workflows to decide againstA. A large company project with PostGIS and a network drive. A project references a PostGIS table and several GeoTIFFs on a shared drive. On Save, B. An offline package with a way back to the source. A colleague receives a C. A single large package in object storage. A 20 GB package sits in an S3 bucket. Opening it reads only the zip index and the COG tiles or Parquet row groups needed. After a style change, we measure whether a server-side byte-range copy plus a minimal upload gives much less client traffic and a short enough save. The folder approach, with the project definition stored separately, is tested alongside. D. Two open copies of the same remote project. Users A and B open the same version. A saves first. B sees at the next poll, or at the latest before saving, that the remote version changed. B then chooses to reload, save as a copy, or merge. With a persisted project log, B sees which commands A's save added, and B's own commands can be replayed on top of A's version. Phases
PoC checksWhat the cloud PoC should measureThe PoC should show not just that a remote package opens, but whether it's practical under realistic loads and small changes.
Targets like "save in under X seconds" aren't set yet. The PoC should first collect a baseline and limits; only then can we set a firm requirement. RisksRisks and possible mitigations
Open decisions
And four from comparing this with the thread and the code:
In short: a slim referencing |
|
Thanks @deniial00, this is an extremely useful response, and checking it against the code caught real problems. Several parts I'd adopt as they stand: snapshots that keep their source reference, the four refresh states with their two timestamps, provenance on derived layers ("possibly out of date" instead of recomputing silently), and the warning that a copy of database data can be read without the database's permissions. I'd still make the self-contained file the default, though. Your proposal made me sharpen what that means:
That also means Here's why I lean this way. It's more ambitious than a referencing default, but it fits what RustGIS already is. v0.1 saved everything in the project, and RustGIS still copies small files in today. People rely on that, whether they realise it or not. A referencing default would take it away from the users least able to notice: a student who adds a shapefile from Downloads and emails the project to an instructor gets a broken layer, and the fix ("choose The costs, stated plainly:
On 11, bounding boxes: you were right. I was actually about to post some test results including that. Dropping the per-row bbox column loses row-group skipping in today's reader: a zoomed-in statewide view read 376 MiB instead of 149 MiB. Keeping the column costs 1.5–2× the size on layers with many small features. The replacement is Parquet's native GEOMETRY statistics, one bounding box per row group. A probe showed the Sizes for the same data (MB, GeoParquet as above, including feature IDs and order)
So roughly a zipped file geodatabase, while staying readable in place. Large polygons need a byte limit per row group as well as a row limit: the counties fit in one group, so nothing is skipped. On today's A few smaller points:
|
|
I’d like to add a user’s perspective, as this is close to how I already work. Implementation details aside, conceptually I'm strongly in favour of keeping both capabilities: a self-contained project that can be sent as one file, and a project that points to data that stays where it is. For small-to-medium projects (up to 1–2 GB of source data in QGIS), the one-file zstd archive is what I have used in practice for a while, as thanks to it there is no folder of sidecars to keep together, and paths don’t break when the project moves to another machine. I’ve implemented this export workflow in a QGIS plugin. One distinction I’d stress, because I think it changes the design: “self-contained” should not mean one zstd-compressed JSON single compressed blob. The model for archive I use compresses each entry separately and keeps a manifest and seek table, so individual entries can be read without unpacking the whole archive. That maps onto the project-versus-package split raised above. A single compressed blob would keep exactly the open-time and memory costs the proposal measures. Storing layers as separate, in-place-readable members (Parquet/COG) also leaves room for indexes over the data itself, spatial or attribute indexes per layer, written at save time, instead of only an index of the archive’s contents. It is what I would not want to lose by choosing plain zstd. Per-entry zstd is fine for the package; the working file needs the layers readable in place, otherwise indexes cannot be used without unpacking On desktop, the number I’d care about most in the prototype is time and peak memory to open a 1–2 GB project, and time to save it again. I wouldn’t hold the browser to the same standard: 1–2 GB is beyond what wasm and browser storage handle comfortably. I’d accept a lower, explicit ceiling there, as long as the new format opens and saves correctly from day one. |
|
I've been thinking about another improvement that could complement either of the proposed approaches to project storage. Currently, the structure of .rgis files is primarily defined by the Rust types and their Serde serialization. While we already have a project format version, as far as I can tell there isn't a standalone, machine-readable JSON Schema describing the complete serialized project structure. I think it would make sense to introduce a versioned JSON Schema, maintained directly in the repository, similar in spirit to GeoLibre's approach to its deployment configuration. This could provide several benefits:
Ideally, the schema could be generated from the Rust types (for example, using schemars), or at least automatically checked against them. A CI check for schema drift would be more reliable than relying on documentation alone. I'd also suggest documenting a simple rule in AGENTS.md and docs/maintaining.md: Whenever a PR changes the serialized project structure, it must update the corresponding schema and relevant test fixtures. This would complement the compatibility guidelines proposed in PR #1481. Importantly, updating the schema would not automatically require bumping the project format version: the version should change according to the actual compatibility implications of the change. I see this as largely independent of the .rgis versus .rgispkg decision. Both could share the same versioned project metadata schema, while the package/container structure and embedded-file rules would be specified separately. The goal would be to make the RustGIS project format an explicit, version-controlled and testable contract, rather than something defined only implicitly by the implementation. |
|
A user's view from academic work in Brazil: soil and land-cover mapping, teaching, and figures for papers and theses. Almost all my data comes from public web sources: IBGE, MapBiomas COGs, INPE and JAXA, through plugins like BrasilData. Whichever default wins, the converging position here works for me: 1. Licence and credit travel with every copy.
2. A manifest that serves as a data citation.
3. A window of a remote raster as a snapshot.
4. Layers made by plugins have provenance too.
None of this changes the container question. Points 1 and 2 are fields in the source and snapshot descriptors, and the JSON Schema. They cost little now and would be hard to add once projects exist without them. |
|
The thread has become long, so here is an overview of the variants proposed so far. Please correct me if I got your position wrong.
Additions that fit any variant:
What is still open is the default: a linking One update on the limit: since #1639, the project size RustGIS opens scales with the machine's memory. The 646 MiB statewide project from the first post now opens on a 16 GB machine. That takes some pressure off, but it doesn't change the memory needed to open a large project. |
|
I said I'd prototype whether a large self-contained project can save a small change quickly and safely on slow storage. Short answer: it can. How it works. The project is a zip with one GeoParquet file per layer. A save doesn't rewrite the whole file. It adds only what changed to the end of it, then a new table of contents. Until that table of contents is safely written, the previous save is still there, untouched. The space taken up by old versions is reclaimed now and then by a full rewrite, for example when saving a copy. How fast. I tested a 583 MB statewide project (3.2 million buildings plus roads, blocks and counties) on a cheap USB stick:
For comparison, the same edits in a GeoPackage on the same stick took between 4 seconds and nearly 4 minutes. On an SSD, both approaches were fast. How safe. I interrupted saves at every step and simulated 3,000 power cuts. Every time, the file reopened as either the previous save or the new one, never a broken mix. This was a throwaway prototype outside RustGIS, so the next step would be building it properly. The same approach carries over to network drives and to the browser, where projects would be kept in the browser's own storage and saved to disk on request. So save speed isn't a reason to make |
Uh oh!
There was an error while loading. Please reload this page.
When you save a project in RustGIS, the
.rgisfile stores your maps, styles and layouts, and it also stores most of your layers' data. That keeps projects easy to send, but on real data the files get large, slow to open and memory-hungry. Some grow past the size RustGIS can open, so they can't be saved at all.I'd like to fix this before 1.0. After 1.0, people will rightly expect saved projects to keep working, so the format should be settled first. This post sets out the problem with measurements, the choice it raises (should a project hold its data, or only point to it?), the design I'd prototype, and what else was considered.
The problem
A
.rgisis one uncompressed JSON document. Every layer loaded whole is written out coordinate by coordinate. I measured realistic projects built from public data (TIGER/Line, Microsoft buildings, NHD, GNIS) with the v0.5.0 CLI:.rgistodayA single step can cross the limit. A 30 m buffer of Shelby County's 76,666 TIGER edges made a 592 MiB project. Opening a project takes 2.4–3.9× its size in RAM, and autosave rewrites the whole file every five minutes. For scale, the Windows installer is 34 MiB.
Save used to write these files and report success, then fail to reopen them. #1364 makes Save refuse instead, which avoids a broken file but still leaves the work unsaved.
Fixes that don't need a decision
The real question: what is a project?
In v0.1.0 a project was one file that held everything. A few hours later, #71 (for #66) streamed large GeoPackage, GeoParquet and FlatGeobuf files so they didn't have to fit in memory. Since then each format has added its own exception, so today whether a layer lives in the project depends on its format and size. Users can't predict it, and that's how the projects above outgrew the limit.
There are two coherent alternatives:
Either model changes what
layer.addDatadoes today, so the choice needs its own decision. Under either model, a team sharing data needs:If your team works with shared data, what else would it need?
The two models in more detail, and a team sharing data under each
The project points to data.
Self-contained by default.
A team sharing data. The canonical data belongs in a database: PostGIS, MySQL or SQL Server. RustGIS already edits PostGIS with pending edits, transactions and row-version checks, so two people can't silently overwrite each other. Read-only reference data can sit on a shared drive. Edited GeoPackages or Shapefiles on a network share are fragile with more than one writer under either model.
If we choose self-contained: the prototype
A
.rgiswould become a zip holding a smallproject.json(maps, styles, layouts, the layer list) and one standard GeoParquet file per stored layer. Layers are stored in the zip uncompressed (Parquet compresses internally), so any layer can be read in place as a byte range from disk, from a browserBlobor over HTTP. RustGIS's GeoParquet reader already works on any byte source.Tuned GeoParquet (zstd, no bounding-box columns) is about the size of compressed JSON. I checked it losslessly, bit for bit:
The gain isn't size. It's that a project wouldn't need to be loaded whole to open, so a 600 MiB project wouldn't need gigabytes of RAM, and saves wouldn't re-encode every layer.
GeoPackage-as-project (see Options considered) would be measured alongside, on the same tests. The prototype has to show:
If we switched:
Prototype details: saving, untrusted files, size layouts, effort
Saving. Write a temp file beside the project, copy unchanged layers byte for byte and verify them against the manifest's hashes, then write the changed layers and
project.json. Flush and replace atomically:ReplaceFileWon Windows,createWritablein the browser. Appending to the end of a zip can't be made crash-safe. Readers keep the old file open and rebind after a successful replace. If the file changed on disk since it was opened, Save asks rather than overwriting.Untrusted files. A strict zip profile:
A smaller, RustGIS-specific layout later. Separate x/y columns, as scaled integers when one decimal precision fits nearly every coordinate, or ALP floats otherwise. That measured 0.44× (buildings), 0.63× (points) and 0.93× (counties) of compressed JSON. The
parquetcrate writes it without Arrow, so it works in the browser. Two catches: other tools couldn't read those layers directly, and ALP is still a preview encoding in the Parquet spec. Because layer encodings are declared in each file's list of required capabilities, this can be added later without breaking anything.Effort. Roughly 2,500–4,000 lines across the codec, the in-zip reader, the writer and save, reopening in place, and browser I/O. The browser part is the riskiest.
Groundwork for shared and cloud work
We'll want what established GIS platforms offer: projects opened from cloud storage, offline edits that sync back, and eventually several people editing at once. None of that is proposed here, but a few choices are cheap now and painful to retrofit:
These serve all three. The detailed sync design (change records, conflicts, permissions, partial copies) is a separate proposal for when we build one of them. One known bug is worth fixing sooner on its own: offline areas currently overwrite features edited both offline and at the source, without saying so.
Options considered
Keep JSON and compress it: fixes size, not memory
zstd makes projects 3.5–7.4× smaller, and every measured project fits under the limit compressed. But opening still parses everything: the statewide project would need about 2 GB of RAM. Every save still rewrites everything. It's a stopgap at best, and moving to it would mean a second format change soon after.
Reshape the JSON, or a binary serde format: little gain
Compact coordinates, MessagePack, CBOR or WKB in JSON gain at most 11% once compressed. postcard, bitcode and rkyv break old files whenever a field is added. bincode is unmaintained.
The project as a GeoPackage: the strongest alternative
QGIS can store a project inside a GeoPackage, and SQLite makes a strong case as an application file format. You get:
It loses for now on four counts:
If editing very large layers in place becomes the priority, it should win. I'd measure it alongside the zip.
A project file plus a managed data file or folder: halfway between the two models
Saves become proportional to what changed, but it gives up the single file and still needs a package to send. Users would manage two forms of every project.
FlatGeobuf, Arrow IPC, Lance, or a custom format
More thresholds and automatic linking: ruled out
Deciding where a layer lives by count, size or format is what made today's behaviour unpredictable. More rules make it worse.
Questions
I'd like to hear from @opengeos/rustgis-maintainers and other contributors before I build anything.
All reactions