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.
Files changed (62) hide show
  1. {env_able-2.0.0 → env_able-2.0.1}/.gitignore +1 -0
  2. {env_able-2.0.0 → env_able-2.0.1}/CHANGELOG.md +35 -0
  3. {env_able-2.0.0 → env_able-2.0.1}/PKG-INFO +36 -1
  4. {env_able-2.0.0 → env_able-2.0.1}/README.md +35 -0
  5. {env_able-2.0.0 → env_able-2.0.1}/pyproject.toml +1 -1
  6. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/__init__.py +1 -1
  7. env_able-2.0.1/src/env_able/atlas/calc.py +211 -0
  8. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/docs/atlas-guide.md +58 -14
  9. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/docs/env-able-library.md +3 -2
  10. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/index.html +404 -48
  11. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/server.py +420 -60
  12. env_able-2.0.1/src/env_able/remote.py +326 -0
  13. env_able-2.0.0/src/env_able/remote.py +0 -203
  14. {env_able-2.0.0 → env_able-2.0.1}/LICENSE +0 -0
  15. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/_gdal_env.py +0 -0
  16. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/_gdal_utils.py +0 -0
  17. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/_pipeline.py +0 -0
  18. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/_result.py +0 -0
  19. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/aggregate.py +0 -0
  20. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/__init__.py +0 -0
  21. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/aix.py +0 -0
  22. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/brandmark.py +0 -0
  23. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/claude_bridge.py +0 -0
  24. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/client.py +0 -0
  25. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/compliance.html +0 -0
  26. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/databricks.py +0 -0
  27. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/debug.py +0 -0
  28. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/desktop.py +0 -0
  29. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/docs/atlas-layout-view.md +0 -0
  30. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/docs/cartographic-judgment.md +0 -0
  31. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/edits.py +0 -0
  32. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/eirstyle.py +0 -0
  33. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/env_layers.py +0 -0
  34. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/landing.html +0 -0
  35. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/naming.py +0 -0
  36. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/palette.py +0 -0
  37. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/project.py +0 -0
  38. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/session.py +0 -0
  39. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/splash.html +0 -0
  40. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/templates/Summary A4 Landscape.pagx +0 -0
  41. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/templates/Summary A4 Portrait.pagx +0 -0
  42. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/templates/Title Bar A4 Landscape.pagx +0 -0
  43. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/templates/Title Bar A4 Portrait.pagx +0 -0
  44. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/HTML-TO-IMAGE-LICENSE.txt +0 -0
  45. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/MAPLIBRE-LICENSE.txt +0 -0
  46. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/PROTOMAPS-LICENSE.txt +0 -0
  47. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/ROBOTO-LICENSE.txt +0 -0
  48. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/html-to-image.js +0 -0
  49. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/maplibre-gl.css +0 -0
  50. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/maplibre-gl.js +0 -0
  51. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/pmtiles.js +0 -0
  52. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/protomaps-basemaps.js +0 -0
  53. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/roboto-latin-400-italic.woff2 +0 -0
  54. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/roboto-latin-400-normal.woff2 +0 -0
  55. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/roboto-latin-500-normal.woff2 +0 -0
  56. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/atlas/vendor/roboto-latin-700-normal.woff2 +0 -0
  57. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/classify.py +0 -0
  58. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/layout.py +0 -0
  59. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/morph.py +0 -0
  60. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/pull.py +0 -0
  61. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/stream.py +0 -0
  62. {env_able-2.0.0 → env_able-2.0.1}/src/env_able/vector.py +0 -0
@@ -172,3 +172,4 @@ tests/*
172
172
  # ~175 MB of it. Only the README that says what each scenario is for is kept.
173
173
  tests/maps/*
174
174
  !tests/maps/README.md
175
+ /incubator
@@ -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.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "env-able"
7
- version = "2.0.0"
7
+ version = "2.0.1"
8
8
  description = "AI-ready GIS toolkit for energy and subsurface workflows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -1,4 +1,4 @@
1
- __version__ = "2.0.0"
1
+ __version__ = "2.0.1"
2
2
 
3
3
  # Resolve GDAL_DATA / PROJ_LIB before anything imports osgeo. IDEs and bare
4
4
  # `python script.py` runs skip conda's activation scripts, which is where those
@@ -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 1.10.
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. The dropdown beside it decides how you pick:
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** | Click a feature to select the topmost one under the cursor. Panning still works |
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 showing only the selected rows. **Selected Only** in the table turns that on and off |
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` (drag the whole folder or pick the `.shp` — Atlas needs the sidecar files),
512
- `.gpkg`, `.gdb`, `.geojson`/`.json`, `.fgb` (FlatGeobuf), `.parquet` (GeoParquet, the format a
513
- Databricks table exports to), `.kml`, `.kmz`, `.gpx`, `.csv`, `.xlsx`.
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. A service is read
523
- the way it is designed to be, paged by its own record limit; a file is downloaded once and kept in a
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. Then:
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
- The Microsoft C++ runtime, not Python. Nothing in that message says so, which is why setup
1998
- checks for it and prints the download link. Installing it needs admin rights.
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 1.10.
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`, `morph_async`, and
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
  ---