env-able 2.0.0__tar.gz → 2.0.1__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-2.0.0 → env_able-2.0.1}/.gitignore +1 -0
- {env_able-2.0.0 → env_able-2.0.1}/CHANGELOG.md +35 -0
- {env_able-2.0.0 → env_able-2.0.1}/PKG-INFO +36 -1
- {env_able-2.0.0 → env_able-2.0.1}/README.md +35 -0
- {env_able-2.0.0 → env_able-2.0.1}/pyproject.toml +1 -1
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/__init__.py +1 -1
- env_able-2.0.1/src/env_able/atlas/calc.py +211 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/docs/atlas-guide.md +58 -14
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/docs/env-able-library.md +3 -2
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/index.html +404 -48
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/server.py +420 -60
- env_able-2.0.1/src/env_able/remote.py +326 -0
- env_able-2.0.0/src/env_able/remote.py +0 -203
- {env_able-2.0.0 → env_able-2.0.1}/LICENSE +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/_gdal_env.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/_gdal_utils.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/_pipeline.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/_result.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/aggregate.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/__init__.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/aix.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/brandmark.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/claude_bridge.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/client.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/compliance.html +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/databricks.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/debug.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/desktop.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/docs/atlas-layout-view.md +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/docs/cartographic-judgment.md +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/edits.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/eirstyle.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/env_layers.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/landing.html +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/naming.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/palette.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/project.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/session.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/splash.html +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/templates/Summary A4 Landscape.pagx +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/templates/Summary A4 Portrait.pagx +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/templates/Title Bar A4 Landscape.pagx +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/templates/Title Bar A4 Portrait.pagx +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/HTML-TO-IMAGE-LICENSE.txt +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/MAPLIBRE-LICENSE.txt +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/PROTOMAPS-LICENSE.txt +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/ROBOTO-LICENSE.txt +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/html-to-image.js +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/maplibre-gl.css +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/maplibre-gl.js +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/pmtiles.js +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/protomaps-basemaps.js +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/roboto-latin-400-italic.woff2 +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/roboto-latin-400-normal.woff2 +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/roboto-latin-500-normal.woff2 +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/roboto-latin-700-normal.woff2 +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/classify.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/layout.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/morph.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/pull.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/stream.py +0 -0
- {env_able-2.0.0 → env_able-2.0.1}/src/env_able/vector.py +0 -0
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.1] - 2026-10-15
|
|
4
|
+
### Added
|
|
5
|
+
- **Calculate Field** in the attribute table: ArcGIS Pro's `!FIELD!` syntax, the selected
|
|
6
|
+
records only when there is a selection, Preview, and one Undo for the whole column. Parsed with
|
|
7
|
+
`ast` and interpreted, never `eval`'d: no import, no attribute walk, no oversized strings.
|
|
8
|
+
- **The attribute table has Pro's toolbar**: All | Selected tabs with counts, Select All, Switch,
|
|
9
|
+
Clear, Zoom To, Add Field and Calculate.
|
|
10
|
+
- **Select is an arrow with a ▾ menu** of Click, Rectangle and Lasso, each with its icon, as in Pro.
|
|
11
|
+
- **Click mode is Pro's Select**: a click picks the topmost feature, a drag picks a box, and
|
|
12
|
+
Shift adds and Ctrl removes either way. Middle-drag pans while Select is on.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Shift-drag to add to a selection zoomed the map instead**: Shift-drag is MapLibre's box zoom.
|
|
16
|
+
Select turns it off while it is on. A tiny Ctrl-drag no longer clears the selection.
|
|
17
|
+
- **Opening an attribute table started editing**, showing Undo, Save As and "Editing" to somebody
|
|
18
|
+
who had come to read it. Edit, Add Field and Calculate arm it now.
|
|
19
|
+
- **A `.shp` dragged on its own lost its `.prj` and `.dbf`**: the CRS was guessed from the
|
|
20
|
+
coordinate range and every attribute dropped. Atlas now reads the file where it lives, sidecars
|
|
21
|
+
and all, when name, size, modified time and a hash of its first and last 256 KB match.
|
|
22
|
+
- **From URL on an ArcGIS group layer loaded nothing**: the group was queried directly and the
|
|
23
|
+
service answered "Invalid or missing input parameters". A group now loads as its sublayers, a
|
|
24
|
+
whole `…/MapServer` as every feature layer in it, and a `…/query` address as its layer.
|
|
25
|
+
- **An upload froze the whole application while it loaded**: the read ran on the server's event
|
|
26
|
+
loop, so the poll, the tiles and the Claude pane all waited. It runs on a worker thread now.
|
|
27
|
+
- **From URL held the layer lock for the whole download**, so every other map change waited on a
|
|
28
|
+
remote server. It is held for the add only.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- **Add Layer no longer uploads a file Atlas can find on disk.** A 34,596-feature well layer
|
|
32
|
+
(183 MB with its `.dbf`) went from 11.2 s to 3.5 s, and is referenced by a save rather than copied.
|
|
33
|
+
- **GeoJSON conversion is 40% faster** (3.3 s to 2.0 s on that layer): attributes column-wise, and
|
|
34
|
+
points and lines from one vectorised coordinate read. Byte-identical output.
|
|
35
|
+
- **ArcGIS services are fetched by object id, six pages at a time, three layers at once**, with a
|
|
36
|
+
toast that counts features as they arrive. A ten-layer Alberta service: 72 s to 31 s.
|
|
37
|
+
|
|
3
38
|
## [2.0.0] - 2026-10-07
|
|
4
39
|
Atlas becomes two products on one engine. **Atlas Pro** (`atlas-pro`) is the desktop GIS and draws
|
|
5
40
|
only with MapLibre. **atlas** is the skill: a folder in, a finished map out, in any assistant or an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: env-able
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: AI-ready GIS toolkit for energy and subsurface workflows.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -404,6 +404,41 @@ print(f"{rows:,} rows written")
|
|
|
404
404
|
---
|
|
405
405
|
|
|
406
406
|
## Changelog
|
|
407
|
+
### v2.0.1 — 2026-10-15
|
|
408
|
+
**Added**
|
|
409
|
+
- **Calculate Field** in the attribute table: ArcGIS Pro's `!FIELD!` syntax, the selected
|
|
410
|
+
records only when there is a selection, Preview, and one Undo for the whole column. Parsed with
|
|
411
|
+
`ast` and interpreted, never `eval`'d: no import, no attribute walk, no oversized strings.
|
|
412
|
+
- **The attribute table has Pro's toolbar**: All | Selected tabs with counts, Select All, Switch,
|
|
413
|
+
Clear, Zoom To, Add Field and Calculate.
|
|
414
|
+
- **Select is an arrow with a ▾ menu** of Click, Rectangle and Lasso, each with its icon, as in Pro.
|
|
415
|
+
- **Click mode is Pro's Select**: a click picks the topmost feature, a drag picks a box, and
|
|
416
|
+
Shift adds and Ctrl removes either way. Middle-drag pans while Select is on.
|
|
417
|
+
|
|
418
|
+
**Fixed**
|
|
419
|
+
- **Shift-drag to add to a selection zoomed the map instead**: Shift-drag is MapLibre's box zoom.
|
|
420
|
+
Select turns it off while it is on. A tiny Ctrl-drag no longer clears the selection.
|
|
421
|
+
- **Opening an attribute table started editing**, showing Undo, Save As and "Editing" to somebody
|
|
422
|
+
who had come to read it. Edit, Add Field and Calculate arm it now.
|
|
423
|
+
- **A `.shp` dragged on its own lost its `.prj` and `.dbf`**: the CRS was guessed from the
|
|
424
|
+
coordinate range and every attribute dropped. Atlas now reads the file where it lives, sidecars
|
|
425
|
+
and all, when name, size, modified time and a hash of its first and last 256 KB match.
|
|
426
|
+
- **From URL on an ArcGIS group layer loaded nothing**: the group was queried directly and the
|
|
427
|
+
service answered "Invalid or missing input parameters". A group now loads as its sublayers, a
|
|
428
|
+
whole `…/MapServer` as every feature layer in it, and a `…/query` address as its layer.
|
|
429
|
+
- **An upload froze the whole application while it loaded**: the read ran on the server's event
|
|
430
|
+
loop, so the poll, the tiles and the Claude pane all waited. It runs on a worker thread now.
|
|
431
|
+
- **From URL held the layer lock for the whole download**, so every other map change waited on a
|
|
432
|
+
remote server. It is held for the add only.
|
|
433
|
+
|
|
434
|
+
**Changed**
|
|
435
|
+
- **Add Layer no longer uploads a file Atlas can find on disk.** A 34,596-feature well layer
|
|
436
|
+
(183 MB with its `.dbf`) went from 11.2 s to 3.5 s, and is referenced by a save rather than copied.
|
|
437
|
+
- **GeoJSON conversion is 40% faster** (3.3 s to 2.0 s on that layer): attributes column-wise, and
|
|
438
|
+
points and lines from one vectorised coordinate read. Byte-identical output.
|
|
439
|
+
- **ArcGIS services are fetched by object id, six pages at a time, three layers at once**, with a
|
|
440
|
+
toast that counts features as they arrive. A ten-layer Alberta service: 72 s to 31 s.
|
|
441
|
+
|
|
407
442
|
### v2.0.0 — 2026-10-07
|
|
408
443
|
Atlas becomes two products on one engine. **Atlas Pro** (`atlas-pro`) is the desktop GIS and draws
|
|
409
444
|
only with MapLibre. **atlas** is the skill: a folder in, a finished map out, in any assistant or an
|
|
@@ -378,6 +378,41 @@ print(f"{rows:,} rows written")
|
|
|
378
378
|
---
|
|
379
379
|
|
|
380
380
|
## Changelog
|
|
381
|
+
### v2.0.1 — 2026-10-15
|
|
382
|
+
**Added**
|
|
383
|
+
- **Calculate Field** in the attribute table: ArcGIS Pro's `!FIELD!` syntax, the selected
|
|
384
|
+
records only when there is a selection, Preview, and one Undo for the whole column. Parsed with
|
|
385
|
+
`ast` and interpreted, never `eval`'d: no import, no attribute walk, no oversized strings.
|
|
386
|
+
- **The attribute table has Pro's toolbar**: All | Selected tabs with counts, Select All, Switch,
|
|
387
|
+
Clear, Zoom To, Add Field and Calculate.
|
|
388
|
+
- **Select is an arrow with a ▾ menu** of Click, Rectangle and Lasso, each with its icon, as in Pro.
|
|
389
|
+
- **Click mode is Pro's Select**: a click picks the topmost feature, a drag picks a box, and
|
|
390
|
+
Shift adds and Ctrl removes either way. Middle-drag pans while Select is on.
|
|
391
|
+
|
|
392
|
+
**Fixed**
|
|
393
|
+
- **Shift-drag to add to a selection zoomed the map instead**: Shift-drag is MapLibre's box zoom.
|
|
394
|
+
Select turns it off while it is on. A tiny Ctrl-drag no longer clears the selection.
|
|
395
|
+
- **Opening an attribute table started editing**, showing Undo, Save As and "Editing" to somebody
|
|
396
|
+
who had come to read it. Edit, Add Field and Calculate arm it now.
|
|
397
|
+
- **A `.shp` dragged on its own lost its `.prj` and `.dbf`**: the CRS was guessed from the
|
|
398
|
+
coordinate range and every attribute dropped. Atlas now reads the file where it lives, sidecars
|
|
399
|
+
and all, when name, size, modified time and a hash of its first and last 256 KB match.
|
|
400
|
+
- **From URL on an ArcGIS group layer loaded nothing**: the group was queried directly and the
|
|
401
|
+
service answered "Invalid or missing input parameters". A group now loads as its sublayers, a
|
|
402
|
+
whole `…/MapServer` as every feature layer in it, and a `…/query` address as its layer.
|
|
403
|
+
- **An upload froze the whole application while it loaded**: the read ran on the server's event
|
|
404
|
+
loop, so the poll, the tiles and the Claude pane all waited. It runs on a worker thread now.
|
|
405
|
+
- **From URL held the layer lock for the whole download**, so every other map change waited on a
|
|
406
|
+
remote server. It is held for the add only.
|
|
407
|
+
|
|
408
|
+
**Changed**
|
|
409
|
+
- **Add Layer no longer uploads a file Atlas can find on disk.** A 34,596-feature well layer
|
|
410
|
+
(183 MB with its `.dbf`) went from 11.2 s to 3.5 s, and is referenced by a save rather than copied.
|
|
411
|
+
- **GeoJSON conversion is 40% faster** (3.3 s to 2.0 s on that layer): attributes column-wise, and
|
|
412
|
+
points and lines from one vectorised coordinate read. Byte-identical output.
|
|
413
|
+
- **ArcGIS services are fetched by object id, six pages at a time, three layers at once**, with a
|
|
414
|
+
toast that counts features as they arrive. A ten-layer Alberta service: 72 s to 31 s.
|
|
415
|
+
|
|
381
416
|
### v2.0.0 — 2026-10-07
|
|
382
417
|
Atlas becomes two products on one engine. **Atlas Pro** (`atlas-pro`) is the desktop GIS and draws
|
|
383
418
|
only with MapLibre. **atlas** is the skill: a folder in, a finished map out, in any assistant or an
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Calculate Field: one expression, evaluated per row, with nothing else reachable.
|
|
3
|
+
|
|
4
|
+
calc.compile_expr("!EUR! * 1000", fields) -> a callable taking a row dict
|
|
5
|
+
calc.compile_expr("!NAME!.upper()", fields)
|
|
6
|
+
|
|
7
|
+
ArcGIS Pro's syntax, so an expression copied out of Pro works here: a field is written
|
|
8
|
+
`!FIELD!`. A bare name works too when it is a field (`EUR * 1000`), because that is what people
|
|
9
|
+
type first.
|
|
10
|
+
|
|
11
|
+
It is Python's grammar, NOT Python. The expression is parsed with `ast` and walked by an
|
|
12
|
+
interpreter that knows a short list of node types; anything else -- attribute access other than
|
|
13
|
+
the string methods below, comprehensions, lambdas, imports, dunder names, calls to anything not
|
|
14
|
+
in FUNCS -- is refused at compile time with the reason. `eval` is never called. That matters
|
|
15
|
+
because the Claude pane can reach this through the same endpoint, and an expression is text a
|
|
16
|
+
model may have been handed by a document it was asked to read.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import ast
|
|
21
|
+
import math
|
|
22
|
+
import operator
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
FUNCS = {
|
|
26
|
+
"abs": abs, "round": round, "min": min, "max": max, "len": len,
|
|
27
|
+
"str": str, "int": int, "float": float, "bool": bool,
|
|
28
|
+
"sqrt": math.sqrt, "floor": math.floor, "ceil": math.ceil, "log": math.log,
|
|
29
|
+
"log10": math.log10, "exp": math.exp,
|
|
30
|
+
}
|
|
31
|
+
#: Methods a string may be asked for. Each returns a string, a bool or an int, so a chain of
|
|
32
|
+
#: them cannot reach anything that is not data.
|
|
33
|
+
STR_METHODS = {"upper", "lower", "title", "capitalize", "strip", "lstrip", "rstrip", "replace",
|
|
34
|
+
"zfill", "startswith", "endswith", "find", "count", "center", "ljust", "rjust"}
|
|
35
|
+
NAMES = {"None": None, "True": True, "False": False, "pi": math.pi}
|
|
36
|
+
|
|
37
|
+
_BIN = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
|
|
38
|
+
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv, ast.Mod: operator.mod,
|
|
39
|
+
ast.Pow: operator.pow}
|
|
40
|
+
_UN = {ast.USub: operator.neg, ast.UAdd: operator.pos, ast.Not: operator.not_}
|
|
41
|
+
_CMP = {ast.Eq: operator.eq, ast.NotEq: operator.ne, ast.Lt: operator.lt, ast.LtE: operator.le,
|
|
42
|
+
ast.Gt: operator.gt, ast.GtE: operator.ge, ast.In: lambda a, b: a in b,
|
|
43
|
+
ast.NotIn: lambda a, b: a not in b, ast.Is: operator.is_, ast.IsNot: operator.is_not}
|
|
44
|
+
|
|
45
|
+
_MAX_POW = 1000
|
|
46
|
+
_MAX_LEN = 10_000
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CalcError(ValueError):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _field_token(i: int) -> str:
|
|
54
|
+
return "__f%d" % i
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def compile_expr(expr: str, fields):
|
|
58
|
+
"""
|
|
59
|
+
A function row -> value for `expr`, or CalcError naming what is not allowed.
|
|
60
|
+
|
|
61
|
+
Fields are resolved now, not per row, so a misspelt `!EURR!` fails before anything runs.
|
|
62
|
+
"""
|
|
63
|
+
src = (expr or "").strip()
|
|
64
|
+
if not src:
|
|
65
|
+
raise CalcError("The expression is empty.")
|
|
66
|
+
if len(src) > 2000:
|
|
67
|
+
raise CalcError("The expression is too long (2,000 characters at most).")
|
|
68
|
+
fields = list(fields)
|
|
69
|
+
lookup = {f.lower(): f for f in fields}
|
|
70
|
+
slots = {}
|
|
71
|
+
|
|
72
|
+
def sub(m):
|
|
73
|
+
name = m.group(1).strip()
|
|
74
|
+
real = name if name in fields else lookup.get(name.lower())
|
|
75
|
+
if real is None:
|
|
76
|
+
raise CalcError("!%s! is not a field in this layer." % name)
|
|
77
|
+
if real not in slots:
|
|
78
|
+
slots[real] = _field_token(len(slots))
|
|
79
|
+
return slots[real]
|
|
80
|
+
|
|
81
|
+
src = re.sub(r"!([^!]+)!", sub, src)
|
|
82
|
+
try:
|
|
83
|
+
tree = ast.parse(src, mode="eval")
|
|
84
|
+
except SyntaxError as e:
|
|
85
|
+
raise CalcError("That is not a valid expression: %s" % (e.msg or e))
|
|
86
|
+
byslot = {v: k for k, v in slots.items()}
|
|
87
|
+
|
|
88
|
+
def check(node):
|
|
89
|
+
for n in ast.walk(node):
|
|
90
|
+
if isinstance(n, ast.Name):
|
|
91
|
+
if n.id.startswith("__f") and n.id in byslot:
|
|
92
|
+
continue
|
|
93
|
+
if n.id in NAMES or n.id in FUNCS:
|
|
94
|
+
continue
|
|
95
|
+
if n.id in fields or n.id.lower() in lookup:
|
|
96
|
+
continue
|
|
97
|
+
raise CalcError("%r is not a field or a function. Write a field as !NAME!." % n.id)
|
|
98
|
+
elif isinstance(n, ast.Attribute):
|
|
99
|
+
if n.attr not in STR_METHODS:
|
|
100
|
+
raise CalcError(".%s is not allowed; string methods are: %s"
|
|
101
|
+
% (n.attr, ", ".join(sorted(STR_METHODS))))
|
|
102
|
+
elif isinstance(n, ast.Call):
|
|
103
|
+
f = n.func
|
|
104
|
+
if not ((isinstance(f, ast.Name) and f.id in FUNCS)
|
|
105
|
+
or (isinstance(f, ast.Attribute) and f.attr in STR_METHODS)):
|
|
106
|
+
raise CalcError("Only these functions are available: %s"
|
|
107
|
+
% ", ".join(sorted(FUNCS)))
|
|
108
|
+
if n.keywords:
|
|
109
|
+
raise CalcError("Keyword arguments are not supported.")
|
|
110
|
+
elif not isinstance(n, (ast.Expression, ast.BinOp, ast.UnaryOp, ast.BoolOp,
|
|
111
|
+
ast.Compare, ast.IfExp, ast.Constant, ast.Load,
|
|
112
|
+
ast.Subscript, ast.Slice, ast.Tuple, ast.JoinedStr,
|
|
113
|
+
ast.FormattedValue, ast.operator, ast.unaryop,
|
|
114
|
+
ast.boolop, ast.cmpop)):
|
|
115
|
+
raise CalcError("%s is not allowed in a field calculation."
|
|
116
|
+
% type(n).__name__)
|
|
117
|
+
|
|
118
|
+
check(tree)
|
|
119
|
+
|
|
120
|
+
def ev(n, row):
|
|
121
|
+
t = type(n)
|
|
122
|
+
if t is ast.Expression:
|
|
123
|
+
return ev(n.body, row)
|
|
124
|
+
if t is ast.Constant:
|
|
125
|
+
return n.value
|
|
126
|
+
if t is ast.Name:
|
|
127
|
+
if n.id in byslot:
|
|
128
|
+
return row.get(byslot[n.id])
|
|
129
|
+
if n.id in NAMES:
|
|
130
|
+
return NAMES[n.id]
|
|
131
|
+
if n.id in FUNCS:
|
|
132
|
+
return FUNCS[n.id]
|
|
133
|
+
return row.get(n.id if n.id in fields else lookup[n.id.lower()])
|
|
134
|
+
if t is ast.BinOp:
|
|
135
|
+
a, b = ev(n.left, row), ev(n.right, row)
|
|
136
|
+
if isinstance(n.op, ast.Pow) and isinstance(b, (int, float)) and abs(b) > _MAX_POW:
|
|
137
|
+
raise CalcError("An exponent over %d is refused." % _MAX_POW)
|
|
138
|
+
if isinstance(n.op, ast.Mult) and (isinstance(a, str) or isinstance(b, str)):
|
|
139
|
+
s, k = (a, b) if isinstance(a, str) else (b, a)
|
|
140
|
+
if isinstance(k, int) and len(s) * k > _MAX_LEN:
|
|
141
|
+
raise CalcError("That would make a string over %d characters." % _MAX_LEN)
|
|
142
|
+
return _BIN[type(n.op)](a, b)
|
|
143
|
+
if t is ast.UnaryOp:
|
|
144
|
+
return _UN[type(n.op)](ev(n.operand, row))
|
|
145
|
+
if t is ast.BoolOp:
|
|
146
|
+
if isinstance(n.op, ast.And):
|
|
147
|
+
v = True
|
|
148
|
+
for x in n.values:
|
|
149
|
+
v = ev(x, row)
|
|
150
|
+
if not v:
|
|
151
|
+
return v
|
|
152
|
+
return v
|
|
153
|
+
v = False
|
|
154
|
+
for x in n.values:
|
|
155
|
+
v = ev(x, row)
|
|
156
|
+
if v:
|
|
157
|
+
return v
|
|
158
|
+
return v
|
|
159
|
+
if t is ast.Compare:
|
|
160
|
+
left = ev(n.left, row)
|
|
161
|
+
for op, c in zip(n.ops, n.comparators):
|
|
162
|
+
right = ev(c, row)
|
|
163
|
+
if not _CMP[type(op)](left, right):
|
|
164
|
+
return False
|
|
165
|
+
left = right
|
|
166
|
+
return True
|
|
167
|
+
if t is ast.IfExp:
|
|
168
|
+
return ev(n.body, row) if ev(n.test, row) else ev(n.orelse, row)
|
|
169
|
+
if t is ast.Call:
|
|
170
|
+
if isinstance(n.func, ast.Attribute):
|
|
171
|
+
obj = ev(n.func.value, row)
|
|
172
|
+
if not isinstance(obj, str):
|
|
173
|
+
raise CalcError(".%s() needs text, and this row has %r"
|
|
174
|
+
% (n.func.attr, obj))
|
|
175
|
+
args = [ev(a, row) for a in n.args]
|
|
176
|
+
# zfill(10**9) and friends build the string before anything could cap it.
|
|
177
|
+
if n.func.attr in ("zfill", "center", "ljust", "rjust") and any(
|
|
178
|
+
isinstance(a, int) and a > _MAX_LEN for a in args):
|
|
179
|
+
raise CalcError("A width over %d is refused." % _MAX_LEN)
|
|
180
|
+
return getattr(obj, n.func.attr)(*args)
|
|
181
|
+
return FUNCS[n.func.id](*[ev(a, row) for a in n.args])
|
|
182
|
+
if t is ast.Attribute:
|
|
183
|
+
raise CalcError(".%s must be called, as .%s()" % (n.attr, n.attr))
|
|
184
|
+
if t is ast.Subscript:
|
|
185
|
+
obj = ev(n.value, row)
|
|
186
|
+
s = n.slice
|
|
187
|
+
if type(s) is ast.Slice:
|
|
188
|
+
return obj[slice(*(ev(x, row) if x is not None else None
|
|
189
|
+
for x in (s.lower, s.upper, s.step)))]
|
|
190
|
+
return obj[ev(s, row)]
|
|
191
|
+
if t is ast.Tuple:
|
|
192
|
+
return tuple(ev(x, row) for x in n.elts)
|
|
193
|
+
if t is ast.JoinedStr:
|
|
194
|
+
return "".join(str(ev(v, row)) for v in n.values)
|
|
195
|
+
if t is ast.FormattedValue:
|
|
196
|
+
v = ev(n.value, row)
|
|
197
|
+
spec = ev(n.format_spec, row) if n.format_spec is not None else ""
|
|
198
|
+
if any(int(d) > _MAX_LEN for d in re.findall(r"\d+", spec)):
|
|
199
|
+
raise CalcError("A width over %d is refused." % _MAX_LEN)
|
|
200
|
+
return format(v, spec)
|
|
201
|
+
raise CalcError("%s is not allowed in a field calculation." % t.__name__)
|
|
202
|
+
|
|
203
|
+
def run(row):
|
|
204
|
+
try:
|
|
205
|
+
return ev(tree, row)
|
|
206
|
+
except CalcError:
|
|
207
|
+
raise
|
|
208
|
+
except Exception as e: # noqa: BLE001 - row-level, named
|
|
209
|
+
raise CalcError("%s: %s" % (type(e).__name__, e))
|
|
210
|
+
|
|
211
|
+
return run
|
|
@@ -8,7 +8,7 @@ GeoPackages, GeoJSON and spreadsheets, browses spatial tables in Databricks, sty
|
|
|
8
8
|
analyses them, lets you edit their attributes, and lays the result out for print.
|
|
9
9
|
|
|
10
10
|
It is part of **env-able** (see the separate *env-able* page for the Python library that
|
|
11
|
-
drives it). Version
|
|
11
|
+
drives it). Version 2.0.
|
|
12
12
|
|
|
13
13
|
> **New to Atlas? Take the tour.** There is a **Take the tour** button on the start page,
|
|
14
14
|
> and **File ▸ Take the tour** inside the map. Thirteen steps that spotlight each area and
|
|
@@ -332,16 +332,20 @@ says so, rather than freezing.
|
|
|
332
332
|
### Selecting features
|
|
333
333
|
|
|
334
334
|
**Select** on the *General* tab turns selection on; **Esc** turns it off and clears what is
|
|
335
|
-
selected.
|
|
335
|
+
selected. Its icon is an arrow showing the current mode, and the **▾** under it chooses how you
|
|
336
|
+
pick, as ArcGIS Pro's does. Choosing a mode also turns Select on. The cursor is an arrow for
|
|
337
|
+
Click and a crosshair for the modes that draw:
|
|
336
338
|
|
|
337
339
|
| Mode | How |
|
|
338
340
|
|---|---|
|
|
339
|
-
| **Click** |
|
|
341
|
+
| **Click** | ArcGIS Pro's Select: click a feature to select the topmost one under the cursor, or click and drag a box to select everything it touches |
|
|
340
342
|
| **Rectangle** | Drag a box; everything it touches is selected |
|
|
341
343
|
| **Lasso** | Draw a freehand loop; everything inside it is selected, including a polygon big enough to contain the whole loop |
|
|
342
344
|
|
|
343
345
|
**Shift** adds to the selection and **Ctrl** removes from it, in every mode. Clicking empty
|
|
344
|
-
space clears it.
|
|
346
|
+
space clears it. While Select is on, Shift-drag selects rather than box-zooming the map, and a
|
|
347
|
+
left drag selects rather than pans: **drag with the middle button** (the wheel) to pan, and the
|
|
348
|
+
wheel still zooms.
|
|
345
349
|
|
|
346
350
|
The rest of the **Selection** group:
|
|
347
351
|
|
|
@@ -352,7 +356,7 @@ The rest of the **Selection** group:
|
|
|
352
356
|
| **Clear** | Drops the selection |
|
|
353
357
|
| **Switch** | Selects what is not selected, in each layer that has a selection |
|
|
354
358
|
| **Zoom To** | Frames the selected features |
|
|
355
|
-
| **Attributes** | Opens the attribute table
|
|
359
|
+
| **Attributes** | Opens the attribute table on its **Selected** view. **All** and **Selected** at the top of the table switch between the two |
|
|
356
360
|
|
|
357
361
|
A selection is made **among what the map shows**: features a query has filtered out are out of
|
|
358
362
|
reach, as a definition query puts them out of reach in ArcGIS Pro. Geoprocessing runs on a layer's
|
|
@@ -508,9 +512,16 @@ one-line entry in `env_layers.py`.
|
|
|
508
512
|
|
|
509
513
|
**Add Layer** on the *Data* group, or **drag a file onto the map**.
|
|
510
514
|
|
|
511
|
-
Supported: `.shp
|
|
512
|
-
`.
|
|
513
|
-
|
|
515
|
+
Supported: `.shp`, `.gpkg`, `.gdb`, `.geojson`/`.json`, `.fgb` (FlatGeobuf), `.parquet`
|
|
516
|
+
(GeoParquet, the format a Databricks table exports to), `.kml`, `.kmz`, `.gpx`, `.csv`, `.xlsx`.
|
|
517
|
+
|
|
518
|
+
**Atlas reads a file where it lives when it can find it**, so nothing is uploaded. It looks for
|
|
519
|
+
the file in the folders it already knows (where earlier layers and projects came from, and the
|
|
520
|
+
folders around where Atlas was started) and accepts it only if the name, size, modified time and
|
|
521
|
+
a hash of its first and last 256 KB all match. That is why a `.shp` dragged **on its own** still
|
|
522
|
+
gets its `.prj` and `.dbf`: they are read from beside the original. A layer found this way is
|
|
523
|
+
recorded as its own source, so a save references it instead of copying it. A file Atlas cannot
|
|
524
|
+
find is uploaded as before, and a lone `.shp` then needs its sidecars dragged with it.
|
|
514
525
|
|
|
515
526
|
A **GPX** or **KML** file declares several layers and usually leaves most of them empty — a GPX
|
|
516
527
|
file has waypoints, routes and tracks whether or not it holds any — so Atlas reads the first one
|
|
@@ -519,8 +530,11 @@ from a WKT column, including **EWKT** (`SRID=4326;POINT (...)`) as PostGIS and D
|
|
|
519
530
|
|
|
520
531
|
**From URL** (beside Add Layer) takes a web address: a file — GeoJSON, GeoPackage, GeoParquet,
|
|
521
532
|
FlatGeobuf, KML/KMZ, GPX, a zipped shapefile — or a **feature service**: an ArcGIS REST layer
|
|
522
|
-
(`…/FeatureServer/0` or `…/MapServer/0`), an OGC API Features collection or a WFS.
|
|
523
|
-
|
|
533
|
+
(`…/FeatureServer/0` or `…/MapServer/0`), an OGC API Features collection or a WFS. An ArcGIS
|
|
534
|
+
**group layer** loads as each of its sublayers, and a whole service (`…/MapServer`) as every
|
|
535
|
+
feature layer in it; an address ending in `/query` is read as its layer. ArcGIS pages are fetched by
|
|
536
|
+
object id, six at a time, three layers at once, and the toast counts features as they arrive.
|
|
537
|
+
Other services are paged by their own record limit; a file is downloaded once and kept in a
|
|
524
538
|
cache, and a file whose address names no format is recognised from its first bytes. Either way the
|
|
525
539
|
result is a file on disk, so it takes the house style like anything dragged in, and a saved project
|
|
526
540
|
reopens it without the network. Downloads are capped at 512 MB and services at 200,000 features.
|
|
@@ -848,9 +862,33 @@ cell size.
|
|
|
848
862
|
The **⋯ menu ▸ Attributes** on a layer row opens its table. You can sort, filter, freeze
|
|
849
863
|
columns, and reorder them.
|
|
850
864
|
|
|
865
|
+
Across the top, as in ArcGIS Pro:
|
|
866
|
+
|
|
867
|
+
| Control | Does |
|
|
868
|
+
|---|---|
|
|
869
|
+
| **All** / **Selected** | Two views of the table, each with its count. Selected is greyed when nothing is |
|
|
870
|
+
| **Select All**, **Switch**, **Clear**, **Zoom To** | The selection, for this table's layer only |
|
|
871
|
+
| **Add Field** | Opens Fields with the new-field row ready |
|
|
872
|
+
| **Calculate** | Calculate Field (below) |
|
|
873
|
+
|
|
874
|
+
### Calculate Field
|
|
875
|
+
|
|
876
|
+
Pick a field and write an expression, ArcGIS Pro's way: a field is `!NAME!` (click a field
|
|
877
|
+
under the box to insert it). Arithmetic, comparisons, `a if test else b`, f-strings, text
|
|
878
|
+
methods (`.upper()`, `.lower()`, `.title()`, `.strip()`, `.replace()`, `.zfill()` and a few
|
|
879
|
+
more) and `abs`, `round`, `min`, `max`, `len`, `str`, `int`, `float`, `sqrt`, `floor`, `ceil`,
|
|
880
|
+
`log`, `log10`, `exp` are available. Nothing else: an expression cannot import, open files or
|
|
881
|
+
reach anything that is not the row's values.
|
|
882
|
+
|
|
883
|
+
With a selection it calculates **only the selected records**, and says so; otherwise all of
|
|
884
|
+
them. **Preview** shows the first five results without changing anything. Every row is checked
|
|
885
|
+
against the field's type and domain first, and one that fails stops the whole calculation with
|
|
886
|
+
its row number. The result is staged as **one edit**, so one Undo puts it all back.
|
|
887
|
+
|
|
851
888
|
### Editing
|
|
852
889
|
|
|
853
|
-
Turn on **Edit** in the panel
|
|
890
|
+
Opening a table does not start editing. Turn on **Edit** in the panel (Add Field and Calculate
|
|
891
|
+
turn it on themselves). Then:
|
|
854
892
|
|
|
855
893
|
- **double-click a cell** to change a value
|
|
856
894
|
- **right-click a column** to rename it, change its type, or set a domain
|
|
@@ -1991,11 +2029,17 @@ naming the host it could not reach.
|
|
|
1991
2029
|
|
|
1992
2030
|
**Setup failed on GDAL.**
|
|
1993
2031
|
See *Install* at the top. Setup tries conda-forge then pip; if both failed,
|
|
1994
|
-
`conda install -c conda-forge gdal` by hand, then run setup again.
|
|
2032
|
+
`conda install -c conda-forge gdal` by hand, then run setup again. With no conda, or a conda that
|
|
2033
|
+
is itself broken, install GDAL with **pip** from Christoph Gohlke's Windows wheels
|
|
2034
|
+
(github.com/cgohlke/geospatial-wheels) into the skill's own `venv`; the skill's `INSTALL.md` has
|
|
2035
|
+
the one command.
|
|
1995
2036
|
|
|
1996
2037
|
**"DLL load failed while importing _gdal" (or any wheel failing to import).**
|
|
1997
|
-
|
|
1998
|
-
checks for it and prints the download link
|
|
2038
|
+
Read the end of the message. *The specified module could not be found* is the Microsoft C++
|
|
2039
|
+
runtime, not Python: setup checks for it and prints the download link, and installing it needs
|
|
2040
|
+
admin rights. *The specified procedure could not be found* is two GDAL builds mixed in one
|
|
2041
|
+
Python, usually after a `conda install` from another channel replaced GDAL's libraries. Do not
|
|
2042
|
+
repair that Python; use the pip route above in the skill's own `venv`.
|
|
1999
2043
|
|
|
2000
2044
|
**Colouring by a number does nothing, or everything comes out one colour.**
|
|
2001
2045
|
Fixed in 1.2, in both Unique Values and Graduated. On an earlier version, type the real min and
|
|
@@ -8,7 +8,7 @@ Databricks, runs GDAL-backed geoprocessing, translates between formats, and driv
|
|
|
8
8
|
the interactive map application (see the separate *Atlas* page).
|
|
9
9
|
|
|
10
10
|
It is designed so an AI assistant can use it reliably: one obvious way to do each thing,
|
|
11
|
-
results that report what happened, and errors that say what to fix. Version
|
|
11
|
+
results that report what happened, and errors that say what to fix. Version 2.0.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -313,7 +313,8 @@ r1, r2, r3 = await asyncio.gather(
|
|
|
313
313
|
```
|
|
314
314
|
|
|
315
315
|
Async variants exist for every operation: `clip_async`, `buffer_async`, `intersect_async`,
|
|
316
|
-
`union_async`, `dissolve_async`, `near_async`, `spatial_join_async`, `
|
|
316
|
+
`union_async`, `dissolve_async`, `near_async`, `spatial_join_async`, `erase_async`,
|
|
317
|
+
`simplify_async`, `morph_async`, and
|
|
317
318
|
`SpatialPipeline.run_async()`.
|
|
318
319
|
|
|
319
320
|
---
|