env-able 2.0.1__tar.gz → 2.1.0__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.1 → env_able-2.1.0}/PKG-INFO +9 -1
- {env_able-2.0.1 → env_able-2.1.0}/README.md +8 -0
- {env_able-2.0.1 → env_able-2.1.0}/pyproject.toml +1 -1
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/__init__.py +3 -2
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/calc.py +102 -5
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/docs/atlas-guide.md +88 -10
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/docs/env-able-library.md +11 -1
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/index.html +24879 -23961
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/server.py +13975 -13411
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/vector.py +137 -0
- {env_able-2.0.1 → env_able-2.1.0}/.gitignore +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/CHANGELOG.md +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/LICENSE +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/_gdal_env.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/_gdal_utils.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/_pipeline.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/_result.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/aggregate.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/__init__.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/aix.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/brandmark.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/claude_bridge.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/client.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/compliance.html +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/databricks.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/debug.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/desktop.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/docs/atlas-layout-view.md +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/docs/cartographic-judgment.md +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/edits.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/eirstyle.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/env_layers.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/landing.html +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/naming.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/palette.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/project.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/session.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/splash.html +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/templates/Summary A4 Landscape.pagx +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/templates/Summary A4 Portrait.pagx +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/templates/Title Bar A4 Landscape.pagx +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/templates/Title Bar A4 Portrait.pagx +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/HTML-TO-IMAGE-LICENSE.txt +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/MAPLIBRE-LICENSE.txt +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/PROTOMAPS-LICENSE.txt +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/ROBOTO-LICENSE.txt +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/html-to-image.js +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/maplibre-gl.css +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/maplibre-gl.js +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/pmtiles.js +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/protomaps-basemaps.js +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/roboto-latin-400-italic.woff2 +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/roboto-latin-400-normal.woff2 +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/roboto-latin-500-normal.woff2 +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/atlas/vendor/roboto-latin-700-normal.woff2 +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/classify.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/layout.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/morph.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/pull.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/remote.py +0 -0
- {env_able-2.0.1 → env_able-2.1.0}/src/env_able/stream.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: env-able
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: AI-ready GIS toolkit for energy and subsurface workflows.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -235,6 +235,14 @@ Lines and polygons with fewer vertices, no edge moved more than `tolerance`. Mea
|
|
|
235
235
|
result = env.Simplify("counties.shp", 200, "meters", output="counties_light.gpkg")
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
+
### `env.SummaryStatistics(input_table, statistics_fields, case_field=None, output=None, separator=", ")`
|
|
239
|
+
ArcGIS Pro's Summary Statistics: one row per unique set of `case_field` values, with each statistic asked for, as a **table** (no geometry). The input is a path, a DataFrame or a GeoDataFrame. The statistics are Dissolve's plus `MODE`; a text field takes only `MIN`, `MAX`, `COUNT`, `FIRST`, `LAST`, `UNIQUE`, `CONCATENATE` and `MODE`. Columns are named `SUM_EUR`, and blanks are left out of every statistic.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
result = env.SummaryStatistics("wells.gpkg", [["EUR", "SUM"], ["API", "COUNT"]], case_field="OPERATOR")
|
|
243
|
+
result.gdf # a pandas DataFrame, one row per operator
|
|
244
|
+
```
|
|
245
|
+
|
|
238
246
|
---
|
|
239
247
|
|
|
240
248
|
### `env.load(source)` — fluent spatial pipeline
|
|
@@ -209,6 +209,14 @@ Lines and polygons with fewer vertices, no edge moved more than `tolerance`. Mea
|
|
|
209
209
|
result = env.Simplify("counties.shp", 200, "meters", output="counties_light.gpkg")
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
+
### `env.SummaryStatistics(input_table, statistics_fields, case_field=None, output=None, separator=", ")`
|
|
213
|
+
ArcGIS Pro's Summary Statistics: one row per unique set of `case_field` values, with each statistic asked for, as a **table** (no geometry). The input is a path, a DataFrame or a GeoDataFrame. The statistics are Dissolve's plus `MODE`; a text field takes only `MIN`, `MAX`, `COUNT`, `FIRST`, `LAST`, `UNIQUE`, `CONCATENATE` and `MODE`. Columns are named `SUM_EUR`, and blanks are left out of every statistic.
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
result = env.SummaryStatistics("wells.gpkg", [["EUR", "SUM"], ["API", "COUNT"]], case_field="OPERATOR")
|
|
217
|
+
result.gdf # a pandas DataFrame, one row per operator
|
|
218
|
+
```
|
|
219
|
+
|
|
212
220
|
---
|
|
213
221
|
|
|
214
222
|
### `env.load(source)` — fluent spatial pipeline
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "2.0
|
|
1
|
+
__version__ = "2.1.0"
|
|
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
|
|
@@ -9,8 +9,9 @@ _gdal_env.configure()
|
|
|
9
9
|
|
|
10
10
|
from .vector import (
|
|
11
11
|
Clip, Buffer, Intersect, Union, Dissolve, Near, SpatialJoin, Erase, Simplify,
|
|
12
|
+
SummaryStatistics,
|
|
12
13
|
clip_async, buffer_async, intersect_async, union_async, dissolve_async,
|
|
13
|
-
near_async, spatial_join_async, erase_async, simplify_async,
|
|
14
|
+
near_async, spatial_join_async, erase_async, simplify_async, summary_statistics_async,
|
|
14
15
|
SJ_MATCH,
|
|
15
16
|
load,
|
|
16
17
|
)
|
|
@@ -5,8 +5,12 @@ Calculate Field: one expression, evaluated per row, with nothing else reachable.
|
|
|
5
5
|
calc.compile_expr("!NAME!.upper()", fields)
|
|
6
6
|
|
|
7
7
|
ArcGIS Pro's syntax, so an expression copied out of Pro works here: a field is written
|
|
8
|
-
`!FIELD!`.
|
|
9
|
-
type first.
|
|
8
|
+
`!FIELD!`. ArcMap's `[FIELD]` works too, because people still type it, and so does a bare name
|
|
9
|
+
when it is a field (`EUR * 1000`), because that is what people type first.
|
|
10
|
+
|
|
11
|
+
A null propagates: `!EUR! * 1000` on a row with no EUR gives no value rather than failing the
|
|
12
|
+
whole calculation on that row, and a comparison against a null is false. That is what a database
|
|
13
|
+
does, and it is what someone calculating a 30,000-row column with a few blanks in it expects.
|
|
10
14
|
|
|
11
15
|
It is Python's grammar, NOT Python. The expression is parsed with `ast` and walked by an
|
|
12
16
|
interpreter that knows a short list of node types; anything else -- attribute access other than
|
|
@@ -18,15 +22,50 @@ model may have been handed by a document it was asked to read.
|
|
|
18
22
|
from __future__ import annotations
|
|
19
23
|
|
|
20
24
|
import ast
|
|
25
|
+
import keyword
|
|
21
26
|
import math
|
|
22
27
|
import operator
|
|
23
28
|
import re
|
|
24
29
|
|
|
30
|
+
def _iif(test, a, b): # never called: ev() evaluates only the branch taken
|
|
31
|
+
return a if test else b
|
|
32
|
+
|
|
33
|
+
|
|
25
34
|
FUNCS = {
|
|
26
35
|
"abs": abs, "round": round, "min": min, "max": max, "len": len,
|
|
27
36
|
"str": str, "int": int, "float": float, "bool": bool,
|
|
28
37
|
"sqrt": math.sqrt, "floor": math.floor, "ceil": math.ceil, "log": math.log,
|
|
29
38
|
"log10": math.log10, "exp": math.exp,
|
|
39
|
+
# The function forms people know from ArcMap and from spreadsheets. iif is lazy, so
|
|
40
|
+
# iif(!N! != 0, 1 / !N!, 0) does not divide by zero on the rows it is guarding.
|
|
41
|
+
"iif": _iif, "upper": str.upper, "lower": str.lower, "trim": str.strip,
|
|
42
|
+
}
|
|
43
|
+
#: Functions that pass a null through rather than tripping over it. bool and iif decide what a
|
|
44
|
+
#: null means themselves, so they are not in it.
|
|
45
|
+
_NULL_SAFE = set(FUNCS) - {"bool", "iif"}
|
|
46
|
+
|
|
47
|
+
#: One line each, for the expression box's suggestions. Every function and method is listed;
|
|
48
|
+
#: verify.py checks the two agree, so a function cannot be added without its help.
|
|
49
|
+
HELP = {
|
|
50
|
+
"abs": ("abs(x)", "the value without its sign"),
|
|
51
|
+
"round": ("round(x, n)", "x to n decimal places"),
|
|
52
|
+
"min": ("min(a, b)", "the smaller value"),
|
|
53
|
+
"max": ("max(a, b)", "the larger value"),
|
|
54
|
+
"len": ("len(text)", "how many characters"),
|
|
55
|
+
"str": ("str(x)", "x as text"),
|
|
56
|
+
"int": ("int(x)", "x as a whole number"),
|
|
57
|
+
"float": ("float(x)", "x as a number"),
|
|
58
|
+
"bool": ("bool(x)", "true or false"),
|
|
59
|
+
"sqrt": ("sqrt(x)", "square root"),
|
|
60
|
+
"floor": ("floor(x)", "round down"),
|
|
61
|
+
"ceil": ("ceil(x)", "round up"),
|
|
62
|
+
"log": ("log(x)", "natural logarithm"),
|
|
63
|
+
"log10": ("log10(x)", "base-10 logarithm"),
|
|
64
|
+
"exp": ("exp(x)", "e to the power x"),
|
|
65
|
+
"iif": ("iif(test, a, b)", "a when test is true, otherwise b"),
|
|
66
|
+
"upper": ("upper(text)", "capitals"),
|
|
67
|
+
"lower": ("lower(text)", "lower case"),
|
|
68
|
+
"trim": ("trim(text)", "without leading and trailing spaces"),
|
|
30
69
|
}
|
|
31
70
|
#: Methods a string may be asked for. Each returns a string, a bool or an int, so a chain of
|
|
32
71
|
#: them cannot reach anything that is not data.
|
|
@@ -79,6 +118,26 @@ def compile_expr(expr: str, fields):
|
|
|
79
118
|
return slots[real]
|
|
80
119
|
|
|
81
120
|
src = re.sub(r"!([^!]+)!", sub, src)
|
|
121
|
+
|
|
122
|
+
def sub_bracket(m):
|
|
123
|
+
# [NAME] is a field only when NAME is one, and only where a value could start: after a
|
|
124
|
+
# name, a closing bracket or a quote it is a subscript (!NAME![0], !NAME![:3]), which
|
|
125
|
+
# is Python's and stays Python's.
|
|
126
|
+
before = src[:m.start()].rstrip()
|
|
127
|
+
word = re.search(r"[A-Za-z_]\w*$", before)
|
|
128
|
+
# ...except after a keyword: `not [ID] == 3` is a value, though `not` ends in a letter.
|
|
129
|
+
if before and (before[-1].isalnum() or before[-1] in "_)]'\"") and not (
|
|
130
|
+
word and keyword.iskeyword(word.group(0))):
|
|
131
|
+
return m.group(0)
|
|
132
|
+
name = m.group(1).strip()
|
|
133
|
+
real = name if name in fields else lookup.get(name.lower())
|
|
134
|
+
if real is None:
|
|
135
|
+
return m.group(0)
|
|
136
|
+
if real not in slots:
|
|
137
|
+
slots[real] = _field_token(len(slots))
|
|
138
|
+
return slots[real]
|
|
139
|
+
|
|
140
|
+
src = re.sub(r"\[([^\[\]]+)\]", sub_bracket, src)
|
|
82
141
|
try:
|
|
83
142
|
tree = ast.parse(src, mode="eval")
|
|
84
143
|
except SyntaxError as e:
|
|
@@ -95,6 +154,12 @@ def compile_expr(expr: str, fields):
|
|
|
95
154
|
if n.id in fields or n.id.lower() in lookup:
|
|
96
155
|
continue
|
|
97
156
|
raise CalcError("%r is not a field or a function. Write a field as !NAME!." % n.id)
|
|
157
|
+
elif isinstance(n, ast.List):
|
|
158
|
+
# What a [NAME] that matched no field parses as. Saying "List is not allowed"
|
|
159
|
+
# about a typo in a field name would send somebody looking in the wrong place.
|
|
160
|
+
if len(n.elts) == 1 and isinstance(n.elts[0], ast.Name):
|
|
161
|
+
raise CalcError("[%s] is not a field in this layer." % n.elts[0].id)
|
|
162
|
+
raise CalcError("A list is not allowed in a field calculation.")
|
|
98
163
|
elif isinstance(n, ast.Attribute):
|
|
99
164
|
if n.attr not in STR_METHODS:
|
|
100
165
|
raise CalcError(".%s is not allowed; string methods are: %s"
|
|
@@ -133,6 +198,8 @@ def compile_expr(expr: str, fields):
|
|
|
133
198
|
return row.get(n.id if n.id in fields else lookup[n.id.lower()])
|
|
134
199
|
if t is ast.BinOp:
|
|
135
200
|
a, b = ev(n.left, row), ev(n.right, row)
|
|
201
|
+
if a is None or b is None:
|
|
202
|
+
return None
|
|
136
203
|
if isinstance(n.op, ast.Pow) and isinstance(b, (int, float)) and abs(b) > _MAX_POW:
|
|
137
204
|
raise CalcError("An exponent over %d is refused." % _MAX_POW)
|
|
138
205
|
if isinstance(n.op, ast.Mult) and (isinstance(a, str) or isinstance(b, str)):
|
|
@@ -141,7 +208,10 @@ def compile_expr(expr: str, fields):
|
|
|
141
208
|
raise CalcError("That would make a string over %d characters." % _MAX_LEN)
|
|
142
209
|
return _BIN[type(n.op)](a, b)
|
|
143
210
|
if t is ast.UnaryOp:
|
|
144
|
-
|
|
211
|
+
v = ev(n.operand, row)
|
|
212
|
+
if v is None and not isinstance(n.op, ast.Not):
|
|
213
|
+
return None
|
|
214
|
+
return _UN[type(n.op)](v)
|
|
145
215
|
if t is ast.BoolOp:
|
|
146
216
|
if isinstance(n.op, ast.And):
|
|
147
217
|
v = True
|
|
@@ -160,6 +230,10 @@ def compile_expr(expr: str, fields):
|
|
|
160
230
|
left = ev(n.left, row)
|
|
161
231
|
for op, c in zip(n.ops, n.comparators):
|
|
162
232
|
right = ev(c, row)
|
|
233
|
+
# Only ==, != and `is` have an answer about a null; anything else is false.
|
|
234
|
+
if (left is None or right is None) and not isinstance(
|
|
235
|
+
op, (ast.Eq, ast.NotEq, ast.Is, ast.IsNot)):
|
|
236
|
+
return False
|
|
163
237
|
if not _CMP[type(op)](left, right):
|
|
164
238
|
return False
|
|
165
239
|
left = right
|
|
@@ -169,6 +243,8 @@ def compile_expr(expr: str, fields):
|
|
|
169
243
|
if t is ast.Call:
|
|
170
244
|
if isinstance(n.func, ast.Attribute):
|
|
171
245
|
obj = ev(n.func.value, row)
|
|
246
|
+
if obj is None:
|
|
247
|
+
return None
|
|
172
248
|
if not isinstance(obj, str):
|
|
173
249
|
raise CalcError(".%s() needs text, and this row has %r"
|
|
174
250
|
% (n.func.attr, obj))
|
|
@@ -178,11 +254,25 @@ def compile_expr(expr: str, fields):
|
|
|
178
254
|
isinstance(a, int) and a > _MAX_LEN for a in args):
|
|
179
255
|
raise CalcError("A width over %d is refused." % _MAX_LEN)
|
|
180
256
|
return getattr(obj, n.func.attr)(*args)
|
|
181
|
-
|
|
257
|
+
fname = n.func.id
|
|
258
|
+
if fname == "iif":
|
|
259
|
+
if len(n.args) != 3:
|
|
260
|
+
raise CalcError("iif() takes three values: iif(test, a, b).")
|
|
261
|
+
return ev(n.args[1], row) if ev(n.args[0], row) else ev(n.args[2], row)
|
|
262
|
+
args = [ev(a, row) for a in n.args]
|
|
263
|
+
if fname in _NULL_SAFE and any(a is None for a in args):
|
|
264
|
+
return None
|
|
265
|
+
if fname in ("upper", "lower", "trim"):
|
|
266
|
+
if len(args) != 1:
|
|
267
|
+
raise CalcError("%s() takes one value." % fname)
|
|
268
|
+
return FUNCS[fname](str(args[0]))
|
|
269
|
+
return FUNCS[fname](*args)
|
|
182
270
|
if t is ast.Attribute:
|
|
183
271
|
raise CalcError(".%s must be called, as .%s()" % (n.attr, n.attr))
|
|
184
272
|
if t is ast.Subscript:
|
|
185
273
|
obj = ev(n.value, row)
|
|
274
|
+
if obj is None:
|
|
275
|
+
return None
|
|
186
276
|
s = n.slice
|
|
187
277
|
if type(s) is ast.Slice:
|
|
188
278
|
return obj[slice(*(ev(x, row) if x is not None else None
|
|
@@ -191,9 +281,16 @@ def compile_expr(expr: str, fields):
|
|
|
191
281
|
if t is ast.Tuple:
|
|
192
282
|
return tuple(ev(x, row) for x in n.elts)
|
|
193
283
|
if t is ast.JoinedStr:
|
|
194
|
-
|
|
284
|
+
# A null anywhere makes the whole string null, as text concatenation does in a
|
|
285
|
+
# database, rather than writing the word "None" into somebody's labels.
|
|
286
|
+
parts = [ev(v, row) for v in n.values]
|
|
287
|
+
if any(p is None for p in parts):
|
|
288
|
+
return None
|
|
289
|
+
return "".join(str(p) for p in parts)
|
|
195
290
|
if t is ast.FormattedValue:
|
|
196
291
|
v = ev(n.value, row)
|
|
292
|
+
if v is None:
|
|
293
|
+
return None
|
|
197
294
|
spec = ev(n.format_spec, row) if n.format_spec is not None else ""
|
|
198
295
|
if any(int(d) > _MAX_LEN for d in re.findall(r"\d+", spec)):
|
|
199
296
|
raise CalcError("A width over %d is refused." % _MAX_LEN)
|
|
@@ -245,6 +245,11 @@ Draw order, top to bottom — drag to reorder. Each row has:
|
|
|
245
245
|
|
|
246
246
|
**Right-clicking the row opens that same menu.** Either gesture, same menu.
|
|
247
247
|
|
|
248
|
+
**Duplicate**, in that menu, copies the layer as it is now, unsaved edits included, as
|
|
249
|
+
"*name* copy", then "*name* copy (2)". It keeps the look and filter and takes a colour of its own,
|
|
250
|
+
and an edit to one does not reach the other. Nothing is written to disk: a save keeps the copy in
|
|
251
|
+
the project, and Morph or Save As makes a file of it. **Undo** takes it away again.
|
|
252
|
+
|
|
248
253
|
A **bar beside the *Layers* heading** runs while geometry is coming in — opening a project, a
|
|
249
254
|
layer arriving, or the full-resolution swap a layout asks for. Hover it and it says what it is
|
|
250
255
|
waiting for. Indeterminate, because none of that reports progress.
|
|
@@ -642,8 +647,8 @@ from 339.8 MB to 53.6 MB. A narrower layer is sent exactly as before.
|
|
|
642
647
|
The colour chip on a layer row opens the picker. For polygons it has **Fill** and **Outline**
|
|
643
648
|
tabs.
|
|
644
649
|
|
|
645
|
-
**A layer's look travels.** Right-click ▸ **Save
|
|
646
|
-
colour, outline, opacity, symbology, labels, without the data — and **Apply
|
|
650
|
+
**A layer's look travels.** Right-click ▸ **Save Style** writes it as an `.atlasstyle` file —
|
|
651
|
+
colour, outline, opacity, symbology, labels, without the data — and **Apply Style…** puts one on
|
|
647
652
|
another layer; the *Symbology* tab's **Styles** group does the same for the layer picked there.
|
|
648
653
|
That is how a client's operator colours carry from one map to the next: the same
|
|
649
654
|
operator takes the same colour. Field names are matched against the new layer's own columns, as
|
|
@@ -845,9 +850,9 @@ picked in that tab: a label field (or *No labels*), **View Unplaced**, and **Con
|
|
|
845
850
|
drawn whatever collides. It is how one label is moved or reworded for print without touching the
|
|
846
851
|
data. The source layer stops labelling, so nothing prints twice; `Ctrl+Z` puts it back.
|
|
847
852
|
|
|
848
|
-
### Hex
|
|
853
|
+
### Hex Bins
|
|
849
854
|
|
|
850
|
-
**Right-click a point layer ▸ Hex
|
|
855
|
+
**Right-click a point layer ▸ Hex Bins** counts its points in hexagons and colours each by how many
|
|
851
856
|
it holds — a new layer, in natural-breaks classes, drawn just above the points, which are hidden
|
|
852
857
|
underneath. At basin scale 30,000 laterals print as solid colour and a heatmap has no legend a
|
|
853
858
|
reader can look a number up in; bins have both. Cells are about 40 across the data by default,
|
|
@@ -871,14 +876,32 @@ Across the top, as in ArcGIS Pro:
|
|
|
871
876
|
| **Add Field** | Opens Fields with the new-field row ready |
|
|
872
877
|
| **Calculate** | Calculate Field (below) |
|
|
873
878
|
|
|
879
|
+
**Click a row to select its feature**, as in Pro: **Ctrl+click** adds or removes one row and
|
|
880
|
+
**Shift+click** selects the run from the last row clicked, in the order the table shows. The
|
|
881
|
+
map draws the selection at once.
|
|
882
|
+
|
|
874
883
|
### Calculate Field
|
|
875
884
|
|
|
876
885
|
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)
|
|
878
|
-
methods (`.upper()`, `.lower()`, `.title()`, `.strip()`,
|
|
879
|
-
more) and `abs`, `round`, `min`, `max`, `len`, `str`, `int`,
|
|
880
|
-
`
|
|
881
|
-
|
|
886
|
+
under the box to insert it), and ArcMap's `[NAME]` works too. Arithmetic, comparisons,
|
|
887
|
+
`a if test else b`, f-strings, text methods (`.upper()`, `.lower()`, `.title()`, `.strip()`,
|
|
888
|
+
`.replace()`, `.zfill()` and a few more) and `abs`, `round`, `min`, `max`, `len`, `str`, `int`,
|
|
889
|
+
`float`, `bool`, `sqrt`, `floor`, `ceil`, `log`, `log10`, `exp`, `iif`, `upper`, `lower`, `trim` are
|
|
890
|
+
available. `iif(test, a, b)` only works out the side it takes, so
|
|
891
|
+
`iif(!N! != 0, 100 / !N!, 0)` never divides by zero. Nothing else: an expression cannot import,
|
|
892
|
+
open files or reach anything that is not the row's values.
|
|
893
|
+
|
|
894
|
+
**Suggestions** appear as you type: a few letters offer the functions and fields that start
|
|
895
|
+
with them, `!` or `[` offers the fields, and a `.` after a value offers the text methods. **Tab**
|
|
896
|
+
or **Enter** takes the highlighted one, the arrow keys move, **Esc** closes the list.
|
|
897
|
+
|
|
898
|
+
**A blank value stays blank.** `!EUR! * 1000` on a row with no EUR gives no value rather than
|
|
899
|
+
stopping the calculation, a comparison with a blank is false, and an f-string with a blank in it
|
|
900
|
+
is blank.
|
|
901
|
+
|
|
902
|
+
**A new field**: choose **+ New field…** at the bottom of the field list, name it and pick its
|
|
903
|
+
type, or *Type from the result* to have Atlas work the type out from what the expression gives.
|
|
904
|
+
The field is created as part of the same edit, so one Undo takes the column away again.
|
|
882
905
|
|
|
883
906
|
With a selection it calculates **only the selected records**, and says so; otherwise all of
|
|
884
907
|
them. **Preview** shows the first five results without changing anything. Every row is checked
|
|
@@ -898,9 +921,45 @@ Edits are **staged**, not written. The count of unsaved changes is shown, and:
|
|
|
898
921
|
|
|
899
922
|
- **Save** writes back to the source file (only offered when there is a file to write to)
|
|
900
923
|
- **Save As** writes a new GeoPackage
|
|
901
|
-
- **Undo** / **Discard** roll back
|
|
924
|
+
- **Undo** / **Redo** / **Discard** roll back, and forward again. Redo is offered until the
|
|
925
|
+
next new edit
|
|
902
926
|
- **Restore** recovers from the backup Atlas takes before its first write
|
|
903
927
|
|
|
928
|
+
### Edit Vertices
|
|
929
|
+
|
|
930
|
+
**Tools ▸ Edit ▸ Vertices** reshapes features. A panel opens at the top right of the map: pick the
|
|
931
|
+
layer, then click a feature. With one feature already selected, the tool opens on it, as Pro's
|
|
932
|
+
does. The feature's vertices show as white handles with a smaller handle at the middle of each
|
|
933
|
+
edge. Then:
|
|
934
|
+
|
|
935
|
+
| To | Do |
|
|
936
|
+
|---|---|
|
|
937
|
+
| move a vertex | drag it |
|
|
938
|
+
| add a vertex | drag an edge's middle handle |
|
|
939
|
+
| delete a vertex | click it, then **Delete Vertex** or the **Delete** key |
|
|
940
|
+
| undo, redo | the panel's arrows, or **Ctrl+Z** / **Ctrl+Y** |
|
|
941
|
+
| put it all back | **Discard**, after a confirm: the shape edits on this layer go, attribute edits stay |
|
|
942
|
+
| stop editing | **✕** at the top right, or **Esc**. Unsaved edits are kept, to save or undo later |
|
|
943
|
+
|
|
944
|
+
**Snapping** lands a dragged vertex on another feature's vertex, or failing that on its nearest
|
|
945
|
+
line, within 12 pixels. Both are on by default and toggle in the panel. It snaps to the real
|
|
946
|
+
vertex even where the map is drawing a simplified copy of a large layer.
|
|
947
|
+
|
|
948
|
+
Every change is a staged edit, like a cell edit, so the attribute table's Undo, Save and Discard
|
|
949
|
+
cover it too. A change that would make a polygon cross itself is refused with the reason, a ring
|
|
950
|
+
keeps three corners and a line two, and a point can only move. **Save** writes to the layer's
|
|
951
|
+
file after a backup; on a layer with no file (a drawn AOI, a query result) it keeps the edits in
|
|
952
|
+
the project instead. A format that stores no geometry (CSV, Excel) cannot be reshaped: Save As a
|
|
953
|
+
GeoPackage first.
|
|
954
|
+
|
|
955
|
+
### Layer Properties
|
|
956
|
+
|
|
957
|
+
**Right-click a layer ▸ Properties** shows where it came from and what it is, as Pro's Layer
|
|
958
|
+
Properties ▸ Source does: the file, feature class, format, size and date; the coordinate system
|
|
959
|
+
the file is in, beside the one Atlas holds it in (WGS 84) and draws it in (Web Mercator); the
|
|
960
|
+
feature count, geometry types, vertices and extent; and every field with its type and how many
|
|
961
|
+
are blank. A layer with no file says so.
|
|
962
|
+
|
|
904
963
|
### Layers with no file behind them
|
|
905
964
|
|
|
906
965
|
A layer pulled from Databricks is a **query result**, not a file — there is nothing to write
|
|
@@ -926,6 +985,7 @@ rather than failing silently.
|
|
|
926
985
|
| **Near** | How far is every feature from the nearest feature of another layer: adds a `dist_<layer>_<unit>` column, empty past an optional limit |
|
|
927
986
|
| **Erase** | This layer, with another layer's polygons cut out of it. Clip's opposite |
|
|
928
987
|
| **Simplify** | Lines and polygons with fewer vertices, none moved more than the tolerance; points are refused |
|
|
988
|
+
| **Summary Statistics** | Totals, averages and counts for each group, as a **table** in the Tables panel, not a layer |
|
|
929
989
|
|
|
930
990
|
**History** (beside the tools) lists this session's runs, newest first. Picking one opens its tool
|
|
931
991
|
with the same settings filled in and stops, so you can check them before pressing Run.
|
|
@@ -983,6 +1043,20 @@ Matches `arcpy.management.Dissolve`.
|
|
|
983
1043
|
end to end and gives each run its own feature, so two unconnected roads called "Main" stay
|
|
984
1044
|
two features.
|
|
985
1045
|
|
|
1046
|
+
### Summary Statistics
|
|
1047
|
+
|
|
1048
|
+
Matches `arcpy.analysis.Statistics`. The input is a layer or a table, and the output is a
|
|
1049
|
+
**table**: it lands in the Tables panel, since it has no shapes to draw.
|
|
1050
|
+
|
|
1051
|
+
- **Group By** — the fields that split the rows into groups, picked as Dissolve's fields are.
|
|
1052
|
+
Tick none, *(Nothing)*, for a single row covering everything. A blank in a Group By field is a
|
|
1053
|
+
group of its own, and the result says how many rows it holds.
|
|
1054
|
+
- **Statistics** — field + statistic pairs: Dissolve's thirteen plus `MODE`, the commonest value
|
|
1055
|
+
(the lowest, on a tie). A text field is offered only what Pro allows text: `MIN`, `MAX`,
|
|
1056
|
+
`COUNT`, `FIRST`, `LAST`, `UNIQUE`, `CONCATENATE`, `MODE`.
|
|
1057
|
+
- Columns are named `SUM_EUR`, `MODE_OPERATOR` and so on. Blanks are left out of every statistic,
|
|
1058
|
+
and there is no `FREQUENCY` column: `COUNT` on a field with no blanks counts each group's rows.
|
|
1059
|
+
|
|
986
1060
|
---
|
|
987
1061
|
|
|
988
1062
|
## The Catalog pane
|
|
@@ -1021,6 +1095,10 @@ A container offers what you can **create** in it. An item offers what you can **
|
|
|
1021
1095
|
| a map | Open, Rename…, Delete Map |
|
|
1022
1096
|
| a layout | Open, Rename…, Delete Layout |
|
|
1023
1097
|
| a feature class or dataset | Add To Current Map, Copy Path |
|
|
1098
|
+
|
|
1099
|
+
**A click selects a dataset; a double-click adds it to the map**, as do dragging it onto the map
|
|
1100
|
+
and *Add To Current Map*. A single click used to add it, which put layers on the map that nobody
|
|
1101
|
+
meant to add.
|
|
1024
1102
|
| a geodatabase or folder | Add All Layers To Map, Copy Path, Refresh, Remove From Project |
|
|
1025
1103
|
|
|
1026
1104
|
An **empty geodatabase registers**. Registering is not loading, and a `.gdb` you point Atlas at
|
|
@@ -280,6 +280,16 @@ Fewer vertices on lines and polygons, no edge moved more than `tolerance`, measu
|
|
|
280
280
|
so the distance is true on the ground. Topology is preserved. Points are refused, and the result's
|
|
281
281
|
warnings say how many vertices went.
|
|
282
282
|
|
|
283
|
+
### `env.SummaryStatistics(table, statistics_fields, case_field=None, output=None, separator=", ")`
|
|
284
|
+
|
|
285
|
+
ArcGIS Pro's Summary Statistics: one row per unique set of `case_field` values, with each
|
|
286
|
+
statistic asked for, as a **table** with no geometry. `table` is a path, a DataFrame or a
|
|
287
|
+
GeoDataFrame, whose geometry is ignored. `statistics_fields` is `[[field, statistic], ...]`, for
|
|
288
|
+
example `[["EUR", "SUM"], ["OPERATOR", "MODE"]]`; the statistics are `SUMMARY_STATISTICS`
|
|
289
|
+
(Dissolve's plus `MODE`), and a text field takes only `SUMMARY_TEXT_STATISTICS`. Leave
|
|
290
|
+
`case_field` out for one row. Columns are named `SUM_EUR`; blanks are left out of every statistic,
|
|
291
|
+
and a blank case value is a group of its own, counted in the warnings.
|
|
292
|
+
|
|
283
293
|
---
|
|
284
294
|
|
|
285
295
|
## Pipelines and async
|
|
@@ -563,7 +573,7 @@ draws no text at all with no error.
|
|
|
563
573
|
`rest_external_links_frames`, `connector_arrow_frames`
|
|
564
574
|
|
|
565
575
|
**Geoprocessing** — `Clip`, `Buffer`, `Intersect`, `Union`, `Dissolve`, `SpatialJoin`, `Near`,
|
|
566
|
-
`Erase`, `Simplify`, plus `*_async` for each, `SJ_MATCH`
|
|
576
|
+
`Erase`, `Simplify`, `SummaryStatistics`, plus `*_async` for each, `SJ_MATCH`
|
|
567
577
|
|
|
568
578
|
**Pipeline** — `load` → `SpatialPipeline` → `.run()` / `.run_async()`
|
|
569
579
|
|