env-able 1.2.0__tar.gz → 1.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {env_able-1.2.0 → env_able-1.2.2}/.gitignore +68 -60
- {env_able-1.2.0 → env_able-1.2.2}/CHANGELOG.md +77 -0
- {env_able-1.2.0 → env_able-1.2.2}/PKG-INFO +129 -6
- {env_able-1.2.0 → env_able-1.2.2}/README.md +988 -865
- {env_able-1.2.0 → env_able-1.2.2}/pyproject.toml +1 -1
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/__init__.py +1 -1
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/desktop.py +152 -132
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/index.html +12700 -12300
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/landing.html +947 -943
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/server.py +4857 -4715
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/vector.py +226 -72
- {env_able-1.2.0 → env_able-1.2.2}/LICENSE +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/_gdal_env.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/_gdal_utils.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/_pipeline.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/_result.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/__init__.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/client.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/compliance.html +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/databricks.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/debug.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/edits.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/project.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/splash.html +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/templates/Summary A4 Landscape.pagx +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/templates/Summary A4 Portrait.pagx +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/templates/Title Bar A4 Landscape.pagx +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/atlas/templates/Title Bar A4 Portrait.pagx +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/layout.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/morph.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/pull.py +0 -0
- {env_able-1.2.0 → env_able-1.2.2}/src/env_able/stream.py +0 -0
|
@@ -1,60 +1,68 @@
|
|
|
1
|
-
# ── Secrets — never commit ────────────────────────────────────────────────────
|
|
2
|
-
# Live PyPI API token used by dev/publish-*.bat
|
|
3
|
-
**/dev/token.txt
|
|
4
|
-
**/token.txt
|
|
5
|
-
*.pem
|
|
6
|
-
*.key
|
|
7
|
-
.env
|
|
8
|
-
.env.*
|
|
9
|
-
|
|
10
|
-
# ── Virtual environments ──────────────────────────────────────────────────────
|
|
11
|
-
.venv/
|
|
12
|
-
venv/
|
|
13
|
-
env/
|
|
14
|
-
**/.venv/
|
|
15
|
-
|
|
16
|
-
# ── Python build / cache ──────────────────────────────────────────────────────
|
|
17
|
-
__pycache__/
|
|
18
|
-
*.py[cod]
|
|
19
|
-
*.egg-info/
|
|
20
|
-
build/
|
|
21
|
-
dist/
|
|
22
|
-
.pytest_cache/
|
|
23
|
-
.mypy_cache/
|
|
24
|
-
.ruff_cache/
|
|
25
|
-
|
|
26
|
-
# ── Atlas debug session logs ──────────────────────────────────────────────────
|
|
27
|
-
# Bug reports are per-session working files, not source. README.md is kept.
|
|
28
|
-
debug/atlas-*.jsonl
|
|
29
|
-
debug/atlas-*.md
|
|
30
|
-
debug/latest.md
|
|
31
|
-
|
|
32
|
-
# ── Large test / sample data — on disk, not in history ────────────────────────
|
|
33
|
-
# 364 MB of Bakken + Vaca Muerta shapefiles. Git is not a data store; keeping
|
|
34
|
-
# these out is what makes the repo clonable. They stay on disk untouched.
|
|
35
|
-
incubator/testing-data/
|
|
36
|
-
**/TestingData/
|
|
37
|
-
*.duckdb
|
|
38
|
-
*.zip
|
|
39
|
-
|
|
40
|
-
# Parked experiments (see CLAUDE.md) — ~58 MB, mostly binary assets.
|
|
41
|
-
# Untracked, not deleted. Remove this line if you want them versioned.
|
|
42
|
-
incubator/archive/
|
|
43
|
-
|
|
44
|
-
# ── Editor / OS ───────────────────────────────────────────────────────────────
|
|
45
|
-
.idea/
|
|
46
|
-
.vscode/
|
|
47
|
-
*.swp
|
|
48
|
-
Thumbs.db
|
|
49
|
-
desktop.ini
|
|
50
|
-
$RECYCLE.BIN/
|
|
51
|
-
|
|
52
|
-
# ── Claude Code working state ─────────────────────────────────────────────────
|
|
53
|
-
.claude/worktrees/
|
|
54
|
-
.claude/shell-snapshots/
|
|
55
|
-
.claude/statsig/
|
|
56
|
-
.claude/todos/
|
|
57
|
-
|
|
58
|
-
# ── Scratch output ────────────────────────────────────────────────────────────
|
|
59
|
-
*.atlas.json.bak
|
|
60
|
-
*.bak-*
|
|
1
|
+
# ── Secrets — never commit ────────────────────────────────────────────────────
|
|
2
|
+
# Live PyPI API token used by dev/publish-*.bat
|
|
3
|
+
**/dev/token.txt
|
|
4
|
+
**/token.txt
|
|
5
|
+
*.pem
|
|
6
|
+
*.key
|
|
7
|
+
.env
|
|
8
|
+
.env.*
|
|
9
|
+
|
|
10
|
+
# ── Virtual environments ──────────────────────────────────────────────────────
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
env/
|
|
14
|
+
**/.venv/
|
|
15
|
+
|
|
16
|
+
# ── Python build / cache ──────────────────────────────────────────────────────
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.py[cod]
|
|
19
|
+
*.egg-info/
|
|
20
|
+
build/
|
|
21
|
+
dist/
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.mypy_cache/
|
|
24
|
+
.ruff_cache/
|
|
25
|
+
|
|
26
|
+
# ── Atlas debug session logs ──────────────────────────────────────────────────
|
|
27
|
+
# Bug reports are per-session working files, not source. README.md is kept.
|
|
28
|
+
debug/atlas-*.jsonl
|
|
29
|
+
debug/atlas-*.md
|
|
30
|
+
debug/latest.md
|
|
31
|
+
|
|
32
|
+
# ── Large test / sample data — on disk, not in history ────────────────────────
|
|
33
|
+
# 364 MB of Bakken + Vaca Muerta shapefiles. Git is not a data store; keeping
|
|
34
|
+
# these out is what makes the repo clonable. They stay on disk untouched.
|
|
35
|
+
incubator/testing-data/
|
|
36
|
+
**/TestingData/
|
|
37
|
+
*.duckdb
|
|
38
|
+
*.zip
|
|
39
|
+
|
|
40
|
+
# Parked experiments (see CLAUDE.md) — ~58 MB, mostly binary assets.
|
|
41
|
+
# Untracked, not deleted. Remove this line if you want them versioned.
|
|
42
|
+
incubator/archive/
|
|
43
|
+
|
|
44
|
+
# ── Editor / OS ───────────────────────────────────────────────────────────────
|
|
45
|
+
.idea/
|
|
46
|
+
.vscode/
|
|
47
|
+
*.swp
|
|
48
|
+
Thumbs.db
|
|
49
|
+
desktop.ini
|
|
50
|
+
$RECYCLE.BIN/
|
|
51
|
+
|
|
52
|
+
# ── Claude Code working state ─────────────────────────────────────────────────
|
|
53
|
+
.claude/worktrees/
|
|
54
|
+
.claude/shell-snapshots/
|
|
55
|
+
.claude/statsig/
|
|
56
|
+
.claude/todos/
|
|
57
|
+
|
|
58
|
+
# ── Scratch output ────────────────────────────────────────────────────────────
|
|
59
|
+
*.atlas.json.bak
|
|
60
|
+
*.bak-*
|
|
61
|
+
|
|
62
|
+
# ── Virtual environments the skill creates ────────────────────────────────────
|
|
63
|
+
# preflight.py builds skill/venv on the user's machine; it is platform-specific and large.
|
|
64
|
+
skill/venv/
|
|
65
|
+
|
|
66
|
+
# ── Handoff artifacts ─────────────────────────────────────────────────────────
|
|
67
|
+
# The .skill is rebuilt by package_skill.py and already versioned in releases/.
|
|
68
|
+
handoff/*.skill
|
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.2] - 2026-08-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **The README documented three operations out of seven.** `Union`, `Dissolve`, `SpatialJoin` and
|
|
7
|
+
`Near` had no entry at all, on the page PyPI renders as the front door, and had not for several
|
|
8
|
+
releases. All four now carry their real signatures and defaults; `Union` and `Intersect` note
|
|
9
|
+
that they accept a list; the async section names every variant rather than saying "all three";
|
|
10
|
+
and `SpatialResult` no longer claims to come from three operations.
|
|
11
|
+
|
|
12
|
+
Two things were wrong in the first draft and were caught against the code rather than shipped:
|
|
13
|
+
`Near` does **not** use ArcGIS's `NEAR_DIST` / `-1` convention — it writes a single column
|
|
14
|
+
named `dist_<near layer>_<unit>` and leaves it *null* beyond `max_distance`, keeping every
|
|
15
|
+
input feature — and the Spatial Join match list is all seven options in `SJ_MATCH`.
|
|
16
|
+
|
|
17
|
+
The durable part is in `docs/verify.py`: the expected operation list is derived from
|
|
18
|
+
`dir(env)`, and the quoted `DISSOLVE_STATISTICS` and `SJ_MATCH` values are compared against the
|
|
19
|
+
module. A new operation is now documented or the release fails. Nothing checked before, which
|
|
20
|
+
is why four of them went missing without anyone noticing.
|
|
21
|
+
|
|
22
|
+
## [1.2.1] - 2026-08-17
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **Union and Intersect take more than two layers.** Both accept a list where they used to
|
|
26
|
+
accept one other layer — `env.Union("a.shp", ["b.shp", "c.shp", "d.shp"])` — and Atlas offers
|
|
27
|
+
a checkbox list of layers instead of a single dropdown, the same picker Dissolve uses for
|
|
28
|
+
fields. Three shapefiles used to be two operations plus an intermediate file to name, track
|
|
29
|
+
and delete.
|
|
30
|
+
|
|
31
|
+
The layers are folded pairwise, which is the correct N-way answer. For Union, the `FID_`
|
|
32
|
+
columns are attached to every input **before** the fold rather than per pair, so each input
|
|
33
|
+
keeps its own through every pass: a piece of the output still says which of the ten layers
|
|
34
|
+
made it. The input's `where` filter applies to the first pass only — after that the
|
|
35
|
+
accumulator is an intermediate the filter was never written against.
|
|
36
|
+
|
|
37
|
+
`POST /api/geoprocess` takes `other_layers` alongside the old `other_layer`, merges the two
|
|
38
|
+
and drops duplicates, so a script written against the previous shape keeps working. Clip and
|
|
39
|
+
Spatial Join still pair exactly two layers, and say so if handed a list.
|
|
40
|
+
- **`POST /api/export/pdf`** builds a PDF from a map image with Pillow and returns the bytes.
|
|
41
|
+
- **`POST /api/databricks/open-token-page`** and **`POST /api/open-url`** open a
|
|
42
|
+
settings-derived address in the user's real default browser. The page names a *setting*, never
|
|
43
|
+
a URL, so there is no request that makes the server open something the user did not configure.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- **The desktop window opens maximized.** Filling the screen is what a map
|
|
47
|
+
application wants: the old 1600x1000 default left a ribbon, two docked panels and a status
|
|
48
|
+
strip competing for what was left. Maximized rather than fullscreen, deliberately — the
|
|
49
|
+
title bar, close button and taskbar stay, because Atlas is something you work alongside
|
|
50
|
+
Claude and a file explorer, not a kiosk. Un-maximizing gives back the 1600x1000 window.
|
|
51
|
+
Both window paths honour it (pywebview's `maximized`, Chromium's `--start-maximized`), an
|
|
52
|
+
older pywebview without the argument degrades to a sized window rather than failing, and
|
|
53
|
+
`open_window(..., maximized=False)` opts out.
|
|
54
|
+
- **Atlas raises no browser dialogs at all.** Every `alert`, `confirm` and `prompt` — twenty of
|
|
55
|
+
them — is now an Atlas panel: themed, with buttons that say what they do (**Delete field**,
|
|
56
|
+
not **OK**), red for destructive answers, Esc to cancel and Enter to accept. A native dialog
|
|
57
|
+
announces its origin in the title bar (*"localhost:8000 says"*), which reads as a stray
|
|
58
|
+
browser inside a desktop application, cannot be styled or relabelled, and blocks the event
|
|
59
|
+
loop so the map stops redrawing behind it.
|
|
60
|
+
- **Nothing calls `window.open` either.** In a desktop window an embedded webview has no
|
|
61
|
+
pop-ups, so those calls returned null and did nothing: **Find token** silently failed, and
|
|
62
|
+
*Export PDF* threw where it wrote into the window it never got. Both now go through the
|
|
63
|
+
server — Find token opens the page, PDF comes back as bytes the page downloads like the SVG.
|
|
64
|
+
A PDF export is also now genuinely a PDF, rather than whatever the print dialog produced.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
- **Intersect said "returned 0 features" when a fold emptied the result**, which with several
|
|
68
|
+
inputs is not a diagnosis. It now names the layer nothing survived, says how many were
|
|
69
|
+
skipped, and points out that every layer overlaps the *input* — it is the running result they
|
|
70
|
+
stop sharing area with.
|
|
71
|
+
- **A zero-area intersection read as success.** Where layers touch along a boundary rather than
|
|
72
|
+
overlapping, OGR returns the shared edge as a feature: `output_count` was 1 with no area in
|
|
73
|
+
it, and `count_slivers` does not catch it because a line is not a thin polygon. Now warned
|
|
74
|
+
about explicitly.
|
|
75
|
+
- **`FID_` collisions in Union.** Two layers with the same stem, or two GeoDataFrames (which
|
|
76
|
+
have no stem at all), produced the same column name — and a collision does not raise, the
|
|
77
|
+
second write just wins, so the output claimed a piece came from a layer that did not
|
|
78
|
+
contribute it.
|
|
79
|
+
|
|
3
80
|
## [1.2.0] - 2026-08-13
|
|
4
81
|
|
|
5
82
|
Symbology fixes, all of them one mistake made in several places: coercing on the wrong side of
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: env-able
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: AI-ready GIS toolkit for energy and subsurface workflows.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -128,18 +128,97 @@ print(result) # SpatialResult stats
|
|
|
128
128
|
---
|
|
129
129
|
|
|
130
130
|
### `env.Intersect(input_layer, intersect_layer, output=None, where=None, preview=False)`
|
|
131
|
-
Geometric intersection
|
|
131
|
+
Geometric intersection — only what the layers share, with attributes from all of them (`OGR Layer.Intersection()` engine). Returns a `SpatialResult`.
|
|
132
132
|
- `input_layer` — point, line, or polygon (file path or GeoDataFrame)
|
|
133
|
-
- `intersect_layer` —
|
|
133
|
+
- `intersect_layer` — **one layer or a list.** A list is folded pairwise, which is exactly the N-way intersection: what the input shares with the first, then what that shares with the second, and so on
|
|
134
134
|
- `output` — output file path
|
|
135
135
|
- `where` — optional SQL attribute filter on the input layer
|
|
136
136
|
- `preview` — if `True`, pushes result to Atlas on completion
|
|
137
137
|
|
|
138
138
|
```python
|
|
139
139
|
result = env.Intersect("wells.shp", "permits.gpkg", output="wells_permits.gpkg")
|
|
140
|
+
result = env.Intersect("wells.shp", ["leases.gpkg", "counties.gpkg"]) # three-way
|
|
140
141
|
result.preview("Permitted Wells")
|
|
141
142
|
```
|
|
142
143
|
|
|
144
|
+
If one layer leaves nothing, the fold stops and the warning names it — with four inputs, "returned 0 features" is not a diagnosis.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### `env.Union(input_layer, union_layer, output=None, join_attributes="ALL", gaps=True, cluster_tolerance=None, where=None, preview=False)`
|
|
149
|
+
Everything from every layer, cut where they overlap (`OGR Layer.Union()` engine). Polygons. Mirrors `arcpy.analysis.Union`. Returns a `SpatialResult`.
|
|
150
|
+
|
|
151
|
+
Where Intersect answers "what do these share?", Union answers "what is covered by either, and by which?" — so the output usually has **more** features than the inputs, because each overlap becomes its own piece.
|
|
152
|
+
|
|
153
|
+
- `union_layer` — **one layer or a list**, folded pairwise. Each input keeps its own `FID_` column through the whole fold
|
|
154
|
+
- `join_attributes` — `"ALL"` (everything, plus a `FID_<layer>` per input), `"NO_FID"` (everything except those), `"ONLY_FID"` (just those)
|
|
155
|
+
- `gaps` — `False` (ArcGIS `NO_GAPS`) gives each enclosed void a feature of its own, with every `FID_` at `-1`, so gaps can be selected and inspected
|
|
156
|
+
- `cluster_tolerance` — snap coordinates within this distance together, as a GEOS precision grid
|
|
157
|
+
- `where` — SQL attribute filter on the input, applied before the overlay
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
result = env.Union("leases.gpkg", "units.gpkg", output="lease_units.gpkg",
|
|
161
|
+
join_attributes="ONLY_FID")
|
|
162
|
+
result = env.Union("a.shp", ["b.shp", "c.shp", "d.shp"]) # four layers, one call
|
|
163
|
+
result = env.Union(a, b, gaps=False) # a feature per enclosed void
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`FID_<layer>` is the source feature's id, or **-1** where that layer contributed nothing to the piece — which is how you tell a lease-only sliver from a genuine overlap. Non-overlapping inputs are valid (the answer is the layers side by side) and warn, naming the layer that misses, because that usually means a CRS mistake.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### `env.Dissolve(input_layer, output=None, dissolve_field=None, statistics_fields=None, multi_part=True, unsplit_lines=False, where=None, preview=False)`
|
|
171
|
+
Merge features that share a value, and summarise the rest. Mirrors `arcpy.management.Dissolve`. Returns a `SpatialResult`.
|
|
172
|
+
|
|
173
|
+
- `dissolve_field` — field name or list of them. **Omit it and every feature merges into one**, which is the usual way to turn a pile of parcels into an outline
|
|
174
|
+
- `statistics_fields` — `[[field, stat], ...]`. `SUM`, `MEAN`, `MIN`, `MAX`, `RANGE`, `STD`, `COUNT`, `FIRST`, `LAST`, `MEDIAN`, `VARIANCE`, `UNIQUE`, `CONCATENATE`. Output columns are named `SUM_acres`, `COUNT_id` and so on, as ArcGIS names them
|
|
175
|
+
- `multi_part` — `False` splits disjoint pieces into a feature each, repeating the attributes (ArcGIS's `SINGLE_PART`)
|
|
176
|
+
- `unsplit_lines` — lines only: merge runs that join end to end, so two unconnected roads called "Main" stay two features
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
result = env.Dissolve("parcels.gpkg", output="operators.gpkg",
|
|
180
|
+
dissolve_field="operator",
|
|
181
|
+
statistics_fields=[["acres", "SUM"], ["wells", "COUNT"]])
|
|
182
|
+
result = env.Dissolve("parcels.gpkg") # everything becomes one outline
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### `env.SpatialJoin(target_layer, join_layer, match="intersects", how="one_to_one", keep_all=True, fields=None, stats=None, search_radius=None, unit="km", output=None, preview=False)`
|
|
188
|
+
Transfer attributes from one layer to another by location. Mirrors `arcpy.analysis.SpatialJoin`. Returns a `SpatialResult`.
|
|
189
|
+
|
|
190
|
+
- `match` — `intersects` (default), `contains`, `within`, `crosses`, `overlaps`, `touches`, `closest`
|
|
191
|
+
- `how` — `one_to_one` (one output row per target, summarised) or `one_to_many` (a row per match)
|
|
192
|
+
- `keep_all` — `True` keeps targets with no match (ArcGIS's `KEEP_ALL`); `False` drops them
|
|
193
|
+
- `fields` — which join-layer fields to carry across. Omit for all of them
|
|
194
|
+
- `stats` — `[[field, stat], ...]` for `one_to_one`, since several matches have to collapse into one row
|
|
195
|
+
- `search_radius` / `unit` — only meaningful for `match="closest"`
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
result = env.SpatialJoin("wells.shp", "counties.gpkg", match="within")
|
|
199
|
+
result = env.SpatialJoin("wells.shp", "leases.gpkg", match="closest",
|
|
200
|
+
search_radius=2, unit="miles")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### `env.Near(input_layer, near_layer, name=None, unit="km", output=None, max_distance=None, preview=False)`
|
|
206
|
+
Distance from each input feature to the nearest feature in another layer. Returns a `SpatialResult`.
|
|
207
|
+
|
|
208
|
+
It **adds a column and changes nothing else** — same geometry, same feature count, every input kept. So the result draws exactly like the input and can be styled, filtered or sorted by distance. Distances are measured in a projected CRS for true ground distance, then the result comes back in the input's own CRS.
|
|
209
|
+
|
|
210
|
+
- `name` — the column to write. Defaults to **`dist_<near layer>_<unit>`**, so measuring one layer against several gives you a column per comparison rather than a collision
|
|
211
|
+
- `unit` — `km` (default), `meters`, `miles`, `feet`, `nm`
|
|
212
|
+
- `max_distance` — in `unit`. Beyond it the column is **null**, not a sentinel value, and the result warns how many features had no neighbour in range
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
result = env.Near("wells.shp", "pipelines.gpkg", unit="miles", max_distance=5)
|
|
216
|
+
result = env.Near("wells.shp", "faults.gpkg", name="dist_to_fault_km")
|
|
217
|
+
result.preview("Wells by distance to pipeline")
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Where several near features tie for closest, the first is kept and the result says how many tied.
|
|
221
|
+
|
|
143
222
|
---
|
|
144
223
|
|
|
145
224
|
### `env.load(source)` — fluent spatial pipeline
|
|
@@ -166,7 +245,7 @@ results = await asyncio.gather(
|
|
|
166
245
|
---
|
|
167
246
|
|
|
168
247
|
### Async variants
|
|
169
|
-
|
|
248
|
+
Every operation has an async counterpart — `clip_async`, `buffer_async`, `intersect_async`, `union_async`, `dissolve_async`, `spatial_join_async`, `near_async`, `morph_async` — running in a thread-pool executor. GDAL releases the GIL, so multiple calls run truly in parallel via `asyncio.gather()`.
|
|
170
249
|
|
|
171
250
|
```python
|
|
172
251
|
import asyncio
|
|
@@ -185,13 +264,13 @@ r1, r2, r3 = await asyncio.gather(
|
|
|
185
264
|
---
|
|
186
265
|
|
|
187
266
|
### `SpatialResult`
|
|
188
|
-
Returned by
|
|
267
|
+
Returned by every operation and by the pipeline's `.run()`. Exposes operation stats and chainable output methods.
|
|
189
268
|
|
|
190
269
|
| Attribute / Method | Description |
|
|
191
270
|
|---|---|
|
|
192
271
|
| `input_count` | Feature count of the input layer |
|
|
193
272
|
| `output_count` | Feature count of the result |
|
|
194
|
-
| `dropped` |
|
|
273
|
+
| `dropped` | Features lost. Never negative — Union and Dissolve can grow the count |
|
|
195
274
|
| `slivers` | Polygon features with suspiciously small area (intersection artifacts) |
|
|
196
275
|
| `crs` | CRS of the output (e.g. `EPSG:4326`) |
|
|
197
276
|
| `elapsed_s` | Wall-clock seconds for the operation |
|
|
@@ -309,6 +388,50 @@ print(f"{rows:,} rows written")
|
|
|
309
388
|
---
|
|
310
389
|
|
|
311
390
|
## Changelog
|
|
391
|
+
### Unreleased
|
|
392
|
+
|
|
393
|
+
**Fixed**
|
|
394
|
+
- **The README documented three operations out of seven.** `Union`, `Dissolve`, `SpatialJoin`
|
|
395
|
+
and `Near` had no entry at all — for several releases, on the page PyPI renders as the front
|
|
396
|
+
door. All four are now documented with their real signatures and defaults, `Union` and
|
|
397
|
+
`Intersect` note that they take a list, and the async list names every variant instead of
|
|
398
|
+
claiming there are three.
|
|
399
|
+
|
|
400
|
+
Two corrections while writing them: `Near` was about to be described with ArcGIS's
|
|
401
|
+
`NEAR_DIST` / `-1` convention, which it does **not** use — it writes one column named
|
|
402
|
+
`dist_<near layer>_<unit>` and leaves it *null* beyond `max_distance` — and the Spatial Join
|
|
403
|
+
match list is all seven options from `SJ_MATCH`, not three and an "and the rest".
|
|
404
|
+
|
|
405
|
+
`docs/verify.py` now derives the expected list from `dir(env)`, so the next operation added is
|
|
406
|
+
either documented or the release fails. That is the actual fix; the prose was only the symptom.
|
|
407
|
+
|
|
408
|
+
### v1.2.1 — 2026-08-17
|
|
409
|
+
|
|
410
|
+
**Added**
|
|
411
|
+
- **Union and Intersect take more than two layers.** Pass a list —
|
|
412
|
+
`env.Union("a.shp", ["b.shp", "c.shp", "d.shp"])` — and Atlas offers a checkbox list of layers
|
|
413
|
+
instead of one dropdown. They fold pairwise, which is the correct N-way answer, and every
|
|
414
|
+
input keeps its own `FID_` column through the whole fold, so the output still says which layer
|
|
415
|
+
made each piece. `POST /api/geoprocess` gained `other_layers`; the old `other_layer` still
|
|
416
|
+
works and the two are merged.
|
|
417
|
+
- `POST /api/export/pdf` builds a real PDF server-side. `POST /api/databricks/open-token-page`
|
|
418
|
+
and `POST /api/open-url` open a settings-derived address in the default browser.
|
|
419
|
+
|
|
420
|
+
**Changed**
|
|
421
|
+
- **The desktop window opens maximized** rather than at 1600x1000 — the whole
|
|
422
|
+
screen, but still a window: title bar, close button and taskbar all stay.
|
|
423
|
+
- **No browser dialogs anywhere in Atlas.** All twenty `alert`/`confirm`/`prompt` calls are now
|
|
424
|
+
themed Atlas panels with buttons that name the action. A native dialog says
|
|
425
|
+
*"localhost:8000 says"*, cannot be styled, and blocks the map from redrawing behind it.
|
|
426
|
+
- **No `window.open` either** — it opens nothing at all in a desktop window, which is why
|
|
427
|
+
**Find token** silently did nothing and *Export PDF* failed. Both go through the server now.
|
|
428
|
+
|
|
429
|
+
**Fixed**
|
|
430
|
+
- Intersect names the layer that emptied a multi-layer fold instead of reporting "0 features".
|
|
431
|
+
- A zero-area intersection (layers that touch rather than overlap) is warned about rather than
|
|
432
|
+
counted as a result.
|
|
433
|
+
- `FID_` column collisions in Union, where two layers shared a stem and the second silently won.
|
|
434
|
+
|
|
312
435
|
### v1.2.0 — 2026-08-13
|
|
313
436
|
|
|
314
437
|
Symbology fixes. All of them the same mistake in several places: coercing on the wrong side of a
|