Skip to main content

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 complete SELECT query
  • output — 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. Default EPSG: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 — if True, 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 distance
  • unit — meters, km, miles, feet, usfeet, nautical miles
  • output — output file path
  • where — optional SQL attribute filter
  • preview — if True, 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 on
  • output — output file path
  • where — optional SQL attribute filter on the input layer
  • preview — if True, 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 own FID_ column through the whole fold
  • join_attributes — "ALL" (everything, plus a FID_<layer> per input), "NO_FID" (everything except those), "ONLY_FID" (just those)
  • gaps — False (ArcGIS NO_GAPS) gives each enclosed void a feature of its own, with every FID_ at -1, so gaps can be selected and inspected
  • cluster_tolerance — snap coordinates within this distance together, as a GEOS precision grid
  • where — 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 outline
  • statistics_fields — [[field, stat], ...]. SUM, MEAN, MIN, MAX, RANGE, STD, COUNT, FIRST, LAST, MEDIAN, VARIANCE, UNIQUE, CONCATENATE. Output columns are named SUM_acres, COUNT_id and so on, as ArcGIS names them
  • multi_part — False splits disjoint pieces into a feature each, repeating the attributes (ArcGIS's SINGLE_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, closest
  • how — one_to_one (one output row per target, summarised) or one_to_many (a row per match)
  • keep_all — True keeps targets with no match (ArcGIS's KEEP_ALL); False drops them
  • fields — which join-layer fields to carry across. Omit for all of them
  • stats — [[field, stat], ...] for one_to_one, since several matches have to collapse into one row
  • search_radius / unit — only meaningful for match="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 to dist_<near layer>_<unit>, so measuring one layer against several gives you a column per comparison rather than a collision
  • unit — km (default), meters, miles, feet, nm
  • max_distance — in unit. Beyond it the column is null, not a sentinel value, and the result warns how many features had no neighbour in range
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 geodatabase
  • output_path — destination file. Extension sets the format. Use trailing / for directory output (one file per layer). Use dot notation for named layers: roads.parcels.gpkg
  • x_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.10.5 — 2026-10-02

Added

  • Graphics Ready carries a title and a 1:x scale. Two assistants' maps of one folder came back with neither. The title is dark ink top-left; the 1:x sits just above the scale bar.
  • The printed 1:x rounds to a figure you would say: 1:12,345 reads 1:12,500 and 1:523,655 reads 1:525,000. The bar still draws from the exact scale.
  • Atlas Lite puts the title, legend and scale in the negative space, measured from where the data is drawn. The zoom is never changed. On the Bakken page the legend had sat over the densest wells; all three now land on 0% data.
  • Atlas Lite asks before keeping a busy off-subject layer on a map coloured by a field (one printed QUESTION, answered with --leave-out), instead of deciding silently.

Fixed

  • The 1:x never printed on a full-bleed page. It did not float, so it drew under the map.
  • Lines no longer run through the furniture: the title, 1:x and scale figures are haloed.
  • Atlas Lite framed a no-AOI folder on the county lines it added, 10 degrees wide around 4 degrees of data. It frames on the DSUs now.
  • 8,017 DSUs at 1.5 pt printed as a black sheet, and 29,869 laterals as solid colour. Both thin to fit the page (0.25 pt and 0.36 pt on the Bakken page).
  • Context layers took palette colours beside a coloured layer (Operator Land in Continental's purple). They step back to grey and keep their legend row.

v1.10.4 — 2026-10-01

Changed

  • The legend names what a layer is, not the file it came from. env_shp-Wells-WellLateral-88569_2026-08-25 reads Well Laterals, psuedo-dsu reads Pseudo DSUs, AOI_Master reads Master AOI. Display only: the layer keeps its real name, and a caption typed on the layout still wins. Atlas Pro and Atlas Lite both.
  • A classed layer's field is headed in words: envoperatr reads Operator. It is matched by the same resolver the tools use, so the legend and Claude agree on what a column means.

v1.10.3 — 2026-09-29

Added

  • Atlas Lite colours by a field: --color-by operator (or Wells=operator), plus --label. It had one style per layer, so "colour the wells by operator" was reported as impossible.
  • --apply runs Atlas Pro's styling tools with their own arguments: symbology, style, colour, query, reorder, rename, legend, remove. Framing, the page and the basemap stay Lite's.
  • Atlas Lite adds US county lines by default, from the public USA Census Counties service, fetched only for the area each page shows. Drawn 0.8 pt Gray 40%, unfilled, the same values as Atlas Pro's Add ENV Layer. --no-counties leaves them out.
  • atlas_set_symbology takes top, within and rank_with. 300 operators against a 7-colour ceiling becomes the six commonest on the page plus one grey Other (n) in the legend, and layers ranked together give one operator one colour on every layer.

Fixed

  • "operator" did not find envoperatr. Enverus columns drop letters as well as adding the ENV prefix. The field matcher now accepts an abbreviation, and atlas_set_symbology resolves its field the way atlas_query_layer already did instead of refusing.
  • Two matching columns now pick the one holding twice the values (34,596 against 7,829) rather than refusing. Closer than that is still refused, naming both.
  • Atlas Lite asked for counties in the wrong units. Web Mercator AOIs were sent as degrees and matched nothing. Lite now converts every extent to EPSG:4326 first.

v1.10.2 — 2026-09-29

Fixed

  • Atlas Lite ran out of memory on a normal folder. Two well layers of ~36,000 laterals sent ~550 MB of JSON to the browser, over 90% of it attribute columns the map never draws (about 2,300 bytes per feature against 116–408 bytes of geometry). The run died at 4.6 GB. A headless render now sends only the fields the map is styled, labelled or filtered by: 78.6 MB becomes 8.7 MB. Atlas Pro still sends every attribute.
  • --basemap was accepted and then discarded. The basemap was applied one poll before the page layout, and Graphics Ready's own none then overwrote it. The layout request now carries its basemap, and a basemap you name beats the template's.
  • With no internet, Atlas showed an empty page. It waited for MapLibre's load event, which needs the basemap tiles, so without tiles no layers or layout were ever drawn. It now starts once the style is ready. Only the basemap goes missing.
  • A legend request that arrived before the page loaded was thrown away. It was marked done, then discarded because no layout was open yet. It now waits for the page.
  • An area of interest was drawn as background: a 0.8 pt grey hairline, left out of the framing.
  • A renamed copy of a dataset was drawn twice. An export and the .zip it came from (36,454 features each) took two colours and two legend rows.

Changed

  • MapLibre is bundled with the package instead of downloaded from unpkg every time the app opens. It is pinned to 4.7.1, with its BSD-3 licence alongside.
  • Areas of interest draw as a 2 pt Mars Red outline, unfilled, in both Atlas Pro and Atlas Lite.
  • Atlas Lite adds a light-gray basemap by default, so a map shows where it is. --basemap none gives the plain background. If the basemap tiles can't be downloaded, the run says so and names the host it couldn't reach.
  • Atlas Lite frames the map on the AOIs, with one page per group of AOIs: two AOIs 900 km apart get two maps, each with a legend listing only its own layers.

v1.10.1 — 2026-09-28

Added

  • Atlas Lite ships. python ship/package_skill.py lite builds atlas-lite-v<version>.skill; the packager was hard-coded to Pro. Lite's installer is generated from Pro's and asserted byte-identical, and make_map.py now hands over to <skill>/venv before importing.
  • preflight.py --with-browser installs playwright and chromium. pip install playwright downloads no browser: the import works and the first real run fails with "Executable doesn't exist". The ~150 MB download is a separate step, and this flag does it.

Fixed

  • A fixed-treatment layer used up a palette slot. A DSU walked the palette, took slot 0, then had 1.5 pt black drawn over it, so the real subject got the second colour: a DSU and a wells layer drew the wells purple. The subject now keeps slot 0 whatever order they load in.
  • The layer note said "name matches element" — an internal role name, which on a layer called Wells reads like a chemical element. Now: "a data layer, so it takes a palette colour".
  • The legend printed each plain layer's name twice (a bold heading, then the same name beside the swatch). One row per layer now; layers with classes keep their heading.
  • The legend box stayed at the template's full height. Two layers left an empty white panel a third of the page tall. It now shrinks to fit, top edge fixed, and the ring, handles and drag follow the new size. Resize it by hand and it keeps your size.

v1.10.0 — 2026-09-24

Changed

  • Atlas is the Enverus ONE design system. Both surfaces — Enverus Dark and Enverus Light, picked in Settings — now carry the platform's own shadcn/ui token contract in OKLCH. There is no separate Atlas palette any more. The setting keys stay dark/light, so no saved setting and no or 'dark' fallback was orphaned.
  • The accent stopped being a hex value typed into component rules. It was hard-coded 104 times across 94 rules, so the first pass repainted every pane and left every active tab, focus ring, toggle and send button the old blue. Six tokens now — --ac, --ac-hi, --ac-ink, --ac-deep, --ac-deep-hi, --ac-line — and verify.py refuses an accent hex outside a :root block.
  • ONE's primary is used as a fill, not an ink. Its own --primary-foreground measures 4.49:1 on it — a hundredth under AA — so labels on the solid accent are white (4.70:1), and the accent used as ink on a tint is lifted to a measured value (6.94:1 dark, 7.18:1 light).
  • Selection stays blue, deliberately. Atlas draws oil green; a green selection highlight could not be told from a green data layer.

Fixed

  • The start page's primary button never followed the light theme, for the life of the page. Six selectors named a theme by hand, so a theme change meant six edits and the ones you forget fail silently. All six are tokens now, and verify.py refuses a theme name in a selector in either page.
  • Eleven regression suites crashed instead of skipping when the Montney dataset is absent, reporting 422s and IndexErrors that cannot be told from a real regression. test_bugs_0922 also still named testing/regression/ after the reorg moved it to dev/, and test_eagleford_roundtrip hard-coded 3 layers against a project that now holds 5.

v1.9.4 — 2026-09-22

Added

  • Claude can narrow a layer by attribute — atlas_query_layer and atlas_field_values, so twenty-three tools. Asked to "query PWI to only Austin Chalk" it reported it had no filter tool, about an application whose Query Layer panel has shipped for releases. atlas_field_values gives the exact spelling first, because Austin Chalk against AUSTIN CHALK matches nothing and looks like a layer that vanished.
  • A project remembers its Claude conversation, saved into the .atlas capped at 400 events and handed back to the CLI with --resume.

Fixed

  • A new project kept the previous one's chat. "Blank map" and File ▸ New left the transcript and its resume_id in place, so the next message carried on inside a different .atlas; Start over emptied the pane while the model carried on where it was. One _clear_project_scope() on every new-project route, and the conversation is versioned so the browser can see a replacement it is structurally blind to.
  • An export stopped fetching layers it will not draw — 140 MB of US Counties downloaded to draw nothing, because the walk included hidden layers and layers outside the view.

v1.9.3 — 2026-09-22

Added

  • Claude can narrow a layer by attribute — atlas_query_layer and atlas_field_values, so twenty-three tools. Asked to "query PWI to only Austin Chalk" it accurately reported it had no filter tool, about an application whose Query Layer panel has shipped for releases. A filter hides rows; symbology colours them and hides nothing. atlas_field_values gives the exact spelling first — Austin Chalk against AUSTIN CHALK matches nothing and looks like a layer that vanished, so the reply counts what matched and says so when it is zero. The field name is inferred as well: "interval" finds ENVInterval, "county" finds ENVCounty, and the reply names the column it used. Anything ambiguous is refused with the candidates listed rather than guessed at.
  • A table in a geodatabase loads as a table, instead of 422 'DataFrame' object has no attribute 'crs'. The container listing already reports a table as geometry_type: None, so it routes before the read. It lands in Tables rather than on the map — it has no coordinates to put it anywhere — and the toast says so and what + is for.
  • A project remembers its Claude conversation, and can carry it on. Events lived only in the bridge's in-memory list, so a reload replayed them and a restart lost them, while nothing reached the .atlas file — reopening a project showed an empty pane beside the map it built. The transcript now saves with the project, capped at the last 400 events.
  • The CLI's session_id, captured from the wire since 1.4.0 and never used, is saved and handed back with --resume, so a reopened project resumes the model's own context. Started lazily on the first message, not during the open, which would cost ~12s for a pane nobody touched. A refused id retries once without it: a fresh session beats a pane that answers nothing.

Changed

  • An export stops fetching layers it will not draw. ensureFullResolution() looped over the whole registry, including hidden layers and layers with nothing in the exported view -- 140 MB of US Counties downloaded to draw nothing. Skipped only when the answer is certain.
  • What a Claude turn did is folded away, and described in words. One disclosure line reads "Looking at the map · Loading layers · Framing the map — 8 actions" and expands to the calls unchanged. Closed it is 17 px and the call name is absent from the pane's text; open, 81 px. The raw argv used to sit above the sentence that answered the request.

Fixed

  • A conversation leaked between project files. The persistence above covered project_open and nothing else: "Blank map" and File > New left the previous project's chat and its resume_id in place, so the next message carried on that session inside a different .atlas and the next save wrote it there. One function now holds everything scoped to a file, and every new-project route calls it. Save As keeps the chat, being the same project renamed.
  • Start over did not start over — claude_reset left resume_id, the field handed to --resume, so the pane emptied while the model carried on from where it was.
  • A restored transcript was invisible to the pane. Its cursor is append-only, so a cursor of 40 against a restored 12 events asks past the end and gets none — the previous project's chat stayed on screen until a reload. The conversation is versioned now and the pane rewinds.
  • A project with nothing said saved an empty conversation block, because an empty transcript is still a truthy dict.
  • An .aix exported with no explicit name wrote DJ Basin.aix.aix — the duplicate extension was stripped before the default was substituted, so it only protected a name the caller typed, and the .aix default already ended in .aix.
  • A subplay was not recognised while a DSU beside it was, because the tier-one registry entry carries no filename aliases, so one pulled from the lake or dragged off a disk walked the brand palette. It has a name-only tier now: 1.5 pt Yogo Blue, unfilled. Reverses 1.7.0, which left subplays out on the reasoning that a subplay file is the map's subject.
  • The treatment was applied and then repainted structural grey one line later, in all three load paths — measured #595a5a on a layer whose treatment says #73B2FF. A DSU escaped only because eirstyle calls it role element and a subplay role outline, which is why the first fix appeared to do nothing.
  • DJ_SubPlay2026 tokenised as subplay2026 and missed whole-token matching. A trailing year or version is stripped and added alongside the real word, so subplayer, replay and subway stay out. Seventeen real name shapes in the suite.
  • The MCP reply described a colour it had not drawn — the treatment's own reason is recorded at the point of decision and quoted back.
  • The Symbology pane did not follow the Contents selection, so the docked pane and the ribbon panel disagreed about what you were editing. An open pane follows; a click cannot open one.
  • test_env_infer had been failing for two releases, asserting twenty MCP tools where atlas_set_legend made twenty-one in 1.9.1. Derived from MCP_TOOLS now.

v1.9.2 — 2026-09-17

Fixed

  • Two loading bars drew their track and never filled — the fill used var(--ac-2) and index.html defines no such token, so it resolved to nothing. landing.html does define one, which is why the recent-project card animated and these did not. The same invented token silently killed the drag-onto-a-container highlight. verify.py now refuses a bare var(--x) on a page that does not define --x.
  • The .aix export button lost its icon — .r-big-btn holds an <svg> plus a label, so assigning textContent replaces every child with one text node.

v1.9.1 — 2026-09-15

Added

  • Drag a loaded layer or table into a .gdb or .gpkg from the Catalog tree. An empty geodatabase is not a special case — a folder named X.gdb has no system tables, so it is created on the first drop and appended to after (GDAL OpenFileGDB DCAP_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.
  • A Legend tab in the Layout ribbon (title, layers, columns, patch, text, frame) and atlas_set_legend so Claude can drive it — 21 tools. Everything in millimetres of paper; options live on the layout.
  • A loading bar in the Layers panel while geometry is in flight, and the + on the Layouts category brings the Layout ribbon tab with it.

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.
  • scripts was 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 .gdb could 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

  • BRAND is 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 index BRAND[7] or BRAND[8].
  • Native dropdowns follow Atlas's theme rather than the OS.

Fixed

  • Two different greys shipped in one package: NEUTRON_GREY was #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.py defaulted 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 enverus catalogue — 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. dedupe draws 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 = True was 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 .gdb under 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-left on 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 — Prelim is V0 and any V_<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 localStorage across 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_boundaries and WellInv_MT_DSU_V_0819 hit and hdsurvey does not, and it overrides a requested colour.
  • The workspace is saved with the project — registered geodatabases, folder connections and reference folders live in the .atlas file 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.py classified .gdb as readonly; it is now full, with directory-aware backup/restore, an OpenFileGDB write path, an explicit geometry_type derived from the data (OpenFileGDB refuses Unknown (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-group had a min-width floor 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 .gdb on the map registers it under Databases — it used to do nothing usable, because a browser hands a dropped directory over as its .gdbtable files. 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 .atlas file 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.py classified .gdb as readonly, so every edit endpoint returned 422. .gdb is now full; the backup helpers use copytree, and _write_gdf writes via driver="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_MULTI does not resolve Unknown, measured. 540 mixed polygons out whole, area delta 6.4e-10%.
  • A .gdb handed to Atlas answered "holds no spatial files", listing its own .gdbtable plumbing, 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 .gdb or .gpkg, with geometry type, count and fields.
  • layer= on env.morph names 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

  • .aix export — 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 .atlas file. .atlas.json still 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_geojson no 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=1 is 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, and parseInt("0.8") is 0, 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_layers was 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, but verify.py now 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/fingerprint was unusable here at 12 ms cold, because it hashes every feature; each layer's _v counter carries the same information. Layout elements live in the browser, so they report their own hash through POST /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.py and 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 (a SubPlay or Play column 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 -progress ticker — which needed an incremental read, since capture_output buffered 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_style can set the fill colour. It took outline and not color, 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 fill in Settings turns it off.
  • The map is one world wide — renderWorldCopies: false, so dragging east stops instead of repeating Alaska forever. Not maxBounds: 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 .dbf is one, and stops being one when a .shp shares 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 on outlineColor — so no slot ever read as taken. It compares the effective colour now.
  • POST /api/views 500s if given a basemap — ViewCreate had no such field while ViewUpdate has 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 via shapely.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 with allow_nan=False), missing and empty geometry (both of which to_json writes 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 urlopen on localhost, which waits out a timeout on ::1 and then 127.0.0.1 when 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 in databricksIncludeAll. 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 databricksVolumes and 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 and read_files() knows nothing about shapefiles. atlas_databricks_browse gives Claude the same route, and a /Volumes/... path goes straight to atlas_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_add routes on the manifest rather than refusing.

Fixed

  • The selection highlight drew underneath every layer — created on style.load before 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 only SELECT. 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_read now.

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.py refuses duplicate function definitions across every module and index.html, since a redefinition is silent in both languages.

v1.4.3 — 2026-08-25

Added

  • atlas_set_basemap, including none — 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 because setStyle rebuilds 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_layer and atlas_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_references takes a path, 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_state names 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, a V2_0812 and a V_0819 are 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 .zip of 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 .shx rebuild — a write — inside the read-only /vsizip/ filesystem; the driver then reported no layer and pyogrio raised a bare IndexError. Scoped to bare .shp files now.
  • Changing a symbology field kept the old field's colours — valueColors is derived from the field, and the 1.4.1 merge carried it across, so envoperatr —> vintage left every well Devon-coloured.
  • A template's own scale bar never re-labelled on zoom, because the repaint keyed on an id that 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_layers loaded everything in it silently, counting a shapefile and a zip of it twice.
  • atlas_set_layout says 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.
  • referenceRoots setting, 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_symbology now 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) on rgb(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.
  • /mcp on 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.py derives 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_fill is a colour, not an opacity of zero.
  • Non-ASCII characters in replies were mangled: the child process emits UTF-8 and text=True decoded 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. getMapScale divided 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 own project() and the WGS84 great-circle distance: 1:2,037,579 true versus 1:4,420,000 printed. There is now a separate getPaperScale() 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-aware quick_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_type on 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; fit uses 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 from BRAND order.
  • 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 fitBounds all 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,000 as 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 .pagx originals 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-above is z-index 103 and the map wrapper is 102, so anything placed without float that 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.prefix has no conda-meta, so the conda check failed while conda sat in sys.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 runs conda install gdal into 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 one preflight.py line, with the !-prefixed variant for the Claude prompt — and SKILL.md tells Claude to hand the command over rather than retry or touch settings.json. No library code changed.

v1.2.2 — 2026-08-17

Fixed

  • The README documented three operations out of seven. Union, Dissolve, SpatialJoin and Near had 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, Union and Intersect note that they take a list, and the async list names every variant instead of claiming there are three.

    Two corrections while writing them: Near was about to be described with ArcGIS's NEAR_DIST / -1 convention, which it does not use — it writes one column named dist_<near layer>_<unit> and leaves it null beyond max_distance — and the Spatial Join match list is all seven options from SJ_MATCH, not three and an "and the rest".

    docs/verify.py now derives the expected list from dir(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 own FID_ column through the whole fold, so the output still says which layer made each piece. POST /api/geoprocess gained other_layers; the old other_layer still works and the two are merged.
  • POST /api/export/pdf builds a real PDF server-side. POST /api/databricks/open-token-page and POST /api/open-url open 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/prompt calls 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.open either — 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-stats coerces strings and returns null when 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 match compares type as well as value, so a shapefile's Index, FID or 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 equalled 1204 in 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.open does 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-motion is 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, mirroring arcpy.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), and dropped is clamped at zero because Union grows the feature count
    • join_attributes (dropdown): ALL / NO_FID / ONLY_FID, with arcpy's spellings. FID_<layer> carries the source id, or -1 where that layer contributed nothing to the piece
    • gaps (checkbox): ticking it gives each enclosed void a feature with every FID_ at -1, so gaps can be selected — ArcGIS's NO_GAPS
    • cluster_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 RuntimeError about 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 12px repeated 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 databricksSkipCatalogs setting; a skipped catalogue also disappears from the pane
    • new GET /api/databricks/catalogs and POST /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
  • 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_sql hands 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 BINARY geometry out there is PostGIS EWKB, and Databricks' own ST_GeomFromWKB rejects its SRID flag — so no server-side ST_ call is possible. Atlas selects it raw, reads it with shapely.wkb (bytes or hex, drivers differ) and clips the extent after the rows land, saying so in note because 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 wkt works against a column declared WKT

  • gdb_geomattr_data, geometrysource, blockgeometrylink and geom_gml are no longer offered as geometry — 32 columns that LIKE '%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 600 on POSIX, where the default 644 would 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: GET returns bullets, and sending bullets back does not overwrite the stored value. The input is type=password and is emptied once saved
    • scrubbed from every error message — run_sql is 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
  • 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 databricksTokenPage setting 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-connector is 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. Marked python_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 existing env.pull users need no setup. The token is write-only over the API and never returned by GET /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.6 graduated), 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:hidden box — and the position:fixed map 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.on hardcoded navy-on-pale-blue, visible on the new Pull Extent / Pull All toggle and every Settings switch. Now on the --accent-bg/--accent-tx pair, 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_grid tables carry gdb_geomattr_data; in landgrid_texas_blocks it 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 can EXCEPT it by name (necessary because EXCEPT errors on absent columns). 25 rows: 1.2s instead of 3.4s. Any binary attribute that slips through anyway is dropped and named in note

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_2 on 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.json and reopen it rendering identically. Declarative specs, no geometry, so it stays a few kilobytes; layers with no file of their own are cached into a MyProject.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.py skip conda's activation scripts, causing Warning 3: Cannot find tms_*.json and quietly degraded CRS lookups
  • An inferred text length was enforced as a constraint, refusing to lengthen "CHEVRON" to "CHEVRON USA"
  • A layer inside a .gdb was 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 .prj resolves its own CRS — dragging a lone .shp gives no .prj to 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 .prj always 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/upload rejected 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 on intersects / within / contains / crosses / overlaps / touches / closest; one-to-one (with stats={"FIELD": "sum"} aggregation) or one-to-many; keep_all for left vs inner join; adds Join_Count, renames colliding fields, and closest adds dist_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/meta plus per-layer /geojson with ETag caching
  • GDAL/PROJ auto-configuration — GDAL_DATA and PROJ_LIB resolved 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 inf attribute 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/dropEffect meant the drop event never fired
  • Shapefile export was unusable — only the .shp was 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. unit in km/meters/miles/feet/nm, max_distance nulls 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() or serve(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> and connect() all unchanged
  • Atlas: SVG export upgrades — new bg argument (none transparent default, light, dark, or hex; was hardcoded navy) and layer groups now carry real names via id / 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 no valueColors matched 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.html cache 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 with upload_layer + set_viewport for 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.zip to 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 load
    • Buffer — OGR Python API with auto-UTM zone selection; result in original CRS
    • Intersect — 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 operations
  • morph spatial→spatial via ogr2ogr — file-to-file conversions no longer load full dataset into memory
  • morph_async() — async variant of env.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; optional fields list to limit what's added
  • client.validate_join() — preview match stats without modifying any layer
  • client.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 .shx regenerated 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.10.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for env-able 1.10.5
File Size Uploaded
env_able-1.10.5.tar.gz 2.4 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for env-able 1.10.5
File Interpreter ABI Platform
env_able-1.10.5-py3-none-any.whl Python 3 none any Details

Total release size: 4.6 MB

Release files / env_able-1.10.5.tar.gz

Download URL env_able-1.10.5.tar.gz
Size 2.4 MB
Tags Source
SHA-256 checksum
How to use checksums
a15b19fef95f3c7bade5e285db5f93fccb7cf2af24dcfe9d4d04aa3c95402bf2
BLAKE2b-256 checksum
How to use checksums
1d17f603c09f84dae5a031f71d61d9bf418b288df05c73f4dad3354e21f9d406
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.10.5-py3-none-any.whl

Download URL env_able-1.10.5-py3-none-any.whl
Size 2.3 MB
Tags Python 3
SHA-256 checksum
How to use checksums
2219c83ca5965490cf0fea37c281e7fec5e2e933b496d639fde653dc5232707e
BLAKE2b-256 checksum
How to use checksums
a0f614b5ba5677f567cba07656c2573df02c64925b1c2d170b8add41c8571d94
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.9

Release history Release notifications | RSS feed

2.0.1

2 release files

2.0.0

2 release files

This release

1.10.5 This release

2 release files

1.10.3

2 release files

1.10.2

2 release files

1.10.1

2 release files

1.10.0

2 release files

1.9.4

2 release files

1.9.3

2 release files

1.9.2

2 release files

1.9.1

2 release files

1.9.0

2 release files

1.8.2

2 release files

1.8.1

2 release files

1.8.0

2 release files

1.7.3

2 release files

1.7.2

2 release files

1.7.1

2 release files

1.7.0

2 release files

1.6.0

2 release files

1.5.0

2 release files

1.4.6

2 release files

1.4.5

2 release files

1.4.4

2 release files

1.4.3

2 release files

1.4.2

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.2

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.4

2 release files

1.2.3

2 release files

1.2.2

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.15.0

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.2

2 release files

0.10.0

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.5

2 release files

0.8.4

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.3.6

2 release files

0.3.3

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page