env-able
An open source spatial analysis library built for AI-driven GIS workflows. Designed to give AI systems like Claude reliable, hallucination-free tools for spatial operations in energy and subsurface contexts.
Install
pip install env-able
# with Databricks support
pip install env-able[databricks]
# with interactive map server (Atlas)
pip install env-able[atlas]
# with large-scale streaming pulls (DuckDB)
pip install env-able[fast]
Usage
import env_able as env
Functions
env.pull(table, output=None, wkt_col=None, chunk_size=None, crs="EPSG:4326")
Pull a Databricks table or query to a local file, chunking around the row/byte limit automatically.
Databricks caps result sets (~4 096 rows for narrow tables, fewer when WKT columns are present). pull() paginates with LIMIT/OFFSET, assembles the complete dataset in memory, and writes it to disk in the requested format.
Parameters
table— fully-qualified table name (catalog.schema.table) or a completeSELECTqueryoutput— destination file path; format inferred from extension (.gpkg,.geojson,.shp,.parquet,.csv,.xlsx). Omit to return a GeoDataFrame or DataFrame.wkt_col— column containing WKT geometry strings. Auto-detected if omitted.chunk_size— rows per Databricks query. Defaults to 512 (WKT) or 4 096 (tabular). Reduce if you hit payload errors on wide tables.crs— CRS to assign geometry. DefaultEPSG:4326.
Environment variables required
DATABRICKS_HOST # https://adb-<workspace-id>.azuredatabricks.net
DATABRICKS_TOKEN # personal access token
DATABRICKS_HTTP_PATH # /sql/1.0/warehouses/<warehouse-id>
import env_able as env
# pull a full table to GeoPackage
env.pull("catalog.schema.wells", "wells.gpkg")
# pull with a filter query
env.pull("SELECT * FROM catalog.schema.wells WHERE state = 'TX'", "wells_tx.gpkg")
# pull tabular (no geometry)
env.pull("catalog.schema.formations", "formations.csv")
# return in-memory without writing
gdf = env.pull("catalog.schema.leases", wkt_col="geom_wkt", crs="EPSG:4269")
env.Clip(input_layer, clip_layer, output=None, where=None, preview=False)
Clips input features to the extent of a polygon boundary (ogr2ogr -clipsrc engine — streaming, handles files larger than RAM). Returns a SpatialResult.
input_layer— point, line, or polygon (file path or GeoDataFrame)clip_layer— polygon clip boundary (file path or GeoDataFrame)output— output file path (.gpkg,.shp,.geojson, etc.)where— optional SQL attribute filter applied before clipping, e.g."STATE = 'TX'"preview— ifTrue, pushes result to a running Atlas server on completion
result = env.Clip("wells.shp", "texas.gpkg", output="wells_tx.gpkg")
result = env.Clip(gdf, "counties.gpkg", where="STATE = 'TX'")
result.preview("Texas Wells", color="#F68D2E") # push to Atlas
result.to("wells_tx.geojson") # write extra format
print(result) # stats: input, output, dropped, CRS, time
gdf = result.gdf # access as GeoDataFrame
env.Buffer(input_layer, distance, unit="meters", output=None, where=None, preview=False)
Buffers input features by a given distance (OGR Python API engine). Auto-selects the best UTM zone for accurate metric distances; result is returned in the original CRS. Returns a SpatialResult.
input_layer— point, line, or polygon (file path or GeoDataFrame)distance— numeric buffer distanceunit—meters,km,miles,feet,usfeet,nautical milesoutput— output file pathwhere— optional SQL attribute filterpreview— ifTrue, pushes result to Atlas on completion
result = env.Buffer("wells.shp", 1, "miles", output="wells_1mi.gpkg")
result = env.Buffer(gdf, 500) # 500 m, no file written
print(result) # SpatialResult stats
env.Intersect(input_layer, intersect_layer, output=None, where=None, preview=False)
Geometric intersection — only what the layers share, with attributes from all of them (OGR Layer.Intersection() engine). Returns a SpatialResult.
input_layer— point, line, or polygon (file path or GeoDataFrame)intersect_layer— one layer or a list. A list is folded pairwise, which is exactly the N-way intersection: what the input shares with the first, then what that shares with the second, and so onoutput— output file pathwhere— optional SQL attribute filter on the input layerpreview— ifTrue, pushes result to Atlas on completion
result = env.Intersect("wells.shp", "permits.gpkg", output="wells_permits.gpkg")
result = env.Intersect("wells.shp", ["leases.gpkg", "counties.gpkg"]) # three-way
result.preview("Permitted Wells")
If one layer leaves nothing, the fold stops and the warning names it — with four inputs, "returned 0 features" is not a diagnosis.
env.Union(input_layer, union_layer, output=None, join_attributes="ALL", gaps=True, cluster_tolerance=None, where=None, preview=False)
Everything from every layer, cut where they overlap (OGR Layer.Union() engine). Polygons. Mirrors arcpy.analysis.Union. Returns a SpatialResult.
Where Intersect answers "what do these share?", Union answers "what is covered by either, and by which?" — so the output usually has more features than the inputs, because each overlap becomes its own piece.
union_layer— one layer or a list, folded pairwise. Each input keeps its ownFID_column through the whole foldjoin_attributes—"ALL"(everything, plus aFID_<layer>per input),"NO_FID"(everything except those),"ONLY_FID"(just those)gaps—False(ArcGISNO_GAPS) gives each enclosed void a feature of its own, with everyFID_at-1, so gaps can be selected and inspectedcluster_tolerance— snap coordinates within this distance together, as a GEOS precision gridwhere— SQL attribute filter on the input, applied before the overlay
result = env.Union("leases.gpkg", "units.gpkg", output="lease_units.gpkg",
join_attributes="ONLY_FID")
result = env.Union("a.shp", ["b.shp", "c.shp", "d.shp"]) # four layers, one call
result = env.Union(a, b, gaps=False) # a feature per enclosed void
FID_<layer> is the source feature's id, or -1 where that layer contributed nothing to the piece — which is how you tell a lease-only sliver from a genuine overlap. Non-overlapping inputs are valid (the answer is the layers side by side) and warn, naming the layer that misses, because that usually means a CRS mistake.
env.Dissolve(input_layer, output=None, dissolve_field=None, statistics_fields=None, multi_part=True, unsplit_lines=False, where=None, preview=False)
Merge features that share a value, and summarise the rest. Mirrors arcpy.management.Dissolve. Returns a SpatialResult.
dissolve_field— field name or list of them. Omit it and every feature merges into one, which is the usual way to turn a pile of parcels into an outlinestatistics_fields—[[field, stat], ...].SUM,MEAN,MIN,MAX,RANGE,STD,COUNT,FIRST,LAST,MEDIAN,VARIANCE,UNIQUE,CONCATENATE. Output columns are namedSUM_acres,COUNT_idand so on, as ArcGIS names themmulti_part—Falsesplits disjoint pieces into a feature each, repeating the attributes (ArcGIS'sSINGLE_PART)unsplit_lines— lines only: merge runs that join end to end, so two unconnected roads called "Main" stay two features
result = env.Dissolve("parcels.gpkg", output="operators.gpkg",
dissolve_field="operator",
statistics_fields=[["acres", "SUM"], ["wells", "COUNT"]])
result = env.Dissolve("parcels.gpkg") # everything becomes one outline
env.SpatialJoin(target_layer, join_layer, match="intersects", how="one_to_one", keep_all=True, fields=None, stats=None, search_radius=None, unit="km", output=None, preview=False)
Transfer attributes from one layer to another by location. Mirrors arcpy.analysis.SpatialJoin. Returns a SpatialResult.
match—intersects(default),contains,within,crosses,overlaps,touches,closesthow—one_to_one(one output row per target, summarised) orone_to_many(a row per match)keep_all—Truekeeps targets with no match (ArcGIS'sKEEP_ALL);Falsedrops themfields— which join-layer fields to carry across. Omit for all of themstats—[[field, stat], ...]forone_to_one, since several matches have to collapse into one rowsearch_radius/unit— only meaningful formatch="closest"
result = env.SpatialJoin("wells.shp", "counties.gpkg", match="within")
result = env.SpatialJoin("wells.shp", "leases.gpkg", match="closest",
search_radius=2, unit="miles")
env.Near(input_layer, near_layer, name=None, unit="km", output=None, max_distance=None, preview=False)
Distance from each input feature to the nearest feature in another layer. Returns a SpatialResult.
It adds a column and changes nothing else — same geometry, same feature count, every input kept. So the result draws exactly like the input and can be styled, filtered or sorted by distance. Distances are measured in a projected CRS for true ground distance, then the result comes back in the input's own CRS.
name— the column to write. Defaults todist_<near layer>_<unit>, so measuring one layer against several gives you a column per comparison rather than a collisionunit—km(default),meters,miles,feet,nmmax_distance— inunit. Beyond it the column is null, not a sentinel value, and the result warns how many features had no neighbour in range
result = env.Near("wells.shp", "pipelines.gpkg", unit="miles", max_distance=5)
result = env.Near("wells.shp", "faults.gpkg", name="dist_to_fault_km")
result.preview("Wells by distance to pipeline")
Where several near features tie for closest, the first is kept and the result says how many tied.
env.load(source) — fluent spatial pipeline
Create a chainable SpatialPipeline from a source layer. Add operations with .buffer(), .clip(), .intersect(), set outputs with .to(), push to Atlas with .preview(), then execute with .run() or await .run_async(). Intermediate temp files are cleaned up automatically.
result = (
env.load("wells.shp")
.buffer(1, "miles")
.clip("texas.gpkg")
.to("wells_1mi_tx.gpkg")
.preview("Wells in TX", color="#F68D2E")
.run()
)
# Fan-out multiple pipelines in parallel
import asyncio
results = await asyncio.gather(
env.load("wells.shp").clip("texas.gpkg").run_async(),
env.load("wells.shp").clip("new_mexico.gpkg").run_async(),
)
Async variants
Every operation has an async counterpart — clip_async, buffer_async, intersect_async, union_async, dissolve_async, spatial_join_async, near_async, morph_async — running in a thread-pool executor. GDAL releases the GIL, so multiple calls run truly in parallel via asyncio.gather().
import asyncio
# Single async call
result = await env.clip_async("wells.shp", "texas.gpkg")
# Fan-out in parallel
r1, r2, r3 = await asyncio.gather(
env.clip_async("wells.shp", "texas.gpkg"),
env.buffer_async("wells.shp", 1, "miles"),
env.intersect_async("wells.shp", "permits.gpkg"),
)
SpatialResult
Returned by every operation and by the pipeline's .run(). Exposes operation stats and chainable output methods.
| Attribute / Method | Description |
|---|---|
input_count |
Feature count of the input layer |
output_count |
Feature count of the result |
dropped |
Features lost. Never negative — Union and Dissolve can grow the count |
slivers |
Polygon features with suspiciously small area (intersection artifacts) |
crs |
CRS of the output (e.g. EPSG:4326) |
elapsed_s |
Wall-clock seconds for the operation |
warnings |
List of non-fatal warnings (CRS reprojection, empty result, etc.) |
.gdf |
Load result as a GeoDataFrame (lazy, cached) |
.preview(name, color) |
Push to a running Atlas server |
.to(*paths) |
Write to one or more additional file formats |
env.morph(input_path, output_path, **kwargs)
Universal format translation. Converts between shp, gpkg, gdb, csv, xlsx, xls, dbf, geojson, json with automatic CRS handling, field name fixes, and multi-layer support.
input_path— source file or geodatabaseoutput_path— destination file. Extension sets the format. Use trailing/for directory output (one file per layer). Use dot notation for named layers:roads.parcels.gpkgx_col,y_col— column names for X/Y coordinates (auto-detected if not provided)wkt_col— column containing WKT geometry (auto-detected if not provided)crs— coordinate reference system e.g.EPSG:4326(required for tabular → spatial)
env.morph("roads.shp", "roads.gpkg")
env.morph("county.gdb", "county.gpkg")
env.morph("county.gdb", "output_folder/")
env.morph("owners.csv", "owners.geojson", crs="EPSG:4269")
env.morph("owners.csv", "owners.shp", x_col="LONGITUDE", y_col="LATITUDE", crs="EPSG:4269")
env.morph("roads.gpkg", "roads.parcels.gpkg")
env.morph("data.json", "data.gpkg")
# async variant
await env.morph_async("roads.shp", "roads.gpkg")
Smart behavior:
- GDB / GPKG with multiple layers → detects all layers automatically
- CRS mismatch → auto-reprojects
- Shapefile field name limit (10 chars) → auto-truncates with warnings
- Invalid output path → plain English error
- Empty layers → skipped with a warning, not a crash
env.atlas — interactive map server
env.atlas launches a browser-based interactive map (MapLibre GL JS) that Claude can load data into and control programmatically. The user opens it in their browser and fine-tunes from there.
Requires pip install env-able[atlas]
import env_able as env
# Start the server (non-blocking — runs in background thread)
env.atlas.serve(block=False)
# Connect and operate
client = env.atlas.connect()
client.add_layer(gdf, "Wells", color="#f5a623") # push a GeoDataFrame
client.set_viewport([-103.0, 32.0], zoom=7) # frame the view
print(client.state()) # check what's on the map
client.save_layout("wells_map.atlas.json") # save for later
AtlasClient methods:
| Method | Description |
|---|---|
add_layer(data, name, color) |
Push GeoDataFrame or file path; serializes inline, no temp file |
upload_layer(path, name, color) |
Load any format (gpkg, geojson, csv, xlsx, zip/shp); converts via morph |
remove_layer(name) |
Remove a layer by name |
clear() |
Remove all layers |
set_layer_color(name, color) |
Change a layer's color; browser updates on next poll |
reorder_layers(names) |
Set rendering order — first name = top of map |
validate_join(input_layer, input_field, join_source, join_field) |
Preview join match stats without modifying any layer |
join_field(input_layer, input_field, join_source, join_field, fields=None) |
Left-join attributes from a loaded layer or table into another layer; fields limits which columns are added |
set_viewport(center, zoom) |
Set map view — browser flies there within ~2 s |
get_viewport() |
Read current viewport (reflects user pan/zoom) |
state() |
Layer count, names, colors, feature counts, current viewport |
save_layout(path) |
Write full map state to .atlas.json |
load_layout(path) |
Restore a saved layout |
export_svg(path) |
Export all data layers as a vector SVG (no basemap) |
is_running() |
Health check |
The browser UI includes an ArcGIS Pro-style ribbon with basemap switching, file upload, and PNG/PDF export, plus a layer panel with drag-and-drop reorder, independent fill and outline color pickers, per-layer opacity (0–100% in 10% steps), visibility toggle, zoom-to, and remove. The Layout tab provides an ArcGIS Pro-style Layout View with 8 A4 templates, north arrow, dual scale bars (map scale 1:N + RF), and title text formatting.
env.stream — large-scale Databricks pulls
env.stream pages arbitrarily large tables through DuckDB without holding them in RAM, writing directly to GPKG, GeoJSON, Parquet, or CSV. Bypasses the ~4096-row / ~2 MB Databricks response cap.
Requires pip install env-able[fast]
from env_able.stream import pull_to_file, connector_arrow_frames
# Stream a full table to GeoPackage via Arrow (no row cap)
frames = connector_arrow_frames(
"SELECT * FROM catalog.schema.wells",
host="https://adb-xxxx.azuredatabricks.net",
http_path="/sql/1.0/warehouses/xxxx",
token="dapixxxx"
)
rows = pull_to_file(frames, "wells.gpkg", wkt_col="geom_wkt")
print(f"{rows:,} rows written")
Transports:
| Function | Method | Cap |
|---|---|---|
connector_arrow_frames |
Databricks SQL connector Arrow batches | None |
rest_external_links_frames |
Statement Execution API, Cloud Fetch | None |
keyset_frames |
Seek/keyset pagination | Configurable page size |
offset_frames |
LIMIT/OFFSET pagination | Configurable page size |
Changelog
v1.9.1 — 2026-09-15
Added
- Drag a loaded layer or table into a
.gdbor.gpkgfrom the Catalog tree. An empty geodatabase is not a special case — a folder namedX.gdbhas no system tables, so it is created on the first drop and appended to after (GDAL OpenFileGDBDCAP_UPDATE). A name already in the container is refused with 409 rather than overwritten. - A Keybinds tab in Settings — bound by pressing the keystroke, per-row reset, conflicts reported. Only what you changed is stored, so a better default still reaches you.
- Morph saves where you tell it: Downloads, the project folder, or a browsed folder. The extension is the format's, and an impossible destination is refused before the export runs.
- A
+on the Layouts category on hover, and a loading bar on a recent project while it opens — a 191 MB project measured 15.2s with nothing on screen to say so.
Fixed
- Right-clicking a layer row did nothing: the menu opened and the same event reached the document listener that dismisses it, so it was shown and hidden inside one event.
scriptswas offered as a browse root — Atlas launches from<skill>/scripts, the one name the tree hides everywhere else. Climbs to the first ancestor that is not hidden.- An empty
.gdbcould not be registered. Registering is not loading; loading one is still an error. - The visibility eye moved left of the colour swatch, Clear stopped reciting its count, empty Catalog categories read None, and adding an ENV layer no longer recites its symbology.
v1.9.0 — 2026-09-14
Added
- A Standards tab in the colour picker, under fill and outline: the named Enverus colours as a labelled list, each name set in the colour it names. The ink is walked toward the panel until it clears 4.5:1 — "Yellow" in #FDCF0C measures 1.4:1, and a colour name nobody can read is not a label.
- 17
env-*colour ramps and 5 Enverus categorical schemes, the ramps listed first rather than after the ColorBrewer sets. - Every colour chooser previews itself — scheme, heat ramp and ramp, painted by one function. A scheme draws hard stops, a ramp draws a blend.
Changed
BRANDis the canonical seven Enverus hues, not nine. It had drifted from the reference sheet — green at #56BC2F rather than #58B947, no pink or yellow, four non-canonical colours.add_layers()walks it, so the Python client and the MCP tool used to colour the same layer differently. Breaking if you indexBRAND[7]orBRAND[8].- Native dropdowns follow Atlas's theme rather than the OS.
Fixed
- Two different greys shipped in one package:
NEUTRON_GREYwas #807f87 where the style guide says #807F83 — a one-digit typo, and the value 551 of 602 scraped projects use. - A fill picked in the symbology pane lasted 500 ms — no
pushLayerState, so the poll put the old colour back and the fill never reached a saved project. - The colour picker's headings measured 1.86:1 against their panel; now 7.79:1.
render_map.pydefaulted to #4A90D9, a blue in no Enverus palette; now #58B947.
v1.8.2 — 2026-09-08
Added
- A Databricks table with latitude and longitude is a point layer. Discovery looked only for
a geometry-named column and missed 72 tables in the
enveruscatalogue — permits, rigs, frac crews, GHG, substations, LMP. A point is now built from the pair in the SELECT. Both halves must be there, matched on whole column names, and a real geometry column still wins. - Several label classes per layer (ArcGIS Pro's label classes): own field, own scope, own
placement, so one layer can label every county and name the state once.
dedupedraws one label per distinct value instead of one per feature. - A line layer can carry an open circle at the end of each line — the surface hole that distinguishes a placed lateral from a producing one. The legend symbol follows.
- Gridlines as a checkbox in Layout ▸ Elements: grey part-opacity graticule, coordinates at the page edge, a 1/2/5 interval, re-stepped on zoom.
- A simpler north arrow, and double-click a legend caption to retitle it.
Fixed
- Opening a layout froze the page - it fetched the full-resolution copy of every layer at once (650 MB on a real project). Exports load it now; entering a layout pulls 0.0 MB and the page answers in 3 ms. The white screen returning to the map view was the same cause.
- Geometry with two vertices per feature was simplified anyway - 6.7 s of work producing a copy byte-identical to the source, plus 227.5 MB of duplicate.
- The legend said "boundary"; a single-symbol layer is one row now, symbol beside name.
- The colour picker closed on a fill pick, taking the outline callback with it, so the outline could only be reached by reopening the picker.
- A second query clause offered values from the whole layer rather than the subset the first selects — 170 counties where 105 were reachable.
- The session recorder shipped armed:
DEBUG = Truewas hard-coded while the comment above it described a protection the line below it had removed. Derived from the repo now. - Layer opacity could not be changed from the right-click menu — choosing Opacity closed
the menu, because opening a
<select>is a click that bubbles to the global click-to-dismiss. A click inside the menu that is not on an action row now leaves it open. - A restyled county or DSU came back to the rule's colour — a standard treatment is a default for a layer arriving the first time, not a rule re-imposed on every load. The recognition flag is saved with the project, and the saved fill is restored on open like every other rendered property.
- The geodatabase icon drew as two empty boxes — U+25EB has no glyph in the UI font. The Catalog's icons are inline SVG now, at 16 px.
- The Catalog listed one folder twice, and a
.gdbunder both Databases and Folders: the sources list stored the path as given while the roots list resolved it, so the de-duplication compared two spellings of one directory. - A vertical line persisted to the left of the pane tabs — orphaned
border-lefton the container; tabs now fill the full dock-width strip evenly. - The Claude pane said "Done" for a turn that stopped to ask a question — an incomplete request read as carried out. It says "Waiting on you" now, judged on the last meaningful line with a trailing option list stripped: a question mark anywhere in the last 300 characters was wrong on 4 of 7 realistic closings, in both directions.
- A Prelim beside a dated version was put to the user as a question —
Prelimis V0 and anyV_<date>supersedes it, so the names settle it. Only several dated versions is asked about. - Two different tool calls could collapse into one row saying "×2" — the count compared the truncated display summary, so two different six-path loads were identical. It compares a digest of the full arguments now.
Added
- Pane tabs drag to reorder — order persists in
localStorageacross sessions. - Geometry-type icons on feature-class rows — Point, Line, and Polygon each get a distinct icon (filled circle / zigzag / polygon outline) instead of the generic layer-stack glyph.
- Catalog auto-expands the right category on add — Databases opens for a dropped
.gdb, Folders for a dropped folder, so the new source is visible immediately.
Changed
- No counts in the Catalog — a feature class shows its geometry type, which is the part that says what you would be loading.
- Bigger and scrollable: 13 px rows, 16 px icons, and long names scrolled to rather than
truncated. Code and tooling folders (
scripts,src,build, …) are hidden from browsing. - The ribbon scrolls at full size instead of scaling down — scaling fit any width but made the labels unreadable at 700 px.
v1.8.1 — 2026-09-08
Added
- A DSU draws the same way whatever route it arrived by — 1.5 pt black, unfilled, from an
upload, a drag, the Catalog, a Databricks table or Claude. Matched on whole name tokens, so
dsu_boundariesandWellInv_MT_DSU_V_0819hit andhdsurveydoes not, and it overrides a requested colour. - The workspace is saved with the project — registered geodatabases, folder connections and
reference folders live in the
.atlasfile and come back on open. A restart used to drop every one of them.
Fixed
- Dropping a .gdb on the map loads it; dropping on the Catalog registers it — both used to only register, then tell you to drag a layer out manually. The Catalog pane highlights when files are dragged over it. Files dropped on the Catalog show a guidance toast.
- File geodatabase attribute writeback was silently refused —
edits.pyclassified.gdbasreadonly; it is nowfull, with directory-aware backup/restore, anOpenFileGDBwrite path, an explicitgeometry_typederived from the data (OpenFileGDB refusesUnknown (any)), and a clear 422 for mixed geometry naming the types found. - The ribbon painted its controls over the next group as the window narrowed —
.r-grouphad amin-widthfloor that let it shrink below its own contents while the panel overflowed visibly. Measured at 800 px: four controls spilling, up to 70 px. The ribbon now scales to fit instead: controls go 108.5 px at 1600 to 70.9 px at 700, nothing is scrolled out of reach, and a panel that already fits is not shrunk. 0 spills across six panels and five widths.
v1.8.0 — 2026-09-04
Added
- The Catalog pane is one Project tree — Maps, Layouts, Databases, Folders — grouped by type the way ArcGIS Pro's Project tab is, with right-click menus: New Map, New Layout, Add Geodatabase, Add Folder Connection, Rename, Delete, Add To Current Map, Copy Path.
- Dropping a
.gdbon the map registers it under Databases — it used to do nothing usable, because a browser hands a dropped directory over as its.gdbtablefiles. The folder name and a sample of the filenames are resolved server-side and confirmed against those names. - Open a geodatabase to see its feature classes, then drag the one you want onto the map. Registering is not loading.
- The project is the workspace: the folder the
.atlasfile sits in appears under Folders. - A drag out of the Catalog carries the path, not the bytes, so nothing is copied and the layer keeps a referenceable source.
- Layout elements are dragged — click to select, drag to move, eight handles to resize; the numeric fields remain for exact values.
Changed
- The pane's stacked sections are gone; adding a map is New Map on the Maps container.
- Rename and delete for maps and layouts are shared by the tree and the tabs.
- The map frame is resized by the numbers, not by dragging: its wrapper is the live map canvas.
Fixed
- A page arranged without pressing "+ Layout" first was never saved —
captureLayout()needs a layout record and none was created, so every element edit was dropped while the save reported success. - The Catalog pane flickered twice a second — the layer poll re-rendered it every tick and the render cleared the pane before it had anything to put back. 201 row-count changes in 20 s of sitting still becomes 0: the tree is written once, and an unchanged tree writes nothing.
- File geodatabase attribute writeback was silently refused —
edits.pyclassified.gdbasreadonly, so every edit endpoint returned 422..gdbis nowfull; the backup helpers usecopytree, and_write_gdfwrites viadriver="OpenFileGDB". - SVG export legend was an empty box —
render_layout_svg()took no layer data, so the legend always drew as a blank rectangle labelled LEGEND. Each visible layer now renders a swatch (circle / stroke / rect) plus its name.
v1.7.3 — 2026-09-04
Fixed
- File Geodatabase export never worked. An export goes through a scratch GeoJSON, which
declares
Unknown (any)geometry, and OpenFileGDB refuses it. The concrete type is derived from the data now —PROMOTE_TO_MULTIdoes not resolveUnknown, measured. 540 mixed polygons out whole, area delta 6.4e-10%. - A
.gdbhanded to Atlas answered "holds no spatial files", listing its own.gdbtableplumbing, because a geodatabase is a directory. It is a container now and every feature class loads. - A multi-layer source converted into one container kept only the last layer — the destination was deleted once per source layer.
- A container read without a named layer took its first in silence. It reports which one it read and what else was inside.
- The GDB failure text blamed the GDAL build; OpenFileGDB has been writable since GDAL 3.6.
Added
env.layers(path)— the layers in a.gdbor.gpkg, with geometry type, count and fields.layer=onenv.morphnames the layer inside a container output.- An export can name the geodatabase and the feature class separately.
v1.7.2 — 2026-09-03
Changed
- A project opens on North America, and nothing pans the map on its own. Adding layers does
not move the camera. Framing is deliberate: Zoom to layer,
client.fit(),atlas_fit.
Fixed
- Ctrl+S left the project reading as unsaved, so closing asked about work already written. The browser's layout signature arrives debounced, and a save inside that window took a baseline the pending touch then contradicted. The save carries the signature now.
v1.7.1 — 2026-09-02
Changed
- Compliance is asked once per project, not once per launch. No project, no question; a new project asks; a vetted project never asks again. Recorded in the project file with the account name and date, and never written into an export.
Fixed
- Closing the window left Python running. It owns a native window now and closing it ends the process, asking about unsaved work first.
- Atlas wore Anaconda's icon in the taskbar. It has its own icon and identity now.
- Labels drew underneath other layers, and ignored a query. Labels draw above all data, and the filter reaches them — four places were applying filters independently; there is one now.
- Opening a project zoomed out to the whole world, because the auto-fit framed the bottom layer. The map is framed once; a project's saved viewport wins.
- Reopening a project made every layer disappear and 500'd the geometry endpoint. The layer table is no longer readable mid-rebuild.
- Dragging a layer put it back where it started — the reorder endpoint stored the order upside down.
- The layer counts were invisible at 1.71:1 and 2.61:1, where 4.5:1 is needed.
v1.7.0 — 2026-09-02
Added
.aixexport — a map opens in Adobe Illustrator as named, editable layers through the ArcGIS Maps for Adobe extension. Vector geometry, not a raster. The format grammar was decoded from real ArcGIS Pro exports and verified with two independent PDF parsers.- A "Graphics Ready" layout — 7.01 in square (measured off the reference exports), no basemap, and only a scale bar, a legend and text. Exports AIX, PDF and SVG only. For artwork a designer will finish, so it carries no title by design.
- A Clear button on the Select toolbar, which names the count it will clear.
Changed
- A project is a
.atlasfile..atlas.jsonstill opens, and saving one keeps its own name. - A save references data instead of copying it. ENV and volume layers reference their local cache and remember the volume path so a cleared cache re-fetches; a dragged-in file is referenced only when its bytes are proven identical to a file on the machine; and a layer that must be copied is not rewritten unless it changed. One real project's sidecar was 191.6 MB rewritten on every save — that save is now under 10 ms and 0 bytes.
- Every PDF is 300 DPI and lossless. The map is re-rendered off-screen at the page's real pixel count (a 7.01 in page is 2103 x 2103), and the image is FlateDecode rather than the JPEG Pillow writes for RGB — a map's fine work is hairlines and small text.
Fixed
- A large project could not be opened at all. Display simplification keyed on feature
count, so a 3,233-feature county layer carrying 7.4 million vertices was sent whole at
294 MB and killed the browser tab, while 32,012 wells at a quarter of the vertices were
simplified. Now judged on vertices too;
_gdf_to_geojsonno longer serialises every geometry to text and parses it back (11.0 s to 2.4 s, byte-identical); and the display copy simplifies 12x faster. 376 MB to 101 MB, 29.5 s to 15.2 s.full=1is untouched, so exports and Layout still draw the stored geometry. - "Simplify large layers" in Settings was read by nothing and had never worked.
- Changing the basemap wiped every user layer — the data is carried across the style swap now (372 samples, none missing).
- A line width could not be set to 0.8 —
min="1"forbade it, the spinner was integer-only, andparseInt("0.8")is0, which is falsy, so a default of 5 was substituted. - Renaming a layer refetched all of its geometry. It renames in place now.
- County layers dragged in from a disk now get the fixed ENV treatment, recognised on name and fields together. Subplay layers deliberately are not: they are the subject of the map.
- A map with no basemap paints
#E2E2E3, measured off the reference export, through a background layer so it reaches the PNG, PDF and SVG.
v1.6.1 — 2026-09-01
Fixed
-
The Claude pane could not call the tool 1.6.0 was about —
atlas_env_layerswas served by MCP and never added to the bridge's permission grant, so the pane said "I need permission" at a user who had no way to give it. The grant still names its tools one by one on purpose, butverify.pynow partitions the served tools against it: every one must be allowed or explicitly withheld, so forgetting fails the build. Proved against the real CLI, not the list. -
A fit framed the planet once context was added — US Counties crosses the antimeridian for Alaska, so adding counties to a Montney map and fitting framed -179 to +180. Reference layers are left out of the frame when there is a subject to frame instead; they are still framed when they are the only thing loaded, and naming one explicitly still frames it.
-
A project knows when it has unsaved work — an orange dot beside its name. Four of sixty-six mutating endpoints set the flag before this, all attribute edits, so a colour change or a pan read as saved.
Derived rather than tracked: the save-relevant state is hashed on save and on open and compared as you work, so no endpoint has to remember, a new one is covered the day it is written, and undoing an edit reads as clean again. 0.03 ms —
/api/fingerprintwas unusable here at 12 ms cold, because it hashes every feature; each layer's_vcounter carries the same information. Layout elements live in the browser, so they report their own hash throughPOST /api/project/touch.
v1.6.0 — 2026-09-01
Added
-
Add ENV Layer — the shared reference layers, from the ENV data volume, in one click from a Data group at the far left of the Map and Layout ribbons. A dropdown tabulates them: what each is, how it draws, how big it is, whether it is already cached.
The symbology is fixed rather than defaulted: US and Canadian Counties at 0.8 pt Gray 40%, Subplay Boundaries at 1.5 pt Yogo Blue, all outline-only. A county layer drawn differently on every map is not a base layer. The numbers live in
atlas/env_layers.pyand are asserted; both colours are named ArcMap Standard entries so a swatch can be found again in the picker.The style is stamped server-side and merged into the symbology rather than replacing it — a record the browser holds and the server never sees does not survive a reload or reach a saved project. The swatch draws the real line at its real weight instead of naming it.
-
atlas_env_layers— twenty tools. Lists the reference layers and says which are relevant to the data already on the map, or loads them with their symbology. Relevance is a fact rather than a reading of the request: the data's extent against each layer's measured coverage, plus the fields the loaded layers carry (aSubPlayorPlaycolumn means the map is play work).A single bbox per layer would have failed — US_Counties spans the antimeridian for Alaska and reaches -14.6 latitude for American Samoa, so its total extent intersects Alberta. Split by region, CONUS stops at 49.4 and a Montney map is correctly not offered US counties. Verified against eight real plays. The measuring also showed that Canadian Counties is western Canada only (AB, BC, MB, SK) and that Subplay Boundaries holds 70 named plays, both now reported by the tool.
-
The ENV layer list is remembered, so the choices appear at once — the dropdown was asking the volume on every open (3,079 ms); it reads what it remembered (12 ms) and checks the volume behind the open panel, redrawing only if the set of files changed. Nothing expires on a timer, the same rule the query cache follows: a silent refetch just reintroduces the wait.
A registered layer is offered with no cache and no network at all, because it is known rather than discovered — "missing" is the worst answer available for a layer that exists and is the point of the feature. A failed refresh keeps the stale listing rather than blanking it.
Changed
-
Add Layer moved to the far left, into the same Data group and behind Add ENV Layer, and now appears on the Layout ribbon too. Both copies share one handler and disable together.
-
The dropdown lists what a layer is, and nothing else — the Symbology and Size columns are gone. A fixed treatment is exactly what nobody choosing a layer needs to look at, and a byte count is an implementation detail.
Fixed
- Small text in the new dropdown measured 3.12:1 against its panel where 9—11px needs 4.5:1; moved to tokens that pass. The caret rendered as a stray dot under the button label.
- An assertion about a removed Data pane was scoped to the pane table, since Data is now also a ribbon group name and the page-wide check had started failing on correct documentation.
v1.5.0 — 2026-08-27
Added
-
The ArcMap Standard palette, all 120 colours, in position — twelve columns of hue by ten rows of value, in the cells ArcMap puts them in, because the position is the information: the column is the hue, the row is the value. Read from the spreadsheet into generated source rather than retyped, since a wrong swatch in a colour table is invisible until somebody notices a green is the wrong green.
Every non-Enverus categorical scheme is now a row of that grid —
ArcMap Bright,Light,Pastel,Medium,Dust,Dark,Deep,Earth— which is what categorical symbology wants: hues far apart, lightness constant. Column 1's neutral greys are skipped, since a grey among hues reads as no data. Enverus is untouched. -
Hovering a swatch names it, after 900 ms. Not a
title— its delay belongs to the browser, it draws in the OS chrome, and it cannot show the hex under the name. -
A progress bar for morph, from ogr2ogr's own
-progressticker — which needed an incremental read, sincecapture_outputbuffered it until after the work was done. -
"Blank map" creates its project file,
MyProject1,MyProject2, ... the lowest number free. A manual save still refuses an empty scene. -
atlas_set_layer_stylecan set the fill colour. It tookoutlineand notcolor, so a polygon could be outlined any colour and never filled one.
Changed
- A new layout or map view starts with no basemap, whatever the working map shows: a layout is composed around its subject and a basemap competes with it. A layout now remembers the basemap you then choose, and a view's is stamped on the way out — without either, the default would put itself back on the next tab switch.
- Only the first layer moves the map — loading three layers used to fly the camera three times
and settle wherever the last one finished.
atlas_fit,client.fit()and Zoom to layer still frame it deliberately. - Polygons draw unfilled by default. Most polygon data is boundaries, and a flooded boundary
hides the basemap and everything under it. The colour becomes the outline; a saved project fill
still wins;
Polygon fillin Settings turns it off. - The map is one world wide —
renderWorldCopies: false, so dragging east stops instead of repeating Alaska forever. NotmaxBounds: one spanning the world is unsatisfiable at low zoom and MapLibre answers it by forcing max zoom, which broke fitting. - An upload says how many layers it is adding — "Adding 3 layers: A, B, C" rather than
"Uploading A.shp (+23 companion files)". A dataset is a stem; a lone
.dbfis one, and stops being one when a.shpshares its stem.
Fixed
- Three layers loaded together all came out the same colour. The palette walk compared each
layer's
color, and unfilled polygons store the sentinel"none"there with the real colour onoutlineColor— so no slot ever read as taken. It compares the effective colour now. POST /api/views500s if given a basemap —ViewCreatehad no such field whileViewUpdatehas had one for releases.- A redundant property scan on every layer load, skipped where it provably cannot find anything.
v1.4.6 — 2026-08-26
Changed
- Layers load about twice as fast, byte for byte identically.
json.loads(gdf.to_json())appeared in fourteen places; it builds the payload as a string and parses it straight back into the objects it came from. Direct viashapely.to_geojson: 1409 ms → 774 ms on 20,370 polygons. Verified identical across seventeen cases — NaN (which would otherwise have raised, since responses are dumped withallow_nan=False), missing and empty geometry (both of whichto_jsonwrites as null), datetimes with NaT, nullable ints, no-CRS frames, and three real shapefiles.
Fixed
- Bug fixes shipped alongside the above.
v1.4.5 — 2026-08-25
Added
-
Two seconds off every launch — the "already running?" check used
urlopenonlocalhost, which waits out a timeout on::1and then127.0.0.1when nothing is listening (2.07s). It is a socket probe now (0.16s). -
Launching reports when the environment is older than the skill, which is otherwise invisible: a venv here was on env-able 1.2.3 while the skill said 1.4.5.
-
Unity Catalog volumes — nineteen tools. A volume holds files rather than tables and lives in
information_schema.volumes, which discovery never queried, so a volume full of shapefiles was invisible whatever went indatabricksIncludeAll. They now appear in the Catalog pane above the tables: expand one to list it, expand a folder, click a shapefile and it loads. A shapefile is one row, not one per sidecar.Name one in
databricksVolumesand it needs no discovery at all — which matters, because reading a volume's files and listing volumes from the metastore are different privileges. Files come through the Databricks Files API, since a MANAGED volume's storage is unreachable directly andread_files()knows nothing about shapefiles.atlas_databricks_browsegives Claude the same route, and a/Volumes/...path goes straight toatlas_add_layers: a file in a volume is a file. -
databricksIncludeAll— catalogues or schemas listed whole, spatial or not, for tables worth reaching before they have geometry. A name matches either level. Empty by default. -
A table with no geometry loads as a table, into the Catalog pane, and the + button spatializes it once you name the coordinate columns.
atlas_databricks_addroutes on the manifest rather than refusing.
Fixed
- The selection highlight drew underneath every layer — created on
style.loadbefore any data layer exists, so it sat at the bottom of the stack and everything added afterwards covered it. It is raised after anything that changes the stack now, and painted with a dark casing under bright cyan so it reads over any fill. A basemap change also used to empty the selection silently. - Refresh did not refresh — it re-read the catalogue list and redrew the stored scan state, so a table added since a catalogue's first open never appeared and the age stayed at "15d ago". It rescans the catalogues that hold spatial tables now and reports the delta; empty ones are left alone, and shift-click rescans a single one.
- A permission failure says which permission. Discovery needs
information_schema; pulling a named table needs onlySELECT. A token with the second but not the first used to get a bare "query failed" that read as a broken connection.
v1.4.4 — 2026-08-25
Fixed
- Add Data and dropping a file on the map were broken in 1.4.3 — every upload returned
"too many values to unpack (expected 2)". The 1.4.2 zip fix defined a second function named
_read_spatial, shadowing the real CRS-resolving reader that returns(gdf, crs_note); callers that unpacked it raised, and the rest silently stopped resolving CRS. The wrapper is_gpd_readnow.
Added
- A test suite for
/api/upload— the regression passed 596 assertions and six suites because they all load data through the MCP tool, so the endpoint a person clicks had no coverage. docs/verify.pyrefuses duplicate function definitions across every module andindex.html, since a redefinition is silent in both languages.
v1.4.3 — 2026-08-25
Added
atlas_set_basemap, includingnone— eighteen tools. Claude could not change the basemap, so "no basemap" was answered with "there's no tool exposed for that here". Turning it off is a cartographic choice, not a fallback: when the subject fills the frame the basemap competes with it. The choice is versioned so a reload keeps it, applied before any layout becausesetStylerebuilds every layer, validated server-side because an unknown name would silently blank the map, and the ribbon's own control moves with it.
v1.4.2 — 2026-08-25
Added
atlas_rename_layerandatlas_rename_field— seventeen tools. Both renames had worked server-side for releases and neither was exposed, so Claude correctly reported that Atlas could not do something it had always done. Renaming a field repoints the symbology, the labels and any filter, so a layer coloured by that field keeps its colours.- Drag files onto the Claude pane, not only folders. A filename is more distinctive than a folder name, so it resolves better; a batch is one question, and a failure gives you a box to paste a path into.
atlas_search_referencestakes apath, so a folder named in the conversation is somewhere it can look. It rises one level when that folder has nothing, and never claims a layer is absent.atlas_statenames the linked reference folders, so "add in these layers" is a complete request instead of a question back. One turn, 16s, where it used to take two.- Versions of one dataset are offered, not loaded — a
Prelim, aV2_0812and aV_0819are one dataset three times, and loading them all draws the same features on top of each other for three palette slots. The unambiguous layers load immediately; the rest are offered as a numbered list. A file and a.zipof that file are never treated as two versions. - The layout legend has contents — every visible layer with the symbol it draws, classes, a graduated ramp with end values, and a count of what did not fit.
Fixed
- Zipped shapefiles could not be read.
SHAPE_RESTORE_SHX=YES, set process-wide, made GDAL attempt an.shxrebuild — a write — inside the read-only/vsizip/filesystem; the driver then reported no layer and pyogrio raised a bareIndexError. Scoped to bare.shpfiles now. - Changing a symbology field kept the old field's colours —
valueColorsis derived from the field, and the 1.4.1 merge carried it across, soenvoperatr—>vintageleft every well Devon-coloured. - A template's own scale bar never re-labelled on zoom, because the repaint keyed on an
idthat only user-added elements have. - The scale bar drew at 6.4mm — the template box enclosed the bar and its numbers. 1.5mm now.
- Resolving a dropped folder starved itself: home at depth 4 is 6,332 directories and 3.3s,
almost all
AppData, and ate the whole budget. Per-root time slices, pruned directories, and every ancestor of the working directory as a root — two levels was not enough to reach a sibling data folder. - A folder given to
atlas_add_layersloaded everything in it silently, counting a shapefile and a zip of it twice. atlas_set_layoutsays when a layout has no title, because an untitled layout is not a map.
v1.4.1 — 2026-08-25
Added
- Drag a folder onto the Claude pane to make it searchable. Dropping on the map loads the layers; dropping on the pane gives Claude somewhere to look, so "add the DSU and viability layers" works without naming a path. A browser will not say where a dropped folder lives, so Atlas resolves it from the folder name and the filenames it saw — bounded to places worth looking, and confirmed against those filenames before it is accepted. Cold resolve: 0.2s.
referenceRootssetting, for data somewhere unguessable.
Fixed — all five found by making real maps and looking at the pictures, and all five in rules written the day before:
- An outline-only context layer was stacked underneath the data and vanished. An outline hides nothing, so it belongs on top; the rank depends on the treatment, not the role alone.
- Colouring by a 63-value field happened without comment.
atlas_set_symbologynow counts its categories and warns past the seven-colour ceiling, while still doing what was asked. - Labelling a layer wiped its outline — the symbology spec was rebuilt instead of merged.
- A white halo behind white text: the halo is derived from the ink now.
- The scale figure and the title were invisible on the dark title bar — ink chosen for white
paper, measured at
rgb(26,26,26)onrgb(26,26,42). Fourth and fifth instance of that class, now asserted per surface.
v1.4.0 — 2026-08-24
Added
- Claude is in Atlas. A new Claude pane — the default pane on the right — takes a request in plain English and changes the map, listing each action as it happens with a live progress row and the session cost. No API key: Atlas drives the Claude Code CLI already installed and signed in on the machine, as a local program. Nothing is sent to a model by the browser and no credential is stored by Atlas.
/mcpon the Atlas server — MCP over HTTP (JSON-RPC 2.0), served by the process that already holds the state, so there is no second process and no new dependency. Fifteen tools: look at the map, load files or folders, frame, colour, style, symbolise, label, read attribute fields, reorder, lay out, remove, and pull from Databricks.claude_bridge.py— one persistent CLI process, its event stream normalised for the UI. Persistent because a cold start costs ~12s and re-writes ~26k tokens of preamble; a warm turn reads ~87k cached tokens against ~800 written, so the preamble is never re-paid.- A layer is interpreted, not just loaded. The EIR design authority is applied on load: oil, gas, water and sand take the industry colour; basemap geometry (counties, states, plays, subplays) becomes an outline in structural grey at the style guide's width for its kind and consumes no palette slot; the stack is ordered so the basemap cannot bury the subject; labels take a white halo. Seven categorical colours, ordered for a measured minimum separation of dE 33. Every assumption is reported, so it can be corrected.
- Databricks pulls — search the lake, count before pulling, then add. The data goes warehouse to map without passing through the conversation, so size is a wait rather than a limit. The token stays in Settings; no tool can set it.
- A resizable dock, 380px by default. 288 was chosen for a property list, not a conversation.
Security
- The CLI's shell tool was reachable in the first cut, and the test that should have caught it
passed. The deny list used Unix names, and on Windows the shell tool is called PowerShell, so
it was never denied; the refusal that had been tested was refused for an unrelated reason. An allow
list does not close this — there is no allow-only mode — so the deny list is now generated from the
CLI's own announced tool list,
verify.pyderives its check the same way, and the bridge reads the tool list back on every session and reports anything unexpected into the transcript.
Fixed
- Three defects of one shape, worth naming: a tool that writes a record the browser does not read reports success and changes nothing. Layer opacity, outline colour and draw order all changed the server and not the map. The poll syncs all three now.
- "No fill" made a layer invisible, because opacity applies to the outline too.
no_fillis a colour, not an opacity of zero. - Non-ASCII characters in replies were mangled: the child process emits UTF-8 and
text=Truedecoded it as cp1252.
v1.3.2 — 2026-08-20
Fixed
- The printed scale was wrong by 2.17x — on every layout, every scale bar, every export.
getMapScaledivided by 256 where MapLibre GL renders 512-pixel tiles (factor 2), and the layout then applied a 96 DPI assumption to a page that measured 88.5 (factor 1.0846). Verified against MapLibre's ownproject()and the WGS84 great-circle distance: 1:2,037,579 true versus 1:4,420,000 printed. There is now a separategetPaperScale()that measures the page instead of assuming a DPI. Any scale figure or scale bar on a map exported before this release is too small by roughly half. - A fit into a layout framed the data at under half the map frame. A fit is a relationship between bounds and a canvas, so the guard is keyed on canvas size as well as version, and the layout is applied before the fit.
- A fit no longer overrides itself with the server's headless centre/zoom estimate, and a page reload no longer loses a queued fit or layout.
Added
client.set_layer_style(name, opacity=, outline=)and role-awarequick_map: a polygon layer with something drawn on top of it is context, and drops to 25% fill with its outline kept. Brand slot order gave context the loudest colour in the palette, which tested worst of four treatments.geometry_typeon the add-layer response.
v1.3.1 — 2026-08-20
Added
client.fit()— frame the map on its data.set_viewport(center, zoom)was the only way to frame a map, so a caller who had not measured the data had to guess a centre and zoom from the layer's name. The extent is a fact;fituses it. The server sends bounds and the browser does the arithmetic against its real canvas, so the framing is right in Layout View too, where the canvas is the map frame rather than the window.client.set_layout(template=, title=, north=). Nothing could pick a layout template from outside the browser, so a scripted map ended by asking the user to click Layout, choose a template and double-click the title. Now the script finishes the deliverable.client.add_layers()— several files in one call, names from filename stems and colours fromBRANDorder.client.quick_map()— all four steps of a map request in the order they have to happen.env.atlas.BRAND/env.atlas.TEMPLATES— the palette in its correct order, and the eight template names.
Fixed
- Fitting a single point no longer zooms to maximum. A zero-area box sent
fitBoundsall the way in; a lone point now frames at zoom 12.
v1.3.0 — 2026-08-20
Added
- Add and delete page elements. The Layout > Elements ribbon gained an Add / remove group: scale bar, scale text, legend, north arrow, text, panel. New elements land mid-page already selected; Delete (or the Delete/Backspace key) removes the selection. The map frame and the template's own scenery refuse — but a panel you add is yours to move, because locking is now a property of the element rather than of its type.
- Scale bars state a real distance. They were four empty divs: stripes with no number and no arithmetic connecting them to the map. The bar's paper width times the scale denominator is a ground distance, rounded down to a 1/2/5 figure, the bar redrawn to exactly that and labelled. A 40 mm box at 1:24,000 draws 20.8 mm and says 500 m; imperial in the same box says 2000 ft at 25.4 mm.
- The representative fraction is printed on every layout —
1:24,000as text, generated rather than drawn into each template. Read from the map, so it cannot be typed into a lie. - Editable text in the ribbon, and Bar units to switch a selected bar between metric and imperial. A second bar comes up in the other system.
- The element list persists — adds, moves and deletes survive a layout-tab switch and are
written into the
.atlas.json.
Changed
- One scale bar per template, not two. The four Summary templates drew two because the
.pagxoriginals had a metric bar and an imperial one, and the port kept both rectangles while dropping what distinguished them — so they rendered as the same stripe twice.
Fixed
- Elements added over the map are visible.
#lv-aboveis z-index 103 and the map wrapper is 102, so anything placed withoutfloatthat landed over the map frame drew underneath it.
v1.2.4 — 2026-08-20
Fixed
- Light mode fixes on the layout page. The north arrow's "N" and the scale bar's alternating segments used theme tokens, but the page is white paper in every theme — so both went white-on-white in light mode. Paper is not themed; those now use fixed ink, along with two washed out labels found by auditing the rest.
- Ribbon text is readable now. Every resting state used the lowest-contrast token: 2.43:1 in
dark, 2.57:1 in light, against the 4.5:1 that 9—11px text needs. Resting states move to
--tx-4, tool buttons to--tx-3, and the group headings gained weight. - A freshly installed conda was invisible to preflight. Inside its own venv,
sys.prefixhas noconda-meta, so the conda check failed while conda sat insys.base_prefix— meaning a user who had just installed Miniforge to fix GDAL was told to install Miniforge. preflight now checks both prefixes, finds conda by path as well as on PATH, and actually runsconda install gdalinto the conda prefix instead of printing the command and stopping.
Added
--install-conda— downloads and silently installs Miniforge (no admin rights), installs GDAL from conda-forge, then re-runs from that Python so the venv inherits it. Opt-in, not default: it fetches an executable and runs it.handoff/setup.ps1— unpacks the skill and sets it up in one right-click, for when Claude Code declines to install a skill file into its own configuration directory.
v1.2.3 — 2026-08-18
Fixed
- GDAL setup on managed Windows. PyPI has no Windows GDAL wheel, so pip builds from source and
fails without a compiler — and preflight then suggested conda, on a machine with no conda.
It now checks what the machine actually has and names the one route that works, including
Miniforge (no admin needed). New
--recreate-venv, because the venv only sees packages from the Python that created it, so a GDAL installed afterwards needs the venv rebuilt. - First-run setup on a clean machine. Every document said "ask Claude to install it", and on a
fresh PC Claude cannot: auto mode declines to run a script just unpacked from a skill file, and
declines to grant itself permission to. Both refusals are right. The docs now lead with the
command the user runs —
scripts/setup_atlas.bat, or onepreflight.pyline, with the!-prefixed variant for the Claude prompt — andSKILL.mdtells Claude to hand the command over rather than retry or touchsettings.json. No library code changed.
v1.2.2 — 2026-08-17
Fixed
-
The README documented three operations out of seven.
Union,Dissolve,SpatialJoinandNearhad no entry at all — for several releases, on the page PyPI renders as the front door. All four are now documented with their real signatures and defaults,UnionandIntersectnote that they take a list, and the async list names every variant instead of claiming there are three.Two corrections while writing them:
Nearwas about to be described with ArcGIS'sNEAR_DIST/-1convention, which it does not use — it writes one column nameddist_<near layer>_<unit>and leaves it null beyondmax_distance— and the Spatial Join match list is all seven options fromSJ_MATCH, not three and an "and the rest".docs/verify.pynow derives the expected list fromdir(env), so the next operation added is either documented or the release fails. That is the actual fix; the prose was only the symptom.
v1.2.1 — 2026-08-17
Added
- Union and Intersect take more than two layers. Pass a list —
env.Union("a.shp", ["b.shp", "c.shp", "d.shp"])— and Atlas offers a checkbox list of layers instead of one dropdown. They fold pairwise, which is the correct N-way answer, and every input keeps its ownFID_column through the whole fold, so the output still says which layer made each piece.POST /api/geoprocessgainedother_layers; the oldother_layerstill works and the two are merged. POST /api/export/pdfbuilds a real PDF server-side.POST /api/databricks/open-token-pageandPOST /api/open-urlopen a settings-derived address in the default browser.
Changed
- The desktop window opens maximized rather than at 1600x1000 — the whole screen, but still a window: title bar, close button and taskbar all stay.
- No browser dialogs anywhere in Atlas. All twenty
alert/confirm/promptcalls are now themed Atlas panels with buttons that name the action. A native dialog says "localhost:8000 says", cannot be styled, and blocks the map from redrawing behind it. - No
window.openeither — it opens nothing at all in a desktop window, which is why Find token silently did nothing and Export PDF failed. Both go through the server now.
Fixed
- Intersect names the layer that emptied a multi-layer fold instead of reporting "0 features".
- A zero-area intersection (layers that touch rather than overlap) is warned about rather than counted as a result.
FID_column collisions in Union, where two layers shared a stem and the second silently won.
v1.2.0 — 2026-08-13
Symbology fixes. All of them the same mistake in several places: coercing on the wrong side of a type boundary. A shapefile gives you real numbers; a CSV, a spreadsheet and most Databricks columns give you the same numbers as text. No API changes.
Fixed
- Graduated colour now actually recolours the map. Five places substituted a plausible range
— 0–1, or 0–100 — for the real one, so the ramp spanned values the data never held and every
feature clamped to one end. An empty range box means derive from the data; a typed 0 is
taken as deliberate;
field-statscoerces strings and returnsnullwhen nothing parses instead of a fake 0/1. - Unique Values works on a numeric field. The colour table is keyed by string and MapLibre's
matchcompares type as well as value, so a shapefile'sIndex,FIDor year matched nothing and drew one flat fallback colour. - A display filter on a text-encoded number matches again.
==used to coerce what you typed to a number, so"1204"in the data never equalled1204in the filter and the layer emptied silently. Both sides compare as strings now, agreeing with the legend beside them. - A degenerate range no longer abandons the render — reversed ends are swapped, equal ends widened, instead of throwing and leaving the old colours in place.
- "Find token" only appears next to the Databricks token, not beside every secret field.
v1.1.0 — 2026-08-11
Selection, navigation and Dissolve get the controls they should have had, plus a data compliance check at launch. One default changes how existing polygons draw — see Changed.
Added
- Selection modes — a dropdown beside Select: Click (topmost feature under the cursor, panning still works), Rectangle, and Lasso. It was rectangle-only, so picking one well meant dragging a box round it. Shift adds, Ctrl removes, in every mode.
- Right-drag zoom — hold the right button, push up to zoom out, pull down to zoom in, about the point you grabbed. Roughly 100 px per zoom level, as in ArcGIS Pro.
- Dissolve's fields are a checkbox list with Toggle all and a filter, instead of a multi-select that needed Ctrl and Shift.
- A startup animation, once per launch, and a collapse on the way back to the start page.
- A data compliance check at launch — third-party data, and whether it has been reviewed.
"Not reviewed" opens the compliance request in your default browser and lets you carry on. An
attestation, not enforcement; readable from Python with
client.compliance(). - An Admin View settings tab for the developer switches, password-gated. No password ships in the package: the first person to open the tab sets one, hashed, on that machine.
Changed
- A polygon's outline starts darker than its fill, derived from it so a recolour moves both. A matching edge makes a polygon read as a flat wash.
- The Atlas mark is redrawn — a hollow chevron A with a notched arrowhead, in Enverus green.
- The tour is thirteen steps; the bug-recorder step is gone.
Fixed
- Dissolve could be sent a field the input layer no longer had, after changing that layer.
- The right mouse button started a selection box as well as a zoom.
- The compliance prompt was unreadable in light mode on the start page — a colour fallback is one fixed value and cannot follow the theme.
- "Not reviewed" opened nothing: the URL shipped blank, the button hid itself when it was unset,
and
window.opendoes nothing inside the desktop window Atlas runs in. It now has a working default and the server launches your real browser.
v1.0.0 — 2026-08-10
A version number rather than a rewrite — nothing here breaks an API. It is 1.0 because Atlas covers a whole job now: data in from files or a Databricks warehouse, styled, analysed with overlay and aggregation tools, edited, and laid out for print — and because there is finally a tour that shows people where all of it is.
Coming from 0.13 or earlier? Two things changed that you will notice: polygon fills are as opaque as the slider says (they were secretly capped at half), and Databricks layers cache their features on disk, so reopening a project reads the cache rather than re-querying — Re-pull fresh gets current data. Full list under Upgrading in CHANGELOG.md.
Added
- A guided tour — Take the tour on the start page, or File ▸ Take the tour in the
map. Fourteen steps that spotlight one area at a time and explain what it is for, aimed at
someone with GIS experience who has never opened Atlas
- each step switches to the ribbon tab or opens the pane its target lives in, so the tour visibly travels to whatever it is describing
- your ribbon tab and panes are restored when it ends; a step whose target is missing is skipped rather than pointing at nothing; the card always stays on screen
- clicks are blocked so it cannot be derailed, arrow keys move, Esc leaves, and
prefers-reduced-motionis respected - it also covers the traps: opacity means opacity, No Fill for outline-only polygons, and the Data Explorer's funnel filtering before the pull
v0.15.0 — 2026-08-10
Added
- Dissolve —
env.Dissolve(...)/env.dissolve_async, plus a Tools-ribbon button immediately left of Spatial Join. ArcGIS Pro's Data Management ▸ Dissolve, mirroringarcpy.management.Dissolve- Dissolve Field(s): a multi-select from the layer's own attributes; select none and everything collapses into one feature
- Statistics: repeatable field + statistic rows, all thirteen arcpy statistics, output
columns named
<STAT>_<field> - Allow multipart features (checkbox): unticked is
SINGLE_PART - Unsplit lines only (checkbox), shown only for line layers: merges runs that join end to end and gives each run its own feature
- a group-by rather than a geometry operation, so it uses geopandas — the second
documented exception after
Near
- Union —
env.Union(a, b)/env.union_async, plus a Tools-ribbon button immediately left of Intersect. Everything from both layers, cut where they overlap, with attributes from both on the shared piece. ArcGIS Pro's Analysis ▸ Overlay ▸ Union, same OGR engine as Intersect. Unlike Intersect, non-overlapping inputs are valid (it warns rather than failing), anddroppedis clamped at zero because Union grows the feature countjoin_attributes(dropdown):ALL/NO_FID/ONLY_FID, witharcpy's spellings.FID_<layer>carries the source id, or -1 where that layer contributed nothing to the piecegaps(checkbox): ticking it gives each enclosed void a feature with everyFID_at -1, so gaps can be selected — ArcGIS'sNO_GAPScluster_tolerance: Python-only, kept off the dialog as ArcGIS does; applied as a GEOS precision grid- Two layers rather than a ranked value table, and non-contributing columns stay null
rather than becoming
""/0—FID_already says which layers made each piece
- The Data Explorer is now "Add ENV Data Source" inside the Catalog pane — it is a data source, so it sits beside the ones already loaded, and the dock is one tab shorter. Collapsed by default; nothing loads until it is opened
- Filter a table before pulling it. A funnel beside every table opens a clause builder
using the same model and operators as the Query panel, so there is one filter language.
Fields are read from the warehouse on demand and cached per table
- Count matches before drawing anything
- the filter combines with the extent rather than replacing it — these rows, in this box
- recorded in the project source (so reopening re-runs it) and part of the cache key (so two filters on one table cache separately)
- clauses travel as data; the SQL is built server-side against the column list the
warehouse reported. A field the table does not have is refused, and the browser never
composes SQL. New
GET /api/databricks/fields
Fixed
- Clip and Intersect can now report a non-overlap. Both deleted the temp file for a
GeoDataFrame input and then built an error message reading that file's bounds, so the
helpful message was replaced by a
RuntimeErrorabout a missing path - Pane content now lines up with the pane's title. A tree row's caret sat before its
label, so a catalogue name landed 14px right of the heading and note above it. Carets and
the filter button moved into a gutter left of a shared text column; nesting steps evenly
(12px schema, 24px table). Driven by four CSS tokens instead of
12pxrepeated nineteen times, and verified by measuring real text positions in a headless browser - Opening the Data Explorer took 6.5 minutes. Discovery scanned every catalogue the token
could see — 115, one query each — and only ten held any spatial tables, so ~6 minutes
went on asking 105 catalogues a question they had already answered. Discovery is now
per-catalogue and on demand: 385s → 1.9s to open the pane, measured on the same
workspace
- a catalogue that comes back empty is remembered as empty, so the 105 are asked once ever
- the tree is catalogue → schema, collapsed, with populated catalogues first and the empty ones summarised in one line instead of 105 dead folders
- Refresh re-reads the catalogue list; a full rescan is now an explicit
GET /api/databricks/catalog?refresh=1 - an existing manifest is migrated, not discarded — v1 already knew which catalogues were empty, which is the expensive knowledge
- new
databricksSkipCatalogssetting; a skipped catalogue also disappears from the pane - new
GET /api/databricks/catalogsandPOST /api/databricks/scan
- A refresh requested while one was already running was dropped, so the layer list did not update after adding a Databricks layer until the next timer tick — the "layers are not refreshing" problem. Requests are now remembered and honoured
v0.14.0 — 2026-08-06
Mostly one feature and its consequences: Atlas can browse a Databricks workspace and pull spatial layers onto the map without the data crossing a language model's context. Connecting it to a real warehouse then disproved much of what had been written from naming conventions, so several fixes below correct this same feature rather than older ones.
Two changes affect maps you have already built: polygon fills are now as opaque as the slider says (they were capped at half), and Databricks layers cache their features on disk, so reopening a project reads the cache instead of re-querying.
Added
-
Data Explorer — a pane that browses a Databricks workspace's spatial layers and clicks them onto the map. Every table with a geometry column, grouped by catalogue and schema, plus a search box and a curated shortlist of everyday layers. Right-click for Add visible extent / Add all features / Row count / Copy table path.
- Discovery is one query per catalogue against
information_schema.columns, which finds the spatial tables and their geometry column in a single round trip - Search reads a cached manifest in the browser, so typing never hits the warehouse
- Row counts are fetched on demand, and shown before a big table is drawn
- Adding defaults to the visible extent; a table that cannot take a spatial predicate falls back to a row limit and says so
- Discovery is one query per catalogue against
-
Claude can fill the pane over MCP, so browsing needs no token. Ask it to list the workspace's spatial layers and it runs discovery through its own Databricks connection, then posts the manifest to Atlas (
client.databricks_set_layers()). The pane reads "Browsing only" and says a token is needed only to draw features. This works because a manifest is metadata — a few hundred short rows, well inside the MCP's ~4096-row cap, where the features are not- Atlas cannot borrow Claude's MCP credential: it lives in claude.ai's infrastructure and the model never sees it. So adding without a token is refused before any request goes out, with a link to the workspace's token page rather than a dead end
GET /api/databricks/discovery_sqlhands out the exact query, so the MCP route and the warehouse route return the same shape
-
Pulled features are cached on disk under a key covering table, geometry column, extent, limit and geometry type — an identical pull becomes a file read. Measured on 2,000 pipeline features: 6.6s → 0.1s
- the extent is rounded to ~10 m, so panning a pixel reuses the cache
- nothing expires on a timer; staleness is reported instead (
cached,cache_age, and "from local cache (1h old)" in the toast), with Re-pull fresh to force the warehouse - a cached layer opens with no token at all, so clearing a token strands nothing
- capped at 512 MB, oldest first;
GET/DELETE /api/databricks/cache
-
Pull Extent / Pull All at the top of the Data pane — chosen before you click, because it changes what clicking means. It used to be implicit (always the extent), which read as Atlas ignoring the request. The active mode is named in the footer as well
-
A progress bar at the bottom of the pane with the current stage and an elapsed clock. Indeterminate deliberately: one warehouse query yields no percentage, and a fake one that stalls at 90% is worse than an honest bar plus a number you can act on
-
Binary geometry columns work. Verified against a live warehouse: the
BINARYgeometry out there is PostGIS EWKB, and Databricks' ownST_GeomFromWKBrejects its SRID flag — so no server-sideST_call is possible. Atlas selects it raw, reads it withshapely.wkb(bytes or hex, drivers differ) and clips the extent after the rows land, saying so innotebecause the row limit then applies to the whole table. Without this, US states, US counties and the PLSS grids are unreachable — they are all binary -
Column-name matching is case-insensitive, so a manifest saying
wktworks against a column declaredWKT -
gdb_geomattr_data,geometrysource,blockgeometrylinkandgeom_gmlare no longer offered as geometry — 32 columns thatLIKE '%geom%'was catching and that cannot be drawn -
status()no longer reports an empty catalogue list next to a full layer count when the manifest came from Claude over MCP -
A Databricks layer's source is its query, so a project re-runs it on open rather than caching the features
-
Somewhere to paste a Databricks token. The Data pane pointed at File › Settings and no such field existed. Settings now has a Databricks group — host, warehouse HTTP path, token, row limit, common-layers file — and the token is treated as a secret on every route out:
- per OS account in
%LOCALAPPDATA%\env-able\settings.json(chmod 600on POSIX, where the default644would have exposed it to other local accounts); never inside the repo or the wheel, so it cannot travel with anything shipped - write-only over the API:
GETreturns bullets, and sending bullets back does not overwrite the stored value. The input istype=passwordand is emptied once saved - scrubbed from every error message —
run_sqlis the single choke point, which matters because the bug recorder captures failed-response bodies - unreachable from
project.py, so no project or export can embed one - a Clear button to remove it
- pasted values are normalised: scheme and trailing slash off the host, leading slash onto the warehouse path, quotes and whitespace off the token
- per OS account in
-
A "Find token" button that opens the workspace's token page, so generating one and pasting it back is two clicks. The URL comes from the new
databricksTokenPagesetting or the configured host — a bare workspace URL is completed for you, and anything that is not a web address is refused, since this value opens a browser tab -
Settings has General / Databricks tabs, and Databricks is on the start page — the connection fields used to exist only inside a map, so you could not set up before opening one. Includes Find token and Clear, with the same write-only token handling
-
databricks-sql-connectoris a core dependency, not an optional extra — a plain install used to give you a Data Explorer that browsed, searched, then failed at the one step that matters. Markedpython_version >= '3.10', because the connector requires 3.10 while this package supports 3.8, and an unconditional dependency would silently backtrack to a 2.x release from years ago. On 3.8/3.9 the error says so instead of suggesting an impossible install.env-able[databricks]still works as an alias -
client.databricks_status()/databricks_layers()/databricks_count()/databricks_add()— the data goes warehouse → Atlas → map without crossing a language model's context, so the MCP row and byte caps no longer apply -
Connection comes from Settings or
DATABRICKS_HOST/DATABRICKS_HTTP_PATH/DATABRICKS_TOKEN, so existingenv.pullusers need no setup. The token is write-only over the API and never returned byGET /api/settings -
client.settings()/set_setting()/set_settings()— referenced in the skill's instructions but missing until now, so following them failed -
No Fill in the colour picker — outline-only polygons, for showing a boundary over imagery or stacking two areas without either hiding the other. Fill tab, polygons only (an outline of "none" leaves nothing; on a point it erases the point)
- it is a colour value, not a flag, so it survives projects, reloads and the legend without every one of those needing to know about it
- choosing it hands the old fill colour to the outline, so the polygon becomes an outline rather than vanishing
- the opacity slider and later colour changes no longer silently re-fill it
- the layer chip goes hollow, outlined in its own colour — a blank chip would read as a broken colour
-
Outline thickness in points, at the top of the picker's Outline tab — previously only in the Symbology ribbon. Stored as
symbology.size, the field the render path already used
Fixed
- Opacity now means opacity. Every polygon fill was multiplied by a hardcoded
0.5(0.6graduated), so 100% still showed the basemap through it and no slider position could make it solid. This changes existing maps: polygons that were half-transparent at 100% are now opaque — drag the slider back, or use No Fill if you wanted the outline all along - The Layout map frame no longer escapes the page. The layout maths rebuilt its view
rectangle from window size and the ribbon height while CSS also subtracted the dock width
and the 26px view bar, so the page was centred in the whole window inside a narrower
overflow:hiddenbox — and theposition:fixedmap wrapper was never clipped by the dock. It measures the container now, verified across eight window sizes × both dock states × every template - Editing a query-backed layer no longer stages edits with nowhere to save them. A Databricks layer has no file, so cells looked editable and changes staged into limbo. Editing waits for the file, and clicking a locked cell offers Save As instead of doing nothing
- Layers-panel text was too faint. The Unique Values field heading and "…and N more" measured 2.5:1 (dark) / 2.7:1 (light) against a 4.5:1 requirement; a hidden layer's name was 1.9:1. Both fixed, with hidden layers still visibly dimmer
- Light-theme fix: the selected option in a switch stayed dark-themed.
.set-sw button.onhardcoded navy-on-pale-blue, visible on the new Pull Extent / Pull All toggle and every Settings switch. Now on the--accent-bg/--accent-txpair, along with eight other rules carrying the same hardcoded values (Join and Geoprocessing run buttons, the project Go button, pane tabs, and four low-contrast pale-blue text rules) - An ESRI housekeeping blob is no longer pulled with land-grid layers. All 22
land_gridtables carrygdb_geomattr_data; inlandgrid_texas_blocksit is up to 98 KB a row across 5,836 rows — ~500 MB of nothing, and unrepresentable in GeoJSON. Discovery records that the column exists so the pull canEXCEPTit by name (necessary becauseEXCEPTerrors on absent columns). 25 rows: 1.2s instead of 3.4s. Any binary attribute that slips through anyway is dropped and named innote
v0.13.5 — 2026-08-05
Fixed
- Geometry was stored simplified, not just displayed that way — above 10,000 features the simplified copy was the only one Atlas held, so exports, the GeoPackage deliverable and Layout view all carried approximated geometry. Full resolution is stored now; simplification happens on the way out, and Layout view asks for full
- Adding a layer whose name was taken silently replaced it — names resolve to
Wells_1,Wells_2on every ingest path, and the response says which name was used. Project open, repoint and restore still replace by name on purpose - A spreadsheet with a title row above the header read as empty — the header row is detected now (a row of numbers or dates is not mistaken for one) and an empty unnamed column A is dropped
- Left-clicking a layer opened its right-click menu and left the row highlighted, so applying symbology looked like the features had been selected
- Context menu text was unreadable in the light theme — menus, the query picker and the attribute modal used hard-coded light greys
Changed
- The query builder opens with a clause ready instead of needing + Add Clause first
v0.13.4 — 2026-08-05
UI fixes, mostly in the light theme and the attribute table.
Fixed
- The light theme only reached half the UI — the layer pane, File menu, right-click menus, Morph, Symbology, Geoprocessing, Join, Query, the attribute table header hover, the modals and the badges were all literal dark hex. Elevated surfaces are theme tokens now, defined for both themes together
- The basemap follows the theme — light defaults to Light Gray, dark to Dark Gray, but only when still on the other theme's default, so a deliberate pick is respected
- File → Start page bounced straight back to the map — the redirect that keeps a programmatic map request from being stranded now knows a deliberate visit from a landing
Changed
- Renaming a field happens in the header cell, not a browser prompt
- Changing a type is a submenu, with the length box only for text
- The Fields dialog edits names and types inline — Rename and Type buttons are gone
- Blank map uses a
+icon rather than an empty square
v0.13.3 — 2026-08-05
Fixed
- A new field did not appear in the attribute table — the only refresh after the layer's data changed was nested inside the poll's filter-changed branch, so the column showed up only if a filter changed too. The table now refreshes whenever its data does, and a schema change pulls the layer immediately instead of waiting for the next poll
Changed
- Editing a cell highlights the row instead of selecting the text — select-all meant the first keystroke silently replaced the value. The table is no longer text-selectable; the cell editor still is
- New Atlas mark — three nested contour lines reading as an A, replacing the stacked plates. Sized so all three intervals still separate at a 16 px favicon
v0.13.2 — 2026-08-05
Bug fixes and refinements from a session of real map-making.
Fixed
- Labels drew nothing at all — the glyph server serves Noto Sans and Atlas asked for Open Sans, which 404s. A symbol layer whose font cannot be fetched draws no text and reports no error. Fonts are now validated against what the server has, numeric label fields are coerced to text, and the debug recorder no longer filters glyph 404s — that filter is why the reports kept saying "no failed request"
- The dock and its tab strip did not line up — both read one width variable now
- A Short field would accept a value it cannot store — ranges are enforced at entry
Added
- ArcGIS field types — Text, Short, Long, Double, Date. Older names still open
- Excel-style attribute columns — drag to resize, double-click the edge to autofit, right-click to sort / freeze / fit / rename / retype / set a domain / delete
- ArcGIS Pro-style label placement — position, weight, and collision avoidance on by default; lines label along the line
Changed
- Saving attribute edits no longer asks first — the backup, not the prompt, was always the protection. The toast names the backup
- Attribute tables open in edit mode; a read-only source still opens disarmed
- The refresh interval setting is gone and the map refreshes at its fastest
- The attribute header no longer shows the file format
v0.13.1 — 2026-08-04
Bug fixes and refinements to the panes and ribbon introduced in 0.13.0.
Fixed
- Opening the File menu blanked the ribbon — File is a menu, not a view, but it shared the tab-switching path, so General / Tools / Symbology vanished. They persist now
- Geoprocessing opened with no tool and every parameter showing — the pane tab strip opened panels directly and skipped their initialisation. It now routes through each panel's real opener, so Symbology and Query also get their target layer
- Recent projects appeared not to persist — test fixtures written into the system temp directory had filled the capped list and evicted the real entries. Temp-directory projects are no longer remembered, and the list holds 30 rather than 20
Added
- Remove a project from the Recent list — an × on each start-page card, plus Remove
missing. Forgetting never deletes.
client.forget_recent()/clear_recent() - An Atlas logo, and a favicon on both pages where there was none
Changed
- Join sits in the Geoprocessing ribbon group with Buffer, Clip, Intersect and Spatial Join
- Add a map view or layout from the Catalog, beside the lists of what they create
- The Catalog opens with a project
v0.13.0 — 2026-08-04
Atlas becomes a project-based application rather than a session that vanishes when you close it. Everything here is additive — no existing call changes behaviour.
Added
- Projects — save the whole scene to one
.atlas.jsonand reopen it rendering identically. Declarative specs, no geometry, so it stays a few kilobytes; layers with no file of their own are cached into aMyProject.data/sidecar. The round-trip is proven, not assumed:client.fingerprint()digests everything that affects what the map draws, and the tests save, clear, reopen and assert it is unchanged - A broken data reference shows as a repointable layer, never a silently dropped one
— it still registers, keeps its symbology, carries a readable reason, and stays in
every view it belonged to.
client.repoint_layer(name, path) - Start page with recent projects, data-free templates and settings. Programmatic
launches (
serve(landing=False)) go straight to the map - Settings, with a light theme — dark stays default. A real theme via CSS custom properties, not an inversion; brand colours and the layout paper stay fixed
- Multiple map views — a bottom tab strip, renameable and closeable. Each view owns its layers, basemap and viewport, and returns to where you left it. Symbology lives on the layer, so two views can style one source differently
- Layouts bound to a map view, with their own tabs. Per-layout template, title and text styling persist. A layout whose view is gone opens unbound and rebindable rather than quietly drawing the wrong map
- Catalog pane listing data sources, views and layouts with the view each draws
- Attribute editing, staged — double-click a cell; the map and table update at once and the file is untouched until Save edits, which copies it aside with a timestamp before writing. Discard returns the layer to how it loaded (the source is byte-identical), Undo steps back one change, and Restore puts a committed file back from its backup
- What each format accepts is shown up front — GeoPackage/GeoJSON full, Shapefile with its limits reported per field, CSV/XLSX attributes only, File Geodatabase read-only, no-file layers via Save As
- Schema editing — add with type/length/default, rename, retype, remove. A retype that would lose a value is refused naming the row; a removal keeps its values so Undo restores the data, not just the column
- Field domains (coded list or numeric range) enforced by Atlas whatever the format underneath, and stored in the project — the only place they can persist
- Session recorder (Ctrl+Shift+D files a bug report) and Atlas as a desktop window
Changed
- The four tool panels share one right-hand dock, one open at a time; the map shrinks rather than being covered
- Project format is version 2 — layer specs are project-level and views list names, so symbology appears exactly once in the file. Version 1 projects still open
save_project(map_name=...)is optional; omitted keeps the view's current name
Fixed
- GDAL_DATA / PROJ_LIB self-configure at import — PyCharm and bare
python script.pyskip conda's activation scripts, causingWarning 3: Cannot find tms_*.jsonand quietly degraded CRS lookups - An inferred text length was enforced as a constraint, refusing to lengthen
"CHEVRON"to"CHEVRON USA" - A layer inside a
.gdbwas not recognised as one, since the directory carries the extension rather than the path
v0.12.2 — 2026-08-04
Bug-fix pass driven by Atlas session recordings. Two of these were producing silently wrong output rather than visible errors.
Added
- Layer colours from the Enverus palette — new layers take the next unused brand colour instead of all arriving blue. An explicit colour still wins
- A shapefile dropped without its
.prjresolves its own CRS — dragging a lone.shpgives no.prjto read, and coordinates alone cannot identify a UTM zone. Atlas now tests candidate systems and keeps whichever places the data closest to the layers already on the map (or the viewport if the map is empty), logging the choice with a graded confidence. A real.prjalways takes precedence - Dropping a folder loads every dataset in it — multiple shapefiles, GeoPackages, GeoJSON, KML and File Geodatabases, each as its own layer. Sidecars are not mistaken for datasets; tabular files park as tables rather than having columns sniffed for coordinates
- Per-value colour editing in the layer legend for Unique Values, including
<all other values> - Eye icon for layer visibility, slashed and dimmed when hidden
Changed
- The layer colour chip hides when a layer is classified by Unique Values — the legend already enumerates every colour. It reappears for Single, Graduated and Heatmap
Fixed
- Exporting a filtered layer exported the whole dataset — the filter lived in the
browser as a MapLibre expression and the export never consulted the stored clauses.
Export now evaluates it server-side and reports
(2 of 5 features) - Geoprocessing ignored the active filter too — buffering a layer filtered to three features buffered all of them. Buffer/Clip/Intersect/Spatial Join now honour it, matching ArcGIS Pro definition-query behaviour
- The Symbology ribbon showed the wrong layer's fields — a removed layer left a dangling reference (producing 404s against a name the server had dropped), and switching layers quickly raced two field requests so the slower one won. Both fixed
- A large layer could land on the map with no row in the layer panel — so it could not be toggled or removed. Building its extent spread one argument per coordinate (~476,000 for a layer of well laterals) and overflowed the call stack partway through registration. The extent is now computed in a single pass, the panel row is built before the zoom, and a failed registration rolls back so the next poll retries
- Hiding a layer left its labels on the map
/api/uploadrejected an omitted colour with HTTP 422
v0.12.1 — 2026-07-30
Mostly fixes, plus a few capabilities that missed the 0.12.0 cut.
Added
env.SpatialJoin()/spatial_join_async()— attribute transfer by spatial relationship, ArcGIS Pro semantics. Match onintersects/within/contains/crosses/overlaps/touches/closest; one-to-one (withstats={"FIELD": "sum"}aggregation) or one-to-many;keep_allfor left vs inner join; addsJoin_Count, renames colliding fields, andclosestaddsdist_m- Atlas: geoprocessing in the ribbon — Buffer, Clip, Intersect and Spatial Join under a new Analysis group, plus
POST /api/geoprocess. Results land as new map layers - Atlas: session recorder —
serve(debug_dir=...)arms a REC control. Idle until pressed; then it captures clicks, panel activity, errors and failed requests, and writes a self-contained markdown bug report on demand - Atlas: incremental layer loading —
/api/layers/metaplus per-layer/geojsonwith ETag caching - GDAL/PROJ auto-configuration —
GDAL_DATAandPROJ_LIBresolved at import, so IDE and bare-script runs stop warning and stop degrading CRS lookups - Atlas: Symbology pane auto-classifies on field change, matching the ribbon
Fixed
- A single
infattribute blanked the whole map — it broke the poll endpoint, so no layers loaded at all. All ingest paths now sanitize non-finite values - Severe lag with large layers — the poll re-sent every layer's full GeoJSON every 2 s. Poll payload dropped 537 KB → 204 bytes on a 4,000-feature layer
- Drag-to-export did nothing for layers — incompatible
effectAllowed/dropEffectmeant the drop event never fired - Shapefile export was unusable — only the
.shpwas sent, without.dbf/.shx/.prj. Now bundled as<name>.shp.zip - Layer → GeoJSON and table → CSV exports returned HTTP 500 — scratch file collided with the output path
- Query filters matched nothing on numeric fields — the map compared type-strictly while the attribute table compared as strings, so the table showed matches the map did not
- Other Atlas fixes: poll fallback for older servers, deterministic layer ordering, recorder log corruption and missing report context, debug controls hidden in Layout view, basemap tile noise
v0.12.0 — 2026-07-29
env.Near()/near_async()— add a distance-to-nearest-feature column to any layer; measured in auto-UTM, returned in the source CRS, every input feature kept.unitin km/meters/miles/feet/nm,max_distancenulls beyond a cutoff, and calls chain so several proximity features land on one table- Atlas: Classification legend in the layer pane — every classified layer lists its colors and values under the row (always visible, no expander). Unique Values gets a field header, a row per value and an
<all other values>fallback; Graduated gets per-class ranges; Heatmap gets a High→Low bar; Single Symbol adds nothing. Swatches follow geometry (circle / bar / square); caps at 100 rows and scrolls - Atlas: Layer chip reflects classification — multi-color band for Unique Values, ramp gradient for Graduated/Heatmap, solid square for Single Symbol
- Atlas: Desktop window —
env.atlas.app()orserve(window=True)opens the map as an app window instead of a browser tab; pywebview native window if installed, otherwise a chromeless Chrome/Edge window, otherwise the browser. Server,localhost:<port>andconnect()all unchanged - Atlas: SVG export upgrades — new
bgargument (nonetransparent default,light,dark, or hex; was hardcoded navy) and layer groups now carry real names viaid/inkscape:label/<title>, so Illustrator and Inkscape show named layers instead of a flat mass of shapes - Atlas: Symbology ribbon — full symbology editing from the ribbon, auto-classifies the moment a field is picked (no Apply step), stays in sync with the slide-in pane, and offers 8 discrete schemes plus 18 continuous ramps. Classifying a field with 100+ distinct values now asks first
- Atlas fix: sparse symbology no longer blanks a layer —
client.set_symbology(name, "unique", field=...)with novalueColorsmatched no render branch and silently dropped the layer off the map; missing pieces are now derived from the layer's own data, with a clean downgrade to single symbol when a field is missing or non-numeric - Other Atlas fixes: poll re-render loop, labels not rendering (glyph font),
index.htmlcache headers, invalid SVG from layer names containing&or<, legend text overflow, ribbon dropdown clipping
v0.11.0 — 2026-07-28
- Atlas: Morph (Format Translator) — drag a layer or table onto the map canvas to export it; "Drop to Export" overlay + sliding panel; format selector; download streams instantly; exported layer auto-adds to the map scene
- Atlas: Fill / Outline color tabs — polygon color picker shows Fill and Outline tabs; switching tabs changes which color the swatch grid edits; points and lines show Fill only
- Atlas:
client.set_map_title(title)— set the Layout View title from Python; browser picks it up in ~2 s; pairs withupload_layer+set_viewportfor a one-block map delivery - Atlas: Selection geometry filters — selection highlight layers now filter by geometry type (fill→polygon, circle→point, line→line+polygon outline)
- Atlas: Ctrl+drag deselects — hold Ctrl while rubber-band selecting to remove features from the active selection
- Atlas: GDB export — File Geodatabase output zipped to
.gdb.zipto avoid Windows permission errors on directory-format writes - SKILL.md — full 9-color Enverus brand palette documented; fast map request playbook (inline
python -c, Layout pane as deliverable, <60 s target) - Various Atlas fixes: layer row click target, single color swatch, Morph button rename,
poll()auto-add after export, defensive morph import
v0.10.1 — 2026-07-24
- Atlas: Query Builder — ArcGIS Pro-style WHERE clause builder (side panel, 14 operators, AND/OR multi-clause, unique-value picker, MapLibre filter integration);
cl ient.query_layer()/client.clear_filter()for Python-driven filtering - Atlas: Attribute Table — bottom drawer with sortable columns, filter highlighting, and row count status; works for layers and tables
- Atlas: Query button — General ribbon tab opens Query Builder with layer/table picker
- Atlas: Layer row —
⋯context menu (Visibility, Zoom, Query, Attributes, Opacity, Rename, Remove); visibility●button kept inline; scale display rounds to 3 si gnificant figures; ribbon label clipping fixed
v0.10.0 — 2026-07-24
- Atlas: Fill + outline color pickers — independent per-layer fill and outline color controls in the layer panel
- Atlas: Layer opacity — 0–100% in 10% steps; applies across all geometry types
- Atlas: None basemap fix — layers reliably reappear after switching to the blank basemap (isStyleLoaded polling replaces fragile style.load event)
- Atlas: Title text controls — bold, italic, and color now apply correctly in Layout View, including Full Bleed float titles
- Atlas: Map scale — populates on page load; no longer drifts on pan (zoom-only updates)
- Atlas: North arrow — ~1.5× larger; bounding box removed
v0.9.0 — 2026-07-21
Breaking: Clip, Buffer, Intersect now return SpatialResult instead of GeoDataFrame. Use .gdf to get the underlying GeoDataFrame.
- GDAL/OGR spatial engine — operations rewritten as true GDAL calls, not geopandas wrappers
Clip—ogr2ogr -clipsrc; streaming, no full memory loadBuffer— OGR Python API with auto-UTM zone selection; result in original CRSIntersect—OGR Layer.Intersection(); attributes from both layers in result
SpatialResult— rich return type: counts, CRS, timing, warnings,.gdf,.preview(),.to()- Async variants —
clip_async(),buffer_async(),intersect_async()via thread-pool; GDAL releases GIL for true parallelism - Fluent pipeline —
env.load(source).buffer(...).clip(...).to(...).preview(...).run()and.run_async() where=filter — SQL attribute filter on all three operationsmorphspatial→spatial via ogr2ogr — file-to-file conversions no longer load full dataset into memorymorph_async()— async variant ofenv.morph
v0.8.4 — 2026-07-21
client.join_field()— programmatic left-join: carry attributes from any loaded layer or table into another layer in-place; optionalfieldslist to limit what's addedclient.validate_join()— preview match stats without modifying any layerclient.export_svg(path)— export all data layers as a vector SVG; browser Export ribbon SVG button- Join Field UI — field checkboxes with Select All / Deselect All; ArcGIS Pro-style two-section validation stats; many-to-one join handling
v0.8.0 — 2026-07-17
- Multi-file shapefile upload — select
.shp+ companions together; missing.shxregenerated automatically - Layer right-click menu — Rename, Zoom To, Attribute Table, Remove
- Folder drag-and-drop — drag a shapefile folder from Explorer onto the map
- CRS prompt modal — manual EPSG/WKT override when CRS cannot be detected
- ESRI WKT fallback — handles non-standard projection names via pyproj → GDAL → regex parameter extraction
Full history in CHANGELOG.md
Metadata
Release files for env-able 1.9.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| env_able-1.9.1.tar.gz | 2.1 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| env_able-1.9.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 4.1 MB
Release files / env_able-1.9.1.tar.gz
| Download URL | env_able-1.9.1.tar.gz |
|---|---|
| Size | 2.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
079c6dae1f8f0cc8e6823c0d6eba941ac750c218e2e1ffa00db5d7052b5ad8d8
|
|
BLAKE2b-256 checksum How to use checksums |
5abfee74f6e56909ebbde0276a0945cf6bd814ec4b080cba58dca8587bbfad44
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.9
|
Release files / env_able-1.9.1-py3-none-any.whl
| Download URL | env_able-1.9.1-py3-none-any.whl |
|---|---|
| Size | 2.0 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0d0a484f86716bc9a1d3844ebc6f2c65e7e6cc00cf6c6c802ef95fe90c131739
|
|
BLAKE2b-256 checksum How to use checksums |
99792e7bdfeb3dcb9c7a18d6119cffe55c21c327abe67e0919bfd128d3b7dd96
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.9
|