diff --git a/.gitignore b/.gitignore index af0a9e44..5476007b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ autk*/docs/ autk*/dist/ autk*/src/*.js dist/ +python/autark/static/ .vite/ @@ -20,6 +21,12 @@ autk*/package-lock.json .cache .eslintcache .stylelintcache +__pycache__/ +*.py[cod] +.ipynb_checkpoints/ +python/build/ +python/*.egg-info/ +python/autark.egg-info/ # Environment variables .env @@ -46,6 +53,7 @@ npm-debug.log* # Playwright output tests/report/ tests/results/ +**/test-results/ # Test data **/public/data-ignore/* diff --git a/Makefile b/Makefile index 6a217f99..490d67d7 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ typecheck: build "cd autk-db && npx tsc --noEmit --skipLibCheck" \ "cd autk-plot && npx tsc --noEmit --skipLibCheck" \ "cd autk-compute && npx tsc --noEmit --skipLibCheck" \ + "cd autk-runtime && npx tsc --noEmit --skipLibCheck" \ "cd autk && npx tsc --noEmit --skipLibCheck" \ "cd gallery && npx tsc --noEmit --skipLibCheck" \ "cd usecases && npx tsc --noEmit --skipLibCheck" @@ -29,6 +30,7 @@ build: "cd autk-plot && npm run build" \ "cd autk-compute && npm run build" cd autk && npm run build + cd autk-runtime && npm run build docs: $(CONCURRENTLY) \ @@ -63,6 +65,7 @@ dev: "cd autk-plot && npm run dev-build" \ "cd autk-compute && npm run dev-build" \ "cd autk && npm run dev-build" \ + "cd autk-runtime && npm run dev" \ "cd $(APP) && VITE_OPEN=\"$(OPEN)\" npm run dev" clean: @@ -74,5 +77,6 @@ clean: "cd autk-plot && $(RIMRAF) dist build node_modules" \ "cd autk-compute && $(RIMRAF) dist build node_modules" \ "cd autk && $(RIMRAF) dist build node_modules" \ + "cd autk-runtime && $(RIMRAF) dist build node_modules" \ "cd gallery && $(RIMRAF) dist build node_modules" \ "cd usecases && $(RIMRAF) dist build node_modules" diff --git a/PYTHON_API.md b/PYTHON_API.md new file mode 100644 index 00000000..810ce3e6 --- /dev/null +++ b/PYTHON_API.md @@ -0,0 +1,645 @@ +# Autark Python API + +Status: MVP implemented and tested as of 2026-06-10. + +The Python API lets users author Autark visual analytics specs with Python +objects, serialize them to AutarkSpec JSON, and execute them in the browser +runtime. The browser side owns DuckDB-WASM, WebGPU rendering, plots, transforms, +and linked interaction; Python owns ergonomic spec construction, validation, and +notebook/export workflows. + +## What Was Built + +### Spec Authoring + +- `ak.Spec` / `ak.AutarkSpec` for top-level specs. +- `ak.Metadata` and `ak.Workspace`. +- JSON serialization through `to_dict()`, `to_json()`, and `save_json()`. +- JSON Schema validation through `spec.validate()`. +- Standalone HTML export through `spec.save_html()`. +- Jupyter display through `_repr_html_()` and the bundled anywidget path. + +### Data Sources + +Runtime-backed data sources: + +- `ak.OSM` +- `ak.GeoJSON` +- `ak.CSV` +- `ak.JSON` (as of 2024-12-20) +- `ak.GeoTIFF` (as of 2024-12-20) + +Python-side helpers: + +- `ak.GeoJSON.from_dataframe()` converts pandas-like tabular data with + latitude/longitude columns into inline GeoJSON points. +- `ak.GeoJSON.from_geopandas()` converts GeoPandas-like objects into inline + GeoJSON, reprojecting to EPSG:4326 when CRS metadata is available. + +### Transforms + +Runtime-backed transforms: + +- `ak.SpatialJoin` +- `ak.Heatmap` +- `ak.Compute.gpgpu()` +- `ak.Compute.render()` + +Aggregation helpers: + +- `ak.count()` +- `ak.sum()` / `ak.total()` +- `ak.avg()` +- `ak.min()` / `ak.minimum()` +- `ak.max()` / `ak.maximum()` +- `ak.weighted()` +- `ak.collect()` for spatial joins + +### Views + +Runtime-backed views: + +- `ak.Map` +- `ak.Histogram` +- `ak.Scatterplot` +- `ak.Table` + +Map helpers: + +- `ak.Layer` +- `ak.Camera` +- `ak.Layout` + +Encoding helpers: + +- `ak.field()` +- `ak.value()` +- `ak.Field` +- `ak.Value` +- `ak.Scale` + +Interaction helpers: + +- `ak.interval()` +- `ak.point()` +- `ak.multi()` +- `ak.Link` + +## Installation + +The Python package is currently installed from the monorepo: + +```bash +cd python +pip install -e . +pip install -e ".[validation]" +pip install -e ".[widget]" +``` + +Use the `validation` extra for `spec.validate()`. Use the `widget` extra for +zero-server Jupyter rendering with `spec.widget()`. + +Python package builds use Hatch and `hatch-jupyter-builder` to run +`npm run build:widget` in `autk-runtime` and include the generated +`autark/static/autark-widget.js` bundle in the wheel. + +## Basic Usage + +```python +import autark as ak + +neighborhoods = ak.GeoJSON( + "neighborhoods", + url="/data/neighborhoods.geojson", + layer_type="polygons", + coordinate_format="EPSG:4326", +) + +spec = ak.Spec( + metadata=ak.Metadata(title="Neighborhoods"), + workspace=ak.Workspace(coordinate_format="EPSG:3857"), + data=[neighborhoods], + views=[ + ak.Map( + camera=ak.Camera(zoom=12), + layers=[ + ak.Layer(neighborhoods, type="polygons").style( + color="#2f6f73", + opacity=0.75, + strokeColor="#123456", + ) + ], + ) + ], +) + +spec.validate("../schema/autark-spec-v0.1.json") +spec.save_json("neighborhoods.json") +spec.save_html("neighborhoods.html") +``` + +## TypeScript Versus Python Usage + +There are two useful comparisons: + +- TypeScript can execute an AutarkSpec directly with `AutarkRuntime`. +- TypeScript can also use the lower-level imperative packages (`AutkDb`, + `AutkMap`, `AutkPlot`, `AutkCompute`) when building a custom application. +- Python is the ergonomic authoring layer for notebooks, scripts, generated + specs, and HTML/widget export. + +### Same Spec, TypeScript Runtime + +```typescript +import { AutarkRuntime } from '@urban-toolkit/autk-runtime'; + +const spec = { + $schema: 'https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json', + version: '0.1', + data: [ + { + type: 'geojson', + name: 'neighborhoods', + url: '/data/neighborhoods.geojson', + layerType: 'polygons', + coordinateFormat: 'EPSG:4326', + }, + ], + views: [ + { + type: 'map', + layers: [ + { + source: 'neighborhoods', + type: 'polygons', + style: { + color: '#2f6f73', + opacity: 0.75, + strokeColor: '#123456', + }, + }, + ], + }, + ], +}; + +await AutarkRuntime.fromSpec(spec, { + container: document.getElementById('app')!, +}); +``` + +### Same Spec, Python Builder + +```python +import autark as ak + +neighborhoods = ak.GeoJSON( + "neighborhoods", + url="/data/neighborhoods.geojson", + layer_type="polygons", + coordinate_format="EPSG:4326", +) + +spec = ak.Spec( + data=[neighborhoods], + views=[ + ak.Map( + layers=[ + ak.Layer(neighborhoods, type="polygons").style( + color="#2f6f73", + opacity=0.75, + strokeColor="#123456", + ) + ] + ) + ], +) + +spec.save_json("neighborhoods.json") +spec.save_html("neighborhoods.html") +``` + +### Imperative TypeScript Workflow + +The imperative TypeScript API is lower-level and useful for custom web apps. +The same spatial-join workflow requires explicitly loading data, running the +query, creating the map, loading layers, applying thematic styling, and drawing. +This mirrors examples such as `gallery/src/autk-map/spatial-join.ts`. + +```typescript +import { AutkDb } from '@urban-toolkit/autk-db'; +import { AutkMap } from '@urban-toolkit/autk-map'; +import { + ColorMapDomainStrategy, + ColorMapInterpolator, +} from '@urban-toolkit/autk-core'; + +const db = new AutkDb(); +await db.init(); + +await db.loadGeojson({ + geojsonFileUrl: '/data/neighborhoods.geojson', + outputTableName: 'neighborhoods', +}); + +await db.loadCsv({ + csvFileUrl: '/data/trees.csv', + outputTableName: 'trees', + geometryColumns: true, +}); + +await db.spatialQuery({ + tableRootName: 'neighborhoods', + tableJoinName: 'trees', + groupBy: [{ column: '*', aggregateFn: 'count' }], +}); + +const map = new AutkMap(document.querySelector('canvas')!); +await map.init(); + +const neighborhoods = await db.getLayer('neighborhoods'); +map.loadCollection('neighborhoods', { + collection: neighborhoods, + type: 'polygons', +}); +map.updateColorMap('neighborhoods', { + colorMap: { + domainSpec: { type: ColorMapDomainStrategy.MIN_MAX }, + interpolator: ColorMapInterpolator.SEQ_GREENS, + }, +}); +map.updateThematic('neighborhoods', { + collection: neighborhoods, + property: 'properties.sjoin.count.trees', +}); + +map.draw(); +``` + +### Equivalent Python Declarative Workflow + +```python +import autark as ak + +neighborhoods = ak.GeoJSON( + "neighborhoods", + url="/data/neighborhoods.geojson", + layer_type="polygons", +) +trees = ak.CSV( + "trees", + url="/data/trees.csv", + geometry=ak.latlng("latitude", "longitude", coordinate_format="EPSG:4326"), +) + +spec = ak.Spec( + data=[neighborhoods, trees], + transforms=[ + ak.SpatialJoin( + root=neighborhoods, + join=trees, + group_by=[ak.count()], + ) + ], + views=[ + ak.Map( + layers=[ + ak.Layer(neighborhoods, type="polygons").encode( + color=ak.field( + "properties.sjoin.count.trees", + scale=ak.Scale(type="quantile", scheme="greens"), + ) + ) + ] + ) + ], +) +``` + +## GeoPandas Workflow + +```python +import autark as ak +import geopandas as gpd + +gdf = gpd.read_file("neighborhoods.geojson") +gdf = gdf[gdf["population"] > 10000] + +source = ak.GeoJSON.from_geopandas("neighborhoods", gdf) + +spec = ak.Spec( + data=[source], + views=[ + ak.Map( + layers=[ + ak.Layer(source, type="polygons").encode( + color=ak.field( + "population", + scale=ak.Scale(type="quantile", scheme="viridis"), + ) + ) + ] + ) + ], +) +``` + +A complete example lives at +`python/examples/geopandas_workflow.py`. + +## Linked Map And Plot + +```python +import autark as ak + +points = ak.GeoJSON("points", url="/data/points.geojson", layer_type="points") +brush = ak.interval("value_brush") + +spec = ak.Spec( + data=[points], + views=[ + ak.Map(layers=[ak.Layer(points, type="points")]), + ak.Histogram(points, x="value", bins=20, selection=brush), + ], + links=[ + ak.Link(source=brush, target="points", action="highlight"), + ], + layout=ak.Layout(type="vertical"), +) +``` + +## Scatterplot And Table + +```python +import autark as ak + +points = ak.GeoJSON("points", url="/data/points.geojson", layer_type="points") +selection = ak.interval("scatter_brush") + +spec = ak.Spec( + data=[points], + views=[ + ak.Scatterplot( + points, + x="density", + y="value", + color="category", + selection=selection, + ), + ak.Table( + points, + columns=["id", "category", "density", "value"], + sort={"column": "value", "direction": "desc"}, + ), + ], +) +``` + +## Spatial Join + +```python +import autark as ak + +neighborhoods = ak.GeoJSON("neighborhoods", url="/data/neighborhoods.geojson", layer_type="polygons") +trees = ak.GeoJSON("trees", url="/data/trees.geojson", layer_type="points") + +spec = ak.Spec( + data=[neighborhoods, trees], + transforms=[ + ak.SpatialJoin( + root=neighborhoods, + join=trees, + near=ak.Near(distance=250), + group_by=[ak.count()], + ) + ], + views=[ + ak.Map( + layers=[ + ak.Layer(neighborhoods, type="polygons").encode( + color=ak.field("properties.sjoin.count.trees") + ) + ] + ) + ], +) +``` + +## Heatmap + +```python +import autark as ak + +points = ak.GeoJSON("points", url="/data/points.geojson", layer_type="points") + +heatmap = ak.Heatmap( + points, + output="points_heatmap", + near=ak.Near(distance=250), + grid=ak.HeatmapGrid(rows=64, columns=64), + group_by=[ak.count("*", as_="point_count")], +) + +spec = ak.Spec( + data=[points], + transforms=[heatmap], + views=[ + ak.Map(layers=[ak.Layer("points_heatmap", type="polygons")]) + ], +) +``` + +## GPGPU Compute + +```python +import autark as ak + +points = ak.GeoJSON("points", url="/data/points.geojson", layer_type="points") + +compute = ak.Compute.gpgpu( + points, + output="computed_points", + variable_mapping={"value": "value"}, + uniforms={"scale": 2.0}, + wgsl_body="return value * scale;", + result_field="score", + layer_type="points", +) + +spec = ak.Spec( + data=[points], + transforms=[compute], + views=[ak.Table("computed_points", columns=["score"])], +) +``` + +`Compute.render()` is also wired for the render-based compute transform. It is +more verbose because the runtime needs layer, viewpoint, aggregation, camera, +and tile-size configuration. + +## JSON Data Source + +```python +import autark as ak + +# Load JSON from URL +remote_json = ak.JSON("remote_data", url="/data/events.json") + +# Use inline JSON data +inline_json = ak.JSON( + "events", + values=[ + {"id": 1, "name": "Event A", "value": 100}, + {"id": 2, "name": "Event B", "value": 150}, + ], +) + +spec = ak.Spec( + data=[inline_json], + views=[ + ak.Scatterplot( + inline_json, + x="id", + y="value", + color="name", + ), + ak.Table(inline_json, columns=["id", "name", "value"]), + ], +) +``` + +## GeoTIFF Raster Data + +```python +import autark as ak + +# Load GeoTIFF raster +raster = ak.GeoTIFF( + "elevation", + url="/data/elevation.tif", + band=1, # Use first band (1-based index) + coordinate_format="EPSG:4326", +) + +spec = ak.Spec( + data=[raster], + views=[ + ak.Map( + layers=[ + # Note: Raster layer support depends on runtime implementation + ak.Layer(raster, type="raster").encode( + color=ak.field( + "value", + scale=ak.Scale( + domain=[0, 1000], + range=["green", "brown", "white"], + ), + ) + ) + ] + ) + ], +) +``` + +## Jupyter Usage + +For the bundled widget path: + +```python +w = spec.widget() +w + +# After interacting with a plot selection: +w.selections +w.observe(lambda change: print(change["new"]), names="selections") +``` + +The widget JavaScript is packaged with the Python wheel. DuckDB WASM and worker +assets still load from the jsDelivr CDN in this mode, so the notebook needs +network access unless those assets are bundled in a future release. + +For development against a local runtime bundle, evaluating `spec` in a notebook +uses `_repr_html_()` and expects the local dev server/runtime URL configured by +the package defaults. + +Development-server display is useful while changing the runtime bundle: + +```bash +cd /path/to/autark +python cors_server.py 8000 +``` + +Then open a notebook from `python/examples` and evaluate a spec object. If the +output is blank, check that `autk-runtime/dist/autk-runtime.js` exists and that +the data URLs used by the spec are reachable from the browser. + +## Examples + +Python examples: + +- `python/examples/simple_geojson_map.py` +- `python/examples/csv_points_map.py` +- `python/examples/spatial_join.py` +- `python/examples/geopandas_workflow.py` +- `python/examples/json_data_example.py` (new) +- `python/examples/geotiff_raster_example.py` (new) + +Spec examples: + +- `examples/specs/01-basic-osm-map.json` +- `examples/specs/02-linked-map-histogram.json` +- `examples/specs/03-spatial-join.json` +- `examples/specs/04-geojson-input.json` +- `examples/specs/05-csv-points.json` +- `examples/specs/06-multiple-layers.json` +- `examples/specs/07-json-data.json` (new) +- `examples/specs/08-geotiff-raster.json` (new) + +Runtime fixtures: + +- `tests/fixtures/runtime/fixture-01-geojson-map.json` +- `tests/fixtures/runtime/fixture-02-csv-histogram.json` +- `tests/fixtures/runtime/fixture-03-osm-har.json` +- `tests/fixtures/runtime/fixture-05-line-style.json` +- `tests/fixtures/runtime/fixture-06-scatter-table.json` + +## Verification + +Useful checks from the repository root: + +```bash +npm run validate:specs +npm run test:runtime +``` + +Python checks: + +```bash +cd python +python -m unittest tests.test_spec_builders +python -m unittest discover -s tests +python -m mypy +``` + +The full Python discovery suite includes notebook/widget tests that start a +local Jupyter kernel, so it may need permission to bind local ports in sandboxed +environments. + +## Current Limitations + +- OSM support works through the runtime and is covered by HAR-backed tests; live + Overpass tests remain manual/skipped by default. +- Large dataset handling is still GeoJSON-oriented. Arrow/Parquet or tiled + raster workflows remain future work. +- Higher-level expression helpers for compute are not implemented; use raw WGSL + bodies through `Compute.gpgpu()`. +- GeoTIFF band selection is defined in the schema but not yet implemented in the + runtime (defaults to first band). + +## Documentation Map + +- `PYTHON_API.md` is the canonical Python API guide and status document. +- `python/README.md` is the package-level quick start. +- `python/examples/README.md` documents runnable examples. +- `PYTHON_API_IMPLEMENTATION.md` is now a compact implementation tracker. diff --git a/PYTHON_API_IMPLEMENTATION.md b/PYTHON_API_IMPLEMENTATION.md new file mode 100644 index 00000000..0fcdcbfb --- /dev/null +++ b/PYTHON_API_IMPLEMENTATION.md @@ -0,0 +1,293 @@ +# Autark Python API Implementation Tracker + +This tracker records implementation status only. For user-facing API +documentation and examples, see `PYTHON_API.md`. + +Checkpoint before documentation consolidation: + +```text +b941f2a feat(python): expand spec builders and runtime support +``` + +## Completed MVP Work + +### Locked Executable Spec Conventions + +- OSM sub-layer tables use `${name}_${layer}`, for example + `manhattan_osm_buildings`. +- MVP spatial joins mutate the `root` table in place. +- Heatmap, GPGPU compute, and render compute transforms write to explicit + `output` tables. +- Map field encodings use full feature-property paths when needed, for example + `properties.height`; plot encodings use table column/property names. +- Link targets resolve to rendered layer/view identifiers. +- `highlight` is the implemented MVP link action. +- Data file references in specs are runtime/browser URLs, not Python filesystem + paths unless served by the current HTML/runtime context. + +### Spec Core + +- `ak.Spec` / `ak.AutarkSpec` +- `ak.Metadata` +- `ak.Workspace` +- `to_dict()`, `to_json()`, `save_json()` +- `save_html()` +- `_repr_html_()` +- `spec.widget()` +- JSON Schema validation with `spec.validate()` + +### Data Sources + +- `ak.OSM` +- `ak.GeoJSON` +- `ak.GeoJSON.from_dataframe()` +- `ak.GeoJSON.from_geopandas()` +- `ak.CSV` +- `ak.JSON` Python builder with runtime support (completed 2024-12-20) +- `ak.GeoTIFF` Python builder with runtime support (completed 2024-12-20) +- CSV lat/lng geometry helper +- CSV WKT geometry helper + +### Transforms + +- `ak.SpatialJoin` +- `ak.Near` +- Aggregation helpers: + - `count` + - `sum` / `total` + - `avg` + - `min` / `minimum` + - `max` / `maximum` + - `weighted` + - `collect` +- `ak.Heatmap` +- `ak.HeatmapGrid` +- `ak.Compute.gpgpu()` +- `ak.Compute.render()` + +### Views + +- `ak.Map` +- `ak.Layer` +- `ak.Camera` +- `ak.Histogram` +- `ak.Scatterplot` +- `ak.Table` +- `ak.Layout` + +### Encodings + +- `ak.field()` +- `ak.value()` +- `ak.Field` +- `ak.Value` +- `ak.Scale` +- Field, value, scale, and style serialization + +### Selections And Links + +- `ak.interval()` +- `ak.point()` +- `ak.multi()` +- `ak.Link` +- Runtime highlight linking for histogram/scatterplot/table driven selections + +### Runtime And Schema Wiring + +- GeoJSON data loading, including inline values +- CSV data loading with geometry options +- OSM loading, with HAR-backed runtime coverage +- Spatial join transform execution +- Heatmap transform execution through `AutkDb.buildHeatmap()` +- GPGPU compute transform execution through `AutkCompute.gpgpuPipeline()` +- Render compute transform execution through `AutkCompute.renderPipeline()` +- Map, histogram, scatterplot, and table rendering +- Widget bundle selection sync +- Hatch/Jupyter build hook generates and packages `autark-widget.js` + +### Examples + +- `python/examples/simple_geojson_map.py` +- `python/examples/csv_points_map.py` +- `python/examples/spatial_join.py` +- `python/examples/geopandas_workflow.py` +- `python/examples/json_data_example.py` (added 2024-12-20) +- `python/examples/geotiff_raster_example.py` (added 2024-12-20) +- `examples/specs/01-basic-osm-map.json` +- `examples/specs/02-linked-map-histogram.json` +- `examples/specs/03-spatial-join.json` +- `examples/specs/04-geojson-input.json` +- `examples/specs/05-csv-points.json` +- `examples/specs/06-multiple-layers.json` +- `examples/specs/07-json-data.json` (added 2024-12-20) +- `examples/specs/08-geotiff-raster.json` (added 2024-12-20) +- `tests/fixtures/runtime/fixture-06-scatter-table.json` + +## Open Backlog + +These unchecked items are intentionally retained until they are implemented or +explicitly rejected. Items that were already completed after the original plan +were not reintroduced here. + +### Runtime Data Sources And Schema + +- [x] Wire `type: "json"` into `schema/autark-spec-v0.1.json` (completed 2024-12-20). +- [x] Load JSON data sources in `autk-runtime/src/data-loader.ts` through + `AutkDb.loadJson()` (completed 2024-12-20). +- [x] Wire `type: "geotiff"` into `schema/autark-spec-v0.1.json` (completed 2024-12-20). +- [x] Load GeoTIFF data sources in `autk-runtime/src/data-loader.ts` through + `AutkDb.loadGeoTiff()` (completed 2024-12-20). +- [ ] Add GeoTIFF map/raster workflow tests. +- [ ] Decide whether `TableRef` belongs in AutarkSpec and add schema/runtime + support if accepted. +- [ ] Handle inline CSV `values` in the runtime loader, or remove inline CSV + values from the schema until supported. +- [ ] Add unit tests for TypeScript spec type definitions. + +### Runtime Rendering And Interaction + +- [ ] Apply map-level `view.style` in the map renderer. +- [ ] Apply polygon `strokeWidth` through mesh-based outline geometry or an + equivalent supported rendering path. +- [ ] Implement field-driven `encoding.opacity`. +- [ ] Implement field-driven `encoding.size`. +- [ ] Implement field-driven `encoding.height`. +- [ ] Honor aggregation `as` aliases in runtime transform execution, or remove + alias examples from executable docs until supported. +- [ ] Implement link `filter` action. +- [ ] Implement link `color` action. +- [ ] Add explicit responsive resize/reflow behavior for rendered views. +- [ ] Add unit tests for individual runtime modules. +- [ ] Add visual regression tests with screenshots. + +### Python API Refinement + +- [ ] Decide whether to add `spec.show()` for explicit Jupyter display, or keep + `_repr_html_()` / `widget()` as the only display APIs. +- [ ] Add warnings or fallback strategies for large inline GeoJSON datasets. +- [ ] Consider Arrow/Parquet helpers for large tabular/spatial data. +- [ ] Consider tiled/streaming helpers for large raster/vector data. +- [ ] Add higher-level compute expression helpers over raw WGSL, or document + raw WGSL as the only supported compute authoring model. +- [ ] Add optional chart builders beyond MVP, such as bar chart and time series. +- [ ] Add legend helper classes if the runtime schema exposes stable legend + configuration. +- [ ] Add missing convenience methods where repeated user patterns emerge. +- [ ] Improve public type hints. +- [ ] Add docstrings to all public APIs. +- [ ] Review naming conventions before release. + +### Jupyter And Packaging + +- [x] Generate and include `autark/static/autark-widget.js` during Python + package builds with Hatch and `hatch-jupyter-builder`. +- [ ] Bundle DuckDB WASM assets, or document CDN requirements as the supported + behavior for the widget path. +- [ ] Test manually in Jupyter Notebook. +- [ ] Test manually in JupyterLab. +- [ ] Test manually in VS Code notebooks. +- [ ] Test manually in Google Colab if feasible. + +### Examples And Documentation + +- [ ] Add full generated API reference documentation. +- [ ] Add comparison guide: declarative Python API versus imperative TypeScript + API. +- [ ] Add migration guide from imperative TypeScript API. +- [ ] Add tutorial notebooks. +- [ ] Add troubleshooting section and FAQ to the consolidated docs. +- [ ] Add more examples after user feedback. +- [ ] Add Example 5: Urban Heat Island Analysis. +- [ ] Recreate the Niteroi use case from Python. +- [ ] Compare the Python Niteroi workflow to the TypeScript version. +- [ ] Add Python examples to the existing gallery. +- [ ] Add side-by-side TypeScript versus Python examples. +- [ ] Link gallery examples to generated specs. +- [ ] Update homepage/docs site with Python examples. +- [ ] Keep `PYTHON_API.md`, `python/README.md`, and + `python/examples/README.md` synchronized. + +### User Testing, Evaluation, And Quality + +- [ ] Recruit 3-5 Python users who are not Autark developers. +- [ ] Provide a short tutorial for user testing. +- [ ] Assign a representative building task, such as neighborhood analysis. +- [ ] Collect feedback on API clarity. +- [ ] Collect feedback on documentation quality. +- [ ] Collect feedback on error messages. +- [ ] Collect feedback on pain points and missing features. +- [ ] Iterate based on feedback. +- [ ] Test spec generation with Claude/GPT-style models. +- [ ] Measure LLM success rate. +- [ ] Measure LLM token usage. +- [ ] Measure LLM iteration count. +- [ ] Categorize LLM error types. +- [ ] Compare LLM results to imperative API results. +- [ ] Document evaluation findings. +- [ ] Measure spec serialization overhead. +- [ ] Test with large GeoJSON datasets over 10k features. +- [ ] Test with large CSV datasets over 100k rows. +- [ ] Test with large OSM areas. +- [ ] Profile runtime execution. +- [ ] Identify and optimize bottlenecks. +- [ ] Test invalid specs. +- [ ] Test missing data sources. +- [ ] Test invalid references. +- [ ] Test network failures. +- [ ] Test incompatible CRS inputs. +- [ ] Verify error messages are helpful. +- [ ] Test on macOS. +- [ ] Test on Linux. +- [ ] Test on Windows. +- [ ] Test in Chrome/Edge. +- [ ] Test in Firefox. +- [ ] Test in Safari. + +### Release And Paper + +- [ ] Decide package publishing metadata and release process. +- [ ] Prepare Python package for PyPI. +- [ ] Add Python package changelog. +- [ ] Prepare TypeScript runtime package for npm. +- [ ] Add TypeScript runtime changelog. +- [ ] Publish alpha releases. +- [ ] Announce to early users/community. +- [ ] Update the paper with the Python API section. +- [ ] Add Python examples to the paper. +- [ ] Discuss declarative versus imperative tradeoffs. +- [ ] Add Python API to evaluation if relevant. +- [ ] Update figures with Python snippets. + +### Long-Term Success Criteria + +- [ ] Feature parity with imperative TypeScript API. +- [ ] User study shows Python API is learnable. +- [ ] LLM/agent evaluation shows improved generation. +- [ ] Performance acceptable for typical datasets. +- [ ] Error messages are helpful. +- [ ] Published Python and runtime packages. +- [ ] Documentation complete. +- [ ] Gallery with 10+ examples. +- [ ] Community adoption. + +## Verification Commands + +Runtime: + +```bash +npm run validate:specs +npm run test:runtime +``` + +Python: + +```bash +cd python +python -m unittest tests.test_spec_builders +python -m unittest discover -s tests +python -m mypy +``` + +The full Python discovery suite includes notebook/widget tests that start a +local Jupyter kernel, so it may need permission to bind local ports in sandboxed +environments. diff --git a/autark-code-quality-review.md b/autark-code-quality-review.md new file mode 100644 index 00000000..f0600626 --- /dev/null +++ b/autark-code-quality-review.md @@ -0,0 +1,302 @@ +# Autark Code Quality Review Notes + +Date: 2026-06-07 + +These notes summarize a static code-review pass over the Autark monorepo, with emphasis on reusable library code rather than gallery or use-case examples. The goal is to capture engineering findings that can later be turned into an implementation plan. + +## Overall Assessment + +Autark is a substantial and technically ambitious codebase. The most sophisticated implementation areas are the DuckDB-WASM spatial database workflows, OSM processing pipeline, WebGPU map renderer, render-based compute pipeline, GPGPU shader generation, and building/roof triangulation. + +The code is much stronger than a throwaway research prototype. It has clear modular packages, typed public APIs, explicit WebGPU cleanup paths in the map renderer, and serious algorithmic work. The main weaknesses are not in the core ideas, but in production hardening: safer SQL construction, more consistent error handling, deterministic behavior, lifecycle cleanup, and algorithm-level tests. + +## Strengths + +- The package decomposition is sensible: `autk-core`, `autk-db`, `autk-compute`, `autk-map`, `autk-plot`, and the umbrella `autk` package each have a clear role. +- WebGPU resource ownership is handled seriously in the map/rendering stack. `AutkMap.destroy()`, renderer cleanup, layer cleanup, and pipeline cleanup are all explicit. +- The compute packages contain nontrivial engineering, especially dynamic WGSL shader construction, input packing, buffer readback, render-based sampling, batching, and visibility aggregation. +- The database package provides a high-level spatial workflow on top of DuckDB-WASM, including OSM loading, OSM layer extraction, GeoJSON/CSV/JSON/GeoTIFF ingestion, spatial joins, heatmaps, and raw SQL. +- The building triangulation code supports many OSM roof forms and includes fallback behavior for difficult geometry. +- The test suite has many Playwright visual/integration tests and reference screenshots for gallery workflows. + +## Main Findings + +### 1. SQL Construction Is Too Ad Hoc + +Severity: high + +Several user-facing or semi-user-facing values are interpolated directly into SQL strings. This creates two related problems: + +- Valid identifiers can break SQL when they contain characters such as hyphens. +- Malicious or accidental input could alter query structure. + +Examples: + +- `AutkDb.setWorkspace(name)` interpolates schema names directly: + - `autk-db/src/db.ts:212` + - `CREATE SCHEMA IF NOT EXISTS ${name}` + - `USE ${name}` +- CSV loading builds qualified names and SQL options directly: + - `autk-db/src/use-cases/load-csv/queries.ts:18` + - `autk-db/src/use-cases/load-csv/queries.ts:123` +- Spatial join has some identifier quoting, but not consistently for schema names, table aliases, and qualified table names: + - `autk-db/src/use-cases/spatial-join/queries.ts:51` +- Other query builders follow similar string-construction patterns for table names, staging tables, indexes, and output names. + +Recommended direction: + +- Add a central SQL utility module with: + - `quoteIdentifier(identifier: string)` + - `quoteQualifiedName(schema: string, table: string)` + - `escapeSqlString(value: string)` + - `assertSafeNumber(value: number, label: string)` + - optional validation for generated temporary identifiers and index names +- Replace local ad hoc SQL escaping with the shared helpers. +- Add unit tests for all SQL builders using edge-case identifiers such as `my-analysis`, `table with space`, quotes, reserved words, and suspicious strings. + +### 2. Drop Failures Can Desynchronize Metadata + +Severity: medium/high + +`DropTableUseCase.exec()` catches errors and returns `{ success: false }` rather than throwing: + +- `autk-db/src/use-cases/drop-table/use-case.ts:31` + +But `AutkDb.removeLayer()` ignores that result and removes the table from the in-memory workspace registry regardless: + +- `autk-db/src/db.ts:814` + +This means DuckDB and Autark metadata can disagree if a drop fails. + +Recommended direction: + +- Either make `DropTableUseCase.exec()` throw on failure, or have `removeLayer()` check the result before mutating `workspaceData.tables`. +- Add a regression test where the drop operation fails and verify metadata remains consistent. + +### 3. Error Handling Is Inconsistent Across Public APIs + +Severity: medium + +Some public methods throw, while others log and return. This makes failures harder to handle programmatically. + +Examples: + +- `AutkMap.loadCollection()` documents that it never throws and logs errors: + - `autk-map/src/map.ts:221` +- Duplicate layer ids log an error and return `null`: + - `autk-map/src/layer-manager.ts:81` +- `AutkMap.createLayer()` silently does nothing when `addLayer()` returns `null`: + - `autk-map/src/map.ts:1191` +- Renderer initialization logs errors and returns `false`: + - `autk-map/src/renderer.ts:205` + +For demos, console output is acceptable. For a toolkit, silent returns make notebook workflows, tutorials, tests, and agent-assisted development harder. + +Recommended direction: + +- Define an error-handling policy for public APIs: + - throw typed errors for invalid caller input and unrecoverable failures; + - return structured result objects for expected recoverable failures; + - reserve `console.warn/error` for optional diagnostics. +- Consider a configurable logger/progress callback for long-running OSM and PBF workflows. + +### 4. `AutkDb` Needs a Public Teardown API + +Severity: medium + +The map side has explicit lifecycle cleanup, but the database side does not appear to expose an equivalent public method. + +`AutkDb` owns an `AsyncDuckDB` and connection: + +- `autk-db/src/db.ts:62` + +It initializes them in `init()`: + +- `autk-db/src/db.ts:158` + +The DuckDB loader creates browser/Node workers with termination capability: + +- `autk-db/src/duckdb.ts:79` + +Recommended direction: + +- Add `await db.close()` or `await db.destroy()`. +- Close the active connection. +- Terminate DuckDB/worker resources where the DuckDB-WASM API supports it. +- Clear internal use-case references and workspace state. +- Add tests for repeated init/destroy cycles. + +### 5. Building Fallback Heights Are Nondeterministic + +Severity: medium + +`TriangulatorBuildings.buildMesh()` assigns random fallback heights when `allowZeroHeightBuildings` is enabled: + +- `autk-core/src/triangulator-buildings.ts:86` +- `heightInfo = [0, (3 + 4 * Math.random()) * 3.4]` + +This makes rendering, screenshots, visual tests, paper figures, and debugging less reproducible. + +Recommended direction: + +- Replace the random fallback with a deterministic default height. +- Or accept a seeded random generator / height callback when variability is desired. +- Add a test verifying deterministic output for the same input. + +### 6. Algorithm-Level Test Coverage Is Thin + +Severity: medium + +The repo has many Playwright gallery tests and reference images, but I did not see package-level unit/property tests for the core algorithms. + +Important areas that deserve targeted tests: + +- SQL query builders and identifier quoting. +- OSM relation reconstruction and multipolygon ring handling. +- Spatial join query generation, especially `NEAR`, `groupBy`, `weighted`, `collect`, and normalization. +- Building and roof triangulation, including degenerate rings and unsupported roof tags. +- GPGPU shader generation and invalid identifier rejection. +- Render-compute batching limits and aggregation edge cases. +- GeoJSON/CSV/JSON/GeoTIFF loader cleanup paths. + +Recommended direction: + +- Add a small unit-test framework such as Vitest for package-level tests. +- Keep Playwright visual tests for end-to-end behavior. +- Add property or golden-output tests for geometry and SQL builder code. + +### 7. Large Modules Concentrate Too Much Responsibility + +Severity: low/medium + +Several important modules are large and own multiple responsibilities: + +- `autk-map/src/map.ts` +- `autk-db/src/db.ts` +- `autk-compute/src/compute-render.ts` +- `autk-compute/src/compute-gpgpu.ts` +- `autk-core/src/triangulator-roofs.ts` +- `autk-db/src/use-cases/spatial-join/queries.ts` + +This is understandable given the complexity of the system, but it increases review, testing, and maintenance cost. + +Recommended direction: + +- Refactor only where there is a clear seam: + - SQL utilities and query builders; + - map layer creation; + - renderer lifecycle; + - compute shader generation; + - roof shape-specific geometry helpers. +- Avoid broad rewrites. Prioritize extracting testable pure functions. + +### 8. Type Safety Is Mostly Good, But Some Escape Hatches Are Too Broad + +Severity: low/medium + +The packages use `strict: true`, which is good. However, ESLint disables `@typescript-eslint/no-explicit-any`: + +- `eslint.config.cjs:40` + +There are real `any` escape hatches in: + +- D3 plot code. +- DuckDB worker adaptation. +- GeoJSON/turf interop. +- OSM processing rows. +- compute render result merging. + +Recommended direction: + +- Do not try to eliminate all `any` immediately. +- Start by tightening `any` at package boundaries and public APIs. +- Keep localized interop casts where third-party typings are weak, but wrap them in small helper functions. + +### 9. Console Logging Should Become Configurable Diagnostics + +Severity: low/medium + +Library code emits many `console.log`, `console.warn`, and `console.error` messages during normal operation, especially in OSM loading, PBF loading, triangulation, map layer creation, and renderer initialization. + +Examples: + +- `autk-db/src/db.ts:380` +- `autk-db/src/use-cases/load-osm-overpass/use-case.ts` +- `autk-db/src/use-cases/load-osm-pbf/use-case.ts` +- `autk-map/src/map.ts` +- `autk-core/src/triangulator-buildings.ts` + +Recommended direction: + +- Add an optional logger interface: + - `debug` + - `info` + - `warn` + - `error` + - progress events for long-running operations +- Default to quiet or warning-only behavior for library consumers. +- Let examples opt into verbose logging. + +## Suggested Implementation Plan + +This section is intentionally rough. It can be converted into issues or milestones later. + +### Phase 1: Safety and Correctness + +1. Add shared SQL identifier/literal utilities. +2. Convert the highest-risk query builders to use them: + - workspace/schema handling; + - CSV/JSON loaders; + - drop table; + - spatial join; + - get layer/table; + - raw query output table creation. +3. Fix `removeLayer()` metadata synchronization on failed drop. +4. Replace random building fallback heights with deterministic behavior. +5. Add unit tests for the above. + +### Phase 2: Lifecycle and API Hardening + +1. Add `AutkDb.close()` / `AutkDb.destroy()`. +2. Define public API error semantics. +3. Replace silent log-and-return paths with typed errors or structured results. +4. Add a configurable logger/progress interface. +5. Add tests for repeated database and map lifecycle operations. + +### Phase 3: Algorithmic Test Coverage + +1. Add package-level unit tests for: + - spatial join SQL generation; + - OSM relation reconstruction; + - triangulation edge cases; + - roof generation; + - GPGPU shader generation; + - render-compute batching and aggregation. +2. Keep existing Playwright tests as integration/visual regression tests. +3. Add small golden fixtures for known urban datasets and geometry outputs. + +### Phase 4: Maintainability Refactors + +1. Extract pure helpers from the largest modules where tests can lock behavior. +2. Reduce `any` at public boundaries. +3. Split map layer creation and render pipeline setup only where it simplifies tests. +4. Avoid broad rewrites unless test coverage is already in place. + +## Paper-Relevant Takeaways + +From a paper perspective, the implementation is strong enough to support a system contribution, but the manuscript should distinguish between: + +- the sophisticated implementation already present, and +- the engineering hardening that remains. + +The strongest implementation claims are: + +- serverless DuckDB-WASM spatial workflows; +- modular urban data loading and layer extraction; +- WebGPU map rendering; +- render-based compute over sampled viewpoints; +- dynamic GPGPU feature computation; +- OSM building and roof triangulation; +- linked map/plot workflows. + +The paper should avoid overclaiming production maturity unless the hardening items above are addressed. A strong revision can present Autark as a research toolkit with serious system architecture, real implementation depth, and a clear roadmap for broader adoption. diff --git a/autark-paper-revision-notes.md b/autark-paper-revision-notes.md new file mode 100644 index 00000000..b9552ede --- /dev/null +++ b/autark-paper-revision-notes.md @@ -0,0 +1,824 @@ +# Autark Paper: Implementation Complexity, Manuscript Gaps, and Review Response Notes + +Prepared from: + +- Submitted paper: `/Users/csilva/Dropbox/2026-submitted-papers/vis26c-sub1774-i6.pdf` +- Reviews: `/Users/csilva/Downloads/Reviews of #1774 _Autark_ A Serverless Toolkit for Prototyping Urban ..._.pdf` +- Current repository: `/Users/csilva/src/autark` + +PDF text was extracted with `/opt/homebrew/bin/pdftoipe`; the extraction is imperfect but sufficient for section-level analysis. + +## Executive Summary + +Autark has more implementation novelty than the submitted manuscript makes visible. The paper correctly emphasizes the serverless architecture, feature-centric model, usage scenarios, performance viability, and LLM/agentic development experiment. However, it often describes implementation-heavy modules as high-level capabilities: "spatial database", "GPU compute engine", "3D map", and "abstract charts." This hides several technically sophisticated algorithms and pipelines that could strengthen the system contribution if described compactly. + +The strongest technical material in the implementation is: + +1. Render-based WebGPU analytics for viewpoint/visibility/sky-exposure-style metrics. +2. Dynamic GeoJSON-to-WGSL GPGPU computation with shader generation and typed buffer packing. +3. OSM ingestion and semantic layer reconstruction, including PBF decoding, Overpass handling, relation ring reconstruction, surface polygonization, and building aggregation. +4. Geometry generation for buildings and roofs, including multiple roof types and straight-skeleton-style roof solving. +5. Spatial join query generation over DuckDB-WASM, including intersect/near predicates, aggregation, normalization, weighted proximity, and JSON property merging. +6. Unified WebGPU rendering and picking over all map layers, which supports the feature-centric interaction model. + +The reviews are not mainly saying "the system is weak." They are saying the paper does not make the system easy to evaluate. Reviewers consistently saw the architectural value but wanted clearer structure, better comparison to alternatives, clearer positioning relative to Urban Toolkit, a stronger developer/agentic evaluation story, and a more readable description of the programming model. + +The revised paper should not become a manual. Instead, it should add a few "algorithmic capsules" and interface-level descriptions that explain what the toolkit encapsulates, why those internals are hard, and why exposing them as feature-level APIs is novel/useful. + +## What Seems Most Technically Sophisticated in the Code + +### 1. Render-Based GPU Compute + +Primary files: + +- `autk-compute/src/compute-render.ts` +- `autk-compute/src/shaders/render-*.wgsl` +- `autk-compute/src/viewpoint.ts` +- `usecases/src/urbane/analysis.ts` +- `gallery/src/autk-map/compute-render-osm-sky-exposure.ts` + +Why it is sophisticated: + +- It triangulates feature layers into renderable geometry. +- It resolves viewpoint samples and builds camera matrices. +- It batches viewpoint rendering according to WebGPU limits. +- It renders tiled offscreen views. +- It runs a counting pass to aggregate visibility/class/object information. +- It maps GPU outputs back into `feature.properties.compute.render`. +- It supports class aggregation and object visibility aggregation. +- It handles GPU memory limits, uniform buffer alignment, staging buffers, and CPU-side accumulation. + +This is the implementation behind sky exposure and visibility-like urban analytics. It is more than "GPU acceleration"; it is a render-as-analysis pipeline. + +Paper coverage: + +- Section 4.2.2 says the compute engine can execute analytical or render-based operations and mentions "pixel counting for feature identification." +- Section 5.1 uses sky exposure in Urbane and shadow contribution in the Chicago example. +- Section 5.2 includes one render-based computation benchmark. + +What is missing: + +- A concise explanation of the render-compute algorithm. +- A diagram showing: features -> triangulation -> sampled cameras -> offscreen tiles -> count shader -> feature-level metrics. +- A clear statement that this encapsulates a recurring class of 3D urban VA analyses: sky exposure, landmark visibility, view impact, shadow/source contribution, object visibility. + +Suggested paper addition: + +Add an "Implementation Note: Render-based analytics" box in Section 4.2.2: + +```ts +type RenderLayer = { + id: string; + collection: FeatureCollection; + type: "buildings" | "roads" | "polygons" | ...; +}; + +type RenderPipelineParams = { + layers: RenderLayer[]; + viewpoints: { collection: FeatureCollection; sampling?: ViewSampling }; + aggregation: { type: "classes" | "objects"; includeBackground?: boolean }; + tileSize?: number; +}; +``` + +Then describe the implementation in one paragraph rather than exposing shader details. + +### 2. Dynamic GPGPU Shader Pipeline + +Primary files: + +- `autk-compute/src/compute-gpgpu.ts` +- `autk-compute/src/types-gpgpu.ts` +- `autk-compute/src/compute-pipeline.ts` +- `usecases/src/urbane/analysis.ts` +- `usecases/src/niteroi/lst-regression-shader.ts` + +Why it is sophisticated: + +- Maps GeoJSON property paths to GPU input buffers. +- Supports scalar, fixed array, fixed matrix, and auto-row matrix inputs. +- Supports uniforms, uniform arrays, and uniform matrices. +- Validates WGSL identifiers and prevents generated symbol collisions. +- Generates complete WGSL around user-provided computation bodies. +- Dispatches WebGPU compute workgroups. +- Reads GPU results back and writes them into `feature.properties.compute`. + +This is effectively a small domain-specific GPU runtime for feature collections. + +Paper coverage: + +- Section 4.2.2 describes feature-wise GPU computation and shows a simple volume example. +- Niteroi heat island example describes per-road linear regression. +- Urbane example describes weighted score computation. + +What is missing: + +- The paper underplays that the engine is not just "accept WGSL"; it handles packing/unpacking, input binding, generated shader structure, validation, and result reintegration. +- The regression example could be used to show why arrays/matrices matter, not only scalar examples. + +Suggested paper addition: + +Use the simple scalar example only as an introductory snippet, then add a sentence like: + +"Internally, Autark packs feature attributes into columnar typed arrays, generates WGSL bindings for scalar, array, and matrix attributes, validates shader identifiers, dispatches the computation over all features, and writes outputs back into `properties.compute`, preserving the original feature collection." + +### 3. OSM Processing and Semantic Layer Reconstruction + +Primary files: + +- `autk-db/src/db.ts` +- `autk-db/src/internal/process-osm/pipeline.ts` +- `autk-db/src/use-cases/load-osm-overpass/use-case.ts` +- `autk-db/src/use-cases/load-osm-pbf/use-case.ts` +- `autk-db/src/use-cases/load-osm-pbf/osm-pbf-parser.ts` +- `autk-db/src/use-cases/load-osm-layer/use-case.ts` +- `autk-db/src/use-cases/load-osm-layer/queries.ts` +- `autk-db/src/internal/process-osm-buildings/use-case.ts` +- `autk-db/src/internal/process-osm-surface/use-case.ts` + +Why it is sophisticated: + +- Supports both Overpass and PBF OSM ingestion paths. +- Converts raw nodes/ways/relations into flat records for DuckDB. +- Derives layer tags for roads, buildings, parks, water, and surface. +- Reconstructs way geometry from node references. +- Builds relation area geometries from outer/inner way members. +- Stitches closed rings and emits Polygon/MultiPolygon geometries. +- Transforms CRS and clips layers to workspace extents. +- Polygonizes surface boundaries. +- Aggregates building components and assigns coherent `building_id` values. + +This directly supports a key claim in the LLM comparison: baseline agents often operate on raw, fragmented geometry, while Autark reconstructs semantically coherent urban features. + +Paper coverage: + +- Section 4.2.1 says the database fetches and parses OSM layers. +- Section 5.3 says Autark reconstructs semantically coherent building footprints and baseline systems lack this consolidation. +- Figure 6 shows baseline geometry artifacts. + +What is missing: + +- The paper does not explain how the semantic reconstruction works at even a conceptual level. +- The current text makes the database sound like a thin DuckDB/Overpass wrapper, when it is actually a domain-specific ETL layer. + +Suggested paper addition: + +Add a short "OSM-to-feature pipeline" paragraph or figure: + +```text +OSM nodes/ways/relations + -> layer tagging + -> way geometry reconstruction + -> relation ring assembly + -> CRS transform and clipping + -> building/surface post-processing + -> feature collections consumed by map/compute/charts +``` + +This would directly address reviewer concerns about differentiation and architecture being more than packaging convenience. + +### 4. Building and Roof Mesh Generation + +Primary files: + +- `autk-core/src/triangulator-buildings.ts` +- `autk-core/src/triangulator-roofs.ts` +- `autk-core/src/triangulator-polygons.ts` +- `autk-core/src/triangulator-polylines.ts` +- `autk-core/src/triangulator-windows.ts` + +Why it is sophisticated: + +- Building geometry is not rendered as a simple extrusion only. +- The code parses OSM building height metadata (`height`, `levels`, `min_height`, etc.). +- It generates walls, floors, and roof caps. +- It supports multiple roof shapes: flat, pyramid/cone, dome, round, skillion, hipped, gabled, half-hipped, mansard, saltbox. +- The roof code includes a straight-skeleton-style process for complex roof forms and fallbacks for unstable cases. +- Polygon triangulation supports holes via `earcut`. + +Paper coverage: + +- The paper says the 3D map renders buildings and that all data are represented as triangle meshes. +- The LLM evaluation notes mesh artifacts in baseline outputs. + +What is missing: + +- The paper does not communicate that Autark provides nontrivial geometry processing for urban physical layers. +- This could help explain why the map is not simply a wrapper around deck.gl/Mapbox. + +Suggested paper addition: + +Add one compact sentence in Section 4.2.3: + +"For physical layers, the map does not rely on pre-rendered tiles: Autark triangulates GeoJSON into GPU meshes, including OSM building parts with height metadata, wall/floor caps, and roof geometry, so rendering, picking, and compute all share the same feature-level mesh representation." + +If space permits, add a table of supported geometric conversions. + +### 5. Spatial Join Query Builder + +Primary files: + +- `autk-db/src/use-cases/spatial-join/use-case.ts` +- `autk-db/src/use-cases/spatial-join/queries.ts` +- `autk-db/src/use-cases/build-heatmap/use-case.ts` + +Why it is sophisticated: + +- Dynamically builds DuckDB spatial SQL. +- Supports intersect and near predicates. +- Supports centroid-based near joins. +- Pre-filters near joins with expanded geometry envelopes. +- Builds JSON `sjoin` properties. +- Supports `count`, `avg`, `sum`, `min`, `max`, `collect`, and weighted proximity. +- Supports normalization of aggregated outputs. +- Handles both JSON `properties` columns and direct table columns. +- Heatmap generation builds a spatial grid, indexes it, runs a near aggregation, then rewrites results as raster-band properties. + +Paper coverage: + +- Section 4.2.1 says the database supports spatial joins and nearest-neighbor queries. +- Usage examples rely heavily on spatial joins. +- Performance section benchmarks spatial join scalability. + +What is missing: + +- The paper does not explain that spatial join outputs are written back into feature properties in a consistent schema. +- The `sjoin` nested property convention is important because it is the bridge from database operations to map/plot thematic encodings. + +Suggested paper addition: + +Use a short TypeScript interface instead of only mathematical notation: + +```ts +type SpatialJoinOutput = FeatureCollection & { + features: Array; + avg?: Record; + weighted?: Record; + }; + }; + }>; +}; +``` + +This would directly satisfy Reviewer 3's request for TypeScript interfaces and make the feature-centric model concrete. + +### 6. Unified Rendering, Picking, and Linked Interaction + +Primary files: + +- `autk-map/src/map.ts` +- `autk-map/src/renderer.ts` +- `autk-map/src/layer*.ts` +- `autk-map/src/pipeline*.ts` +- `autk-plot/src/plot.ts` +- `autk-plot/src/plot-base-interactive.ts` + +Why it is sophisticated: + +- All spatial layers go through one WebGPU renderer. +- Picking is feature/component-aware and can drive selections. +- Map and plot modules share event semantics over feature IDs. +- The system avoids the "multiple rendering libraries / fractured interaction stack" problem observed in baseline LLM outputs. + +Paper coverage: + +- Section 4.1 explains the feature-centric interaction model. +- Section 4.2.3 and 4.2.4 explain map/chart events. +- Figure 3 demonstrates linked map/plot interaction. +- Section 5.3 identifies fragmented visual stacks in baseline outputs. + +What is missing: + +- The implementation mechanism behind picking and cross-view identity could be clearer. +- The paper should connect "single renderer" and "single feature ID space" more explicitly to correctness, not only convenience. + +## Mismatches Between Current Code and Submitted Paper + +Some paper snippets appear out of sync with the current public API. This matters because Reviewer 3 explicitly asked for TypeScript interfaces and programming-model clarity. If the revised paper uses interface snippets, they should match the actual package. + +Observed mismatches: + +- Paper: `AutkSpatialDb`; current code: `AutkDb`. +- Paper: `loadOsmFromOverpassApi`; current code exposes `loadOsm`, selecting Overpass vs. PBF via params. +- Paper: `AutkChart`; current code: `AutkPlot`. +- Paper snippets use callback-style thematic mapping; current map API uses property paths, e.g. `updateThematic(id, { collection, property })`. +- Paper event snippets imply callbacks receive `selection` directly; current examples use event payloads such as `({ selection }) => ...`. +- Paper spatial query example uses `spatialPredicate: 'JOIN' | 'NEAREST'`; current API appears to expose intersect/near through `near` config and spatial join params. + +Recommendation: + +Before resubmission, regenerate every API snippet from current examples in `gallery/src` and `usecases/src`. The paper should use the real current API, or clearly mark any snippet as pseudocode. Given the reviews, using accurate TypeScript interfaces is preferable. + +## What the Paper Already Communicates Well + +The reviewers did notice several strengths, and the paper contains the material to support them: + +- Serverless, browser-based architecture. +- Feature-centric model unifying data, rendering, and interaction. +- Four modules: database, compute, map, charts. +- Urbane remake, shadow analysis, heat island analysis. +- Performance viability for loading, joins, and render compute. +- LLM/agentic comparison showing fewer LOC, fewer files, fewer dependencies, and lower cyclomatic complexity. + +The problem is presentation and emphasis, not absence of a system. + +## What Is Missing or Under-Described + +### Missing Technical Depth + +The paper abstracts away the hard internal work. It should add concise implementation descriptions for: + +- Render-based analytics. +- Dynamic WGSL computation. +- OSM semantic reconstruction. +- Building/roof triangulation. +- Spatial join output schema. +- Unified picking/feature ID propagation. + +These should be framed as "what the toolkit encapsulates" rather than as a manual. + +### Missing Comparison Table + +Reviewer 1 explicitly requested a systematic comparison. The paper should include a table comparing Autark with: + +- Urban Toolkit. +- deck.gl / Mapbox / MapLibre. +- Vega-Lite. +- Mosaic. +- DuckDB-WASM alone. +- QGIS/ArcGIS, perhaps as desktop GIS baselines. + +Suggested columns: + +- Browser-only / static deployment. +- OSM semantic layer extraction. +- Spatial joins in browser. +- Feature-level linked interaction. +- Unified 3D map + abstract charts. +- GPU analytical compute. +- Render-based urban analytics. +- Raster/GeoTIFF support. +- LLM-friendly narrow API / promptable documentation. +- Extensibility/custom visualization. + +The key argument should be that Autark is not merely packaging convenience. It combines these capabilities around a single feature collection/selection contract. + +### Missing Urban Toolkit Positioning + +Reviewer 1 is right that the relationship to Urban Toolkit needs explicit treatment. The paper mentions Urban Toolkit, but does not clearly answer: + +- Why build Autark instead of extending Urban Toolkit? +- What limitations of Urban Toolkit motivated Autark? +- What changed in target usage? + +Likely distinction to articulate: + +- Urban Toolkit: grammar-based, client-server, focused on expressing urban VA views/layers. +- Autark: serverless browser runtime, feature-centric API, in-browser spatial database, GPU compute, LLM-friendly modular TypeScript package, direct support for agentic/prototyping workflows. + +This should be stated in Related Work and perhaps revisited in the comparison table. + +### Missing Developer Evaluation + +Reviewer 1 wanted a user/developer study. This is the hardest concern to fully address with writing alone. + +Possible responses: + +- If feasible: run a small external developer study before resubmission. +- If not feasible: narrow the claim. Present Autark primarily as a system/toolkit contribution with an initial agentic coding evaluation, not as a proven developer productivity intervention. +- Add qualitative evidence from external users if available: setup time, time to first working prototype, errors encountered, comprehension of feature-centric model. + +A minimal study could be: + +- 6-10 participants familiar with TypeScript or visualization. +- Task: build a small urban VA app from provided data. +- Conditions: Autark docs only vs. chosen common stack, or Autark with/without feature-centric tutorial. +- Measures: completion rate, time, number of help interventions, LOC, subjective NASA-TLX-like burden, post-task questions about model clarity. + +### Agentic Coding Claim Needs Repositioning + +Reviewer 2 saw a disconnect: is the contribution a toolkit for urban VA that also helps agents, or a toolkit specifically designed for agentic coding? + +Recommendation: + +Position Autark as: + +"A serverless, feature-centric toolkit for urban VA whose constrained, typed APIs also make it suitable for agentic coding." + +Avoid making LLM-readiness the primary contribution unless the revised paper foregrounds the agentic workflow throughout. The current usage scenarios are compelling as urban VA systems, but not enough by themselves to support a central "agentic coding" claim. + +### Software Metrics Need More Care + +Reviewer 2 objected that "single file" can be a bad quality metric. The revision should not imply one file is inherently better. Reframe: + +- LOC, file count, dependencies, and cyclomatic complexity are proxies for inspection burden, not universal quality. +- Single-file outputs are not always desirable; what matters is whether the generated system remains small enough to inspect, build, and verify. +- Add generation time and token consumption if available, as Reviewer 3 requested. +- Add functional correctness / failure categories more explicitly. + +## Review Crosswalk + +### R1 / Summary: Writing and Structure + +Reviewer concern: + +- Monolithic paragraphs. +- Hard to navigate. +- Key ideas buried. + +Assessment: + +- Valid. Section 3 and Section 4.1 are dense. +- The design principles are present, but not skimmable. +- The feature-centric model is central, but the notation competes with the programming model. + +Recommended response: + +- Rewrite Section 3 as explicit principles: + - Urban-specific. + - Web-first/serverless. + - Component-oriented. + - Feature-centric. + - LLM-ready. +- Give each principle 2-3 sentences and one design consequence. +- Add a summary table: principle -> system decision -> implementation mechanism. + +### R1: Related Work Lacks Synthesis + +Reviewer concern: + +- Related work reads like a catalog. +- Need gaps and positioning. + +Assessment: + +- Mostly valid. The paper names many works, but the "why Autark is different" argument should be sharper. + +Recommended response: + +- End each related-work subsection with a synthesis paragraph. +- Add a comparison table. +- Explicitly contrast Autark with Urban Toolkit, Mosaic, deck.gl/Mapbox, GIS tools, and general LLM dashboard systems. + +### R1: Urban Toolkit Relationship + +Reviewer concern: + +- Urban Toolkit appears to be the direct predecessor and is under-discussed. + +Assessment: + +- Valid and important. + +Recommended response: + +- Add a paragraph explaining why Urban Toolkit was not simply extended. +- Frame Autark as a shift from grammar/client-server architecture to feature-centric/serverless/API-oriented toolkit. + +### R1: No User Study + +Reviewer concern: + +- All usage scenarios are authored by toolkit developers. + +Assessment: + +- Valid, unless the target venue accepts the system contribution plus performance and agent study as sufficient. + +Recommended response: + +- Add small developer study if feasible. +- Otherwise, reduce claims about learnability/productivity and present developer evaluation as future work. +- Keep the agentic evaluation as "initial evidence." + +### R2: Agentic Coding Disconnect + +Reviewer concern: + +- Unclear whether Autark is a toolkit that can help agents or a toolkit designed specifically for agents. + +Assessment: + +- Valid. The abstract and introduction foreground LLMs strongly, but most scenarios are not organized as agentic workflows. + +Recommended response: + +- Choose one primary narrative. +- Recommended: urban VA toolkit first; agentic-readiness as a secondary consequence and evaluation dimension. +- Add one figure showing the actual agentic workflow: prompt + Autark docs/interfaces -> generated app -> verification. + +### R2: Software Quality Metrics + +Reviewer concern: + +- File count/single-file output may be a questionable quality signal. + +Assessment: + +- Valid. Single-file code is not inherently maintainable. + +Recommended response: + +- Reframe metrics as "inspection burden" proxies. +- Add token count, generation time, build success, functional correctness categories. +- Avoid saying single-file is good by itself. + +### R2: Architectural Limitations + +Reviewer concern: + +- May bias toward 3D representations and predefined charts/interactions. + +Assessment: + +- Partly valid. The paper mentions custom D3 extensibility but could emphasize that packages are modular and charts are optional. + +Recommended response: + +- Add a limitation and clarify extension points. +- Add examples of using `autk-db` or `autk-compute` without `autk-map`. + +### R3: TypeScript Interfaces Instead of Math + +Reviewer concern: + +- The formal notation in Section 4.1 obscures the programming model. + +Assessment: + +- Strongly valid for a toolkit paper. + +Recommended response: + +- Keep the feature-centric concept, but replace most notation with TypeScript interfaces: + +```ts +type FeatureId = string | number; + +type AutkFeature = Feature>; + compute?: Record; +}>; + +type FeatureSelection = FeatureId[]; + +interface FeatureConsumer { + updateCollection(collection: FeatureCollection): void; + setSelection(selection: FeatureSelection): void; +} +``` + +Use one small equation only if necessary. + +### R3: Generation Speed and Token Consumption + +Reviewer concern: + +- Add speed and token consumption to LLM metrics. + +Assessment: + +- Good suggestion and likely easy if experiment logs exist. + +Recommended response: + +- Report median/mean generation time. +- Report input/output token counts or approximate transcript sizes. +- Report number of agent iterations/tool calls/build failures if available. + +## Recommended Revised Paper Structure + +One possible structure: + +1. Introduction + - Problem: urban VA prototypes require recurring complex infrastructure. + - Opportunity: browser runtimes + WebGPU/WASM + structured APIs. + - Contribution: serverless feature-centric toolkit; agentic-readiness as secondary. + +2. Related Work and Positioning + - Urban VA systems and recurring components. + - Urban VA toolkits, including Urban Toolkit. + - Browser/serverless data and visualization tools. + - LLM/agentic VA development. + - Comparison table. + +3. Design Goals + - Urban-specific. + - Web-first/serverless. + - Component-oriented. + - Feature-centric. + - Agent-friendly typed API. + +4. Programming Model + - TypeScript interfaces for feature collection, selection, module contracts. + - Dataflow figure. + - Short explanation of why this removes translation layers. + +5. System Implementation + - Database and OSM-to-feature pipeline. + - GPU compute: analytical and render-based. + - Map: triangulation, rendering, picking. + - Charts: feature-bound plots and extensibility. + - Add "implementation capsules" for the most complex internals. + +6. Usage Scenarios + - Urbane remake. + - Shadow analysis. + - Heat island analysis. + - For each: state which Autark internals are exercised. + +7. Evaluation + - Performance. + - Agentic coding experiment. + - Optional developer study. + +8. Limitations and Future Work + +## Concrete Additions That Would Most Improve Novelty/Clarity + +Highest priority: + +1. Add a feature comparison table against closest alternatives. +2. Replace mathematical formalization with TypeScript interfaces and a short dataflow diagram. +3. Add an OSM semantic reconstruction pipeline figure. +4. Add a render-based GPU analytics capsule. +5. Clarify the relationship to Urban Toolkit. +6. Reframe agentic coding as secondary unless the workflow is foregrounded throughout. +7. Add generation time/token metrics and avoid treating single-file output as inherently good. +8. Add a small developer study or explicitly narrow claims about learnability. + +## Suggested "Implementation Complexity" Table for the Paper + +| Capability | What Autark Exposes | Hidden Implementation Complexity | Why It Matters | +| --- | --- | --- | --- | +| OSM layer loading | `db.loadOsm(...)` | Overpass/PBF parsing, layer tagging, relation reconstruction, CRS transform, clipping, building aggregation | Converts raw OSM into coherent urban features instead of fragmented geometries | +| Spatial joins | `db.spatialQuery(...)` | Dynamic DuckDB spatial SQL, intersect/near predicates, aggregation, normalization, JSON property merging | Makes thematic-to-physical data integration one operation | +| Analytical GPU compute | `compute.gpgpuPipeline(...)` | Property-path extraction, typed buffer packing, WGSL generation, validation, dispatch, readback | Lets users write feature-wise analytics without WebGPU boilerplate | +| Render-based compute | `compute.renderPipeline(...)` | Triangulation, viewpoint sampling, offscreen tiled rendering, count pass, feature-level aggregation | Supports visibility, sky exposure, and view-impact analyses | +| Map rendering/picking | `map.loadCollection(...)`, `MapEvent.PICKING` | Mesh generation, WebGPU pipelines, picking buffers, feature/component alignment | Keeps rendering and interaction in the same feature ID space | +| Linked charts | `new AutkPlot(...)` | Plot-specific transforms, event-to-selection mapping, feature-bound updates | Enables coordinated views without custom glue code | + +## Bottom Line + +The revised paper should make the reader understand that Autark is not just a convenient bundle of DuckDB, WebGPU, D3, and a map. Its novelty is the combination of: + +- a feature-centric contract, +- serverless browser execution, +- domain-specific urban data processing, +- GPU analytical/render compute, +- unified rendering and interaction, +- and a constrained API surface that is easier for humans and agents to compose. + +The current manuscript contains pieces of this story, but the implementation complexity is under-described and the contribution hierarchy is blurred by the LLM framing. A stronger version should foreground Autark as a technical urban VA system/toolkit contribution, then use agentic coding as an evaluation lens showing why the abstraction boundary matters. + +## Additional Discussion: Repositioning and Developer Evaluation + +### Reposition Away From Agentic Coding as the Core Claim + +A likely major source of reviewer confusion is that the submitted paper tried too hard to frame Autark around "agentic coding." Agentic coding is relevant, but it is not the core contribution. The core work is a serverless, feature-centric urban VA toolkit that encapsulates difficult recurring infrastructure for urban visual analytics prototypes. + +Recommended positioning: + +- Primary contribution: Autark as a serverless, feature-centric toolkit for urban VA prototyping. +- Technical novelty: browser-side spatial data management, OSM semantic reconstruction, WebGPU analytical/render compute, unified map rendering/picking, and linked abstract charts through one feature collection/selection model. +- Evaluation: usage scenarios, performance, and developer evaluation. +- Agentic coding: a secondary consequence of the same design. Autark's typed, narrow, domain-specific APIs can help LLMs produce more cohesive code, but this should not be the main story unless the entire paper, video, website, and evaluation are reorganized around agentic workflows. + +Suggested revised framing sentence: + +> Autark is a serverless, feature-centric toolkit for rapid urban visual analytics prototyping. Its constrained TypeScript APIs also make it suitable for agent-assisted development, but this is a consequence of the toolkit design rather than the central contribution. + +This framing would likely resolve Reviewer 2's concern about whether the paper contributes a toolkit for urban VA systems or a toolkit specifically designed for agentic coding. The revised manuscript should make the answer clear: it is first a toolkit for urban VA systems; agentic coding is an important emerging use case and evaluation lens. + +### Addressing the Long-Paragraph Problem + +The reviewers' criticism of long paragraphs is fair and should be addressed structurally, not just by copyediting. A systems paper needs visible signposts: + +- What problem is being solved? +- What design decision addresses it? +- What implementation mechanism realizes it? +- What evidence supports it? + +Recommended structure changes: + +- Break Section 3 into explicit design goals/principles, each with a short title and 2-3 sentence explanation. +- Replace dense prose in Section 4.1 with a compact programming-model description and TypeScript interfaces. +- Add summary tables for design principles, module responsibilities, and comparison to alternatives. +- Use short "implementation capsule" boxes to expose technical depth without turning the paper into API documentation. +- Make each usage scenario explicitly list which Autark capabilities it exercises. + +### Proposed Developer Evaluation + +To strengthen the evaluation and directly address Reviewer 1's concern that all usage scenarios were author-built, run a small developer evaluation. This should be framed as a developer/prototyping study, not a full domain-user study of urban planning outcomes. + +Goal: + +Evaluate whether TypeScript-proficient users outside the author team can understand Autark's programming model and build a small urban VA prototype after a short tutorial. + +Participants: + +- 6-10 participants. +- Comfortable with TypeScript or JavaScript. +- Preferably some mix of visualization/geospatial experience, but not required. +- Exclude Autark developers and close collaborators who already know the system. + +Session format: + +- One morning session, approximately 3-4 hours. +- In person or synchronous remote. +- Use a controlled starter repo and fixed datasets. + +Suggested schedule: + +1. 20 minutes: tutorial on Autark concepts. + Cover feature collections, selections, `AutkDb`, `AutkMap`, `AutkPlot`, and optionally `AutkCompute`. + +2. 20 minutes: guided warm-up. + Participants load a small GeoJSON layer and render a thematic map. + +3. 90-120 minutes: independent build task. + Participants build a small urban VA prototype from provided data. + +4. 20-30 minutes: survey and short interview. + Collect subjective feedback, pain points, and suggestions. + +Candidate task: + +> Build an urban VA prototype that loads neighborhood polygons and a CSV of incidents, joins incidents to neighborhoods, renders a thematic map, and adds one linked chart where brushing/clicking highlights features on the map. + +Optional stretch goals: + +- Load OSM buildings or roads. +- Use a nearest-neighbor join to associate points with buildings or roads. +- Add a GPU-derived score or weighted metric. +- Add color-map controls. +- Add a second linked chart. + +Metrics to collect: + +- Completion rate for each milestone. +- Time to first successful build. +- Time to first rendered map. +- Time to successful spatial join. +- Time to linked map/chart interaction. +- Number and type of errors. +- Number of help requests. +- Which examples/docs participants used. +- Whether the final prototype meets functional requirements. +- Subjective ratings of API clarity, feature-centric model clarity, frustration, confidence, and likelihood of reuse. + +Qualitative data: + +- Observer notes. +- Screen recordings or IDE activity logs, with consent. +- Build logs and terminal errors. +- Short post-task survey. +- 5-10 minute semi-structured interview. + +Possible survey prompts: + +- "I understood how data flows between Autark modules." +- "The feature collection / selection model was clear." +- "The examples were sufficient for completing the task." +- "The API names and TypeScript types helped me understand what to do." +- "I could imagine using Autark for a future urban VA prototype." +- "What was the most confusing part of the workflow?" +- "What documentation or example was missing?" + +Milestone rubric: + +| Milestone | Success Criteria | +| --- | --- | +| Build setup | App installs/builds/runs from starter repo | +| Data load | Participant loads provided GeoJSON/CSV or OSM layer | +| Spatial join | Output feature properties contain joined/aggregated values | +| Map rendering | Thematic layer appears with correct coloring | +| Plot rendering | At least one chart renders from the same feature collection | +| Linked interaction | Selection in map or plot highlights corresponding features in the other view | +| Explanation | Participant can describe the dataflow and where joined/computed values live | + +How to report it in the paper: + +> To evaluate learnability and practical prototyping support, we conducted a half-day developer study with TypeScript-proficient participants. After a short tutorial, participants built an urban VA prototype using Autark from provided data. We measured task completion, time-to-milestone, support requests, build/runtime errors, and perceived API clarity. + +This would directly support the claim that Autark lowers prototyping overhead for developers outside the author team. It also reduces the pressure on the agentic coding experiment to carry the entire evaluation burden. + +### Relationship to Agentic Coding Evaluation + +If the developer evaluation is added, the agentic coding experiment should be reframed as secondary: + +- Developer study: evidence that humans can learn/use Autark. +- Usage scenarios: evidence that Autark is expressive enough for nontrivial urban VA systems. +- Performance: evidence that browser-side execution is viable. +- Agentic coding experiment: evidence that the same abstraction boundaries also help LLM-based code generation. + +This creates a cleaner evaluation story: + +1. Can Autark build real systems? Usage scenarios. +2. Can it run interactively in the browser? Performance. +3. Can new developers use it? Developer evaluation. +4. Do its abstractions help agents? Agentic coding experiment. diff --git a/autk-db/src/duckdb.ts b/autk-db/src/duckdb.ts index c012b297..16368f8a 100644 --- a/autk-db/src/duckdb.ts +++ b/autk-db/src/duckdb.ts @@ -1,24 +1,37 @@ import * as duckdb from '@duckdb/duckdb-wasm'; -import mvpModuleUrl from '@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url'; -import mvpWorkerUrl from '@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?url'; -import ehModuleUrl from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url'; -import ehWorkerUrl from '@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?url'; /** - * Browser-specific DuckDB-Wasm bundle definitions used for runtime selection. + * Resolves the browser DuckDB-Wasm bundles for the current module location. * - * Maps the supported WebAssembly variants to the worker and module assets emitted with the package. + * When the module is served over HTTP(S) or from disk, the worker and wasm + * assets emitted next to the bundle are used. When the module was loaded from + * a blob:/data: URL (e.g. bundled into an anywidget ESM), relative asset URLs + * cannot be resolved, so the official jsDelivr CDN bundles are used instead. */ -const BROWSER_BUNDLES: duckdb.DuckDBBundles = { - mvp: { - mainModule: mvpModuleUrl, - mainWorker: mvpWorkerUrl, - }, - eh: { - mainModule: ehModuleUrl, - mainWorker: ehWorkerUrl, - }, -}; +function resolveBrowserBundles(): duckdb.DuckDBBundles { + try { + const base = import.meta.url; + if (base.startsWith('http:') || base.startsWith('https:') || base.startsWith('file:')) { + return { + mvp: { + mainModule: new URL(/* @vite-ignore */ './duckdb-mvp.wasm', base).href, + mainWorker: new URL(/* @vite-ignore */ './duckdb-browser-mvp.worker.js', base).href, + }, + eh: { + mainModule: new URL(/* @vite-ignore */ './duckdb-eh.wasm', base).href, + mainWorker: new URL(/* @vite-ignore */ './duckdb-browser-eh.worker.js', base).href, + }, + }; + } + } catch { + // Fall through to CDN bundles. + } + return duckdb.getJsDelivrBundles(); +} + +const NODE_PATH_MODULE = 'node:path'; +const NODE_WORKER_THREADS_MODULE = 'node:worker_threads'; +const NODE_MODULE_MODULE = 'node:module'; /** * Loads and instantiates a DuckDB-Wasm database for the current runtime. @@ -35,9 +48,9 @@ const BROWSER_BUNDLES: duckdb.DuckDBBundles = { */ export async function loadDb() { if (typeof process !== 'undefined' && process.versions?.node) { - const path = await import(/* @vite-ignore */ 'node:path'); - const { Worker: NodeWorker } = await import(/* @vite-ignore */ 'node:worker_threads'); - const { createRequire } = await import(/* @vite-ignore */ 'node:module'); + const path = await import(/* @vite-ignore */ NODE_PATH_MODULE); + const { Worker: NodeWorker } = await import(/* @vite-ignore */ NODE_WORKER_THREADS_MODULE); + const { createRequire } = await import(/* @vite-ignore */ NODE_MODULE_MODULE); const require = createRequire(import.meta.url); const dist = path.dirname(require.resolve('@duckdb/duckdb-wasm')); const workerPath = path.join(dist, 'duckdb-node-eh.worker.cjs'); @@ -86,9 +99,24 @@ export async function loadDb() { return db; } - const bundle = await duckdb.selectBundle(BROWSER_BUNDLES); - const worker = new Worker(bundle.mainWorker!); - const db = new duckdb.AsyncDuckDB(new duckdb.VoidLogger(), worker); - await db.instantiate(bundle.mainModule); - return db; + const bundle = await duckdb.selectBundle(resolveBrowserBundles()); + // Browsers block `new Worker(url)` when `url` is cross-origin, even with + // CORS headers (e.g. runtime served from :8000 inside a Jupyter page on + // :8888). Workaround recommended by the DuckDB-WASM docs: create the + // worker from a same-origin blob: URL that importScripts() the real + // worker script. Classic-worker importScripts() may load cross-origin + // scripts, and the wasm module fetch still uses CORS as before. + const workerBlobUrl = URL.createObjectURL( + new Blob([`importScripts(${JSON.stringify(bundle.mainWorker!)});`], { + type: 'text/javascript', + }), + ); + try { + const worker = new Worker(workerBlobUrl); + const db = new duckdb.AsyncDuckDB(new duckdb.VoidLogger(), worker); + await db.instantiate(bundle.mainModule); + return db; + } finally { + URL.revokeObjectURL(workerBlobUrl); + } } diff --git a/autk-db/src/use-cases/load-osm-layer/queries.ts b/autk-db/src/use-cases/load-osm-layer/queries.ts index 9e7705f3..2c595d72 100644 --- a/autk-db/src/use-cases/load-osm-layer/queries.ts +++ b/autk-db/src/use-cases/load-osm-layer/queries.ts @@ -33,7 +33,10 @@ export const LOAD_LAYER_QUERY = ({ tableName, layer, sourceCrs, targetCrs, outpu let actualTableName = qualifiedInputTableName; if (layer === 'surface') { - const baseTableName = tableName.replace(new RegExp(`^${workspace}\.`), ''); + const workspacePrefix = `${workspace}.`; + const baseTableName = tableName.startsWith(workspacePrefix) + ? tableName.slice(workspacePrefix.length) + : tableName; actualTableName = `${workspace}.${baseTableName}_boundaries`; } diff --git a/autk-db/vite.config.ts b/autk-db/vite.config.ts index 3ef2f008..ad3537fb 100644 --- a/autk-db/vite.config.ts +++ b/autk-db/vite.config.ts @@ -1,16 +1,38 @@ - +import { readFileSync } from 'fs'; import { resolve } from 'path'; import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; +function duckdbAssets() { + const assets = [ + 'duckdb-mvp.wasm', + 'duckdb-browser-mvp.worker.js', + 'duckdb-eh.wasm', + 'duckdb-browser-eh.worker.js', + ]; + + return { + name: 'duckdb-assets', + generateBundle() { + for (const fileName of assets) { + this.emitFile({ + type: 'asset', + fileName, + source: readFileSync(resolve(__dirname, '../node_modules/@duckdb/duckdb-wasm/dist', fileName)), + }); + } + }, + }; +} + export default defineConfig({ resolve: { alias: { '@urban-toolkit/autk-core': resolve(__dirname, '../autk-core/src/index.ts'), }, }, - plugins: [dts()], + plugins: [duckdbAssets(), dts()], build: { lib: { entry: resolve(__dirname, 'src/index.ts'), diff --git a/autk-map/src/api.ts b/autk-map/src/api.ts index 7ab04570..8579d129 100644 --- a/autk-map/src/api.ts +++ b/autk-map/src/api.ts @@ -60,6 +60,13 @@ export interface LoadCollectionParams { * Optional flag to treat building zero-height extrusions. */ allowZeroHeightBuildings?: boolean; + /** + * Optional full visual width for triangulated polyline/road layers. + * + * The renderer stores polylines as buffered meshes, so this value is applied + * while loading the collection rather than as a later render-state update. + */ + lineWidth?: number; /** * Property accessor used to derive layer values. * diff --git a/autk-map/src/layer-sprite.ts b/autk-map/src/layer-sprite.ts index ea1797d9..7af3218b 100644 --- a/autk-map/src/layer-sprite.ts +++ b/autk-map/src/layer-sprite.ts @@ -158,6 +158,7 @@ export class SpriteLayer extends Layer { toggleHighlightedIds(ids: number[]): void { for (const id of ids) { + if (id < 0 || id >= this._components.length) { continue; } if (this._highlightedIds.has(id)) { this._highlightedIds.delete(id); } else { @@ -165,7 +166,7 @@ export class SpriteLayer extends Layer { } const start = id > 0 ? this._components[id - 1].nPoints : 0; - const end = this._components[id]?.nPoints ?? start; + const end = this._components[id].nPoints; for (let index = start; index < end; index++) { this._highlightedVertices[index] = 1 - this._highlightedVertices[index]; } @@ -184,9 +185,10 @@ export class SpriteLayer extends Layer { override setHighlightedIds(ids: number[]): void { this.clearHighlightedIds(); for (const id of ids) { + if (id < 0 || id >= this._components.length) { continue; } this._highlightedIds.add(id); const start = id > 0 ? this._components[id - 1].nPoints : 0; - const end = this._components[id]?.nPoints ?? start; + const end = this._components[id].nPoints; this._highlightedVertices.fill(1, start, end); } this.makeLayerDataDirty(); @@ -195,9 +197,10 @@ export class SpriteLayer extends Layer { override setSkippedIds(ids: number[]): void { this.clearSkippedIds(); for (const id of ids) { + if (id < 0 || id >= this._components.length) { continue; } this._skippedIds.add(id); const start = id > 0 ? this._components[id - 1].nPoints : 0; - const end = this._components[id]?.nPoints ?? start; + const end = this._components[id].nPoints; this._skippedVertices.fill(1, start, end); } this.makeLayerDataDirty(); diff --git a/autk-map/src/layer-triangles2D.ts b/autk-map/src/layer-triangles2D.ts index 78d5279e..2b655e3c 100644 --- a/autk-map/src/layer-triangles2D.ts +++ b/autk-map/src/layer-triangles2D.ts @@ -210,6 +210,7 @@ export class Triangles2DLayer extends VectorLayer { // fill/picking pipelines, so preserve the data-dirty state needed to // keep the border buffers in sync for skip/geometry changes. const dataDirty = this._dataIsDirty; + const renderInfoDirty = this._renderInfoIsDirty; super.renderPass(camera, passEncoder); @@ -219,6 +220,10 @@ export class Triangles2DLayer extends VectorLayer { this._pipelineBorder.updateVertexBuffers(this); } + if (renderInfoDirty) { + this._pipelineBorder.updateColorUniforms(this); + } + this._pipelineBorder.updateZIndex(this._layerInfo.zIndex); this._pipelineBorder.renderPass(camera, passEncoder); } diff --git a/autk-map/src/map.ts b/autk-map/src/map.ts index 1aa6c600..fa06fd4e 100644 --- a/autk-map/src/map.ts +++ b/autk-map/src/map.ts @@ -220,7 +220,7 @@ export class AutkMap { * @param params.allowZeroHeightBuildings Optional flag to treat building zero-height extrusions. * @throws Never throws. Errors are logged to the console. */ - loadCollection(id: string, { collection, type = null, property, allowZeroHeightBuildings = false }: LoadCollectionParams): void { + loadCollection(id: string, { collection, type = null, property, allowZeroHeightBuildings = false, lineWidth }: LoadCollectionParams): void { if (!this.layerManager.hasOrigin) { this.layerManager.initializeOrigin(collection); } @@ -238,7 +238,7 @@ export class AutkMap { case 'roads': case 'polylines': { - this.createPolylinesLayer(id, collection as FeatureCollection, sType, typeof property === 'string' ? property : undefined); + this.createPolylinesLayer(id, collection as FeatureCollection, sType, typeof property === 'string' ? property : undefined, lineWidth); break; } case 'points': @@ -989,7 +989,7 @@ export class AutkMap { * @param property Optional value extractor used to initialize thematic data. * @returns Nothing. The layer is created when triangulation succeeds. */ - private createPolylinesLayer(layerName: string, geojson: FeatureCollection, typeLayer: LayerType, property?: string) { + private createPolylinesLayer(layerName: string, geojson: FeatureCollection, typeLayer: LayerType, property?: string, lineWidth?: number) { const layerInfo: LayerInfo = { id: `${layerName}`, zIndex: this._layerManager.computeZindex(typeLayer), @@ -1004,8 +1004,12 @@ export class AutkMap { isSkip: false, }; - TriangulatorPolylines.offset = typeLayer === 'roads' ? TriangulatorPolylines.DEFAULT_ROAD_HALF_WIDTH : 8.5; - const layerMesh = typeLayer === 'roads' + const fixedHalfWidth = typeof lineWidth === 'number' && Number.isFinite(lineWidth) && lineWidth > 0 + ? lineWidth / 2 + : undefined; + + TriangulatorPolylines.offset = fixedHalfWidth ?? (typeLayer === 'roads' ? TriangulatorPolylines.DEFAULT_ROAD_HALF_WIDTH : 8.5); + const layerMesh = typeLayer === 'roads' && fixedHalfWidth === undefined ? TriangulatorPolylines.buildMesh( geojson, this.layerManager.origin, diff --git a/autk-map/src/pipeline-triangle-border.ts b/autk-map/src/pipeline-triangle-border.ts index b938b891..0a70d41d 100644 --- a/autk-map/src/pipeline-triangle-border.ts +++ b/autk-map/src/pipeline-triangle-border.ts @@ -17,6 +17,7 @@ import { Pipeline } from './pipeline'; import { Renderer } from './renderer'; import { Camera } from '@urban-toolkit/autk-core'; +import type { ColorRGB } from '@urban-toolkit/autk-core'; import { Triangles2DLayer } from './layer-triangles2D'; @@ -68,6 +69,16 @@ export class PipelineTriangleBorder extends Pipeline { super(renderer); } + /** + * Border passes use the stroke color when one is configured. + * + * @param layer Layer instance whose render configuration is uploaded. + * @returns RGB color consumed by the border shader uniform. + */ + protected override resolveLayerColor(layer: Triangles2DLayer): ColorRGB { + return layer.layerRenderInfo.strokeColor ?? super.resolveLayerColor(layer); + } + /** * Releases GPU resources owned by this pipeline. * diff --git a/autk-map/src/pipeline.ts b/autk-map/src/pipeline.ts index 41b24f07..cae43c40 100644 --- a/autk-map/src/pipeline.ts +++ b/autk-map/src/pipeline.ts @@ -17,6 +17,7 @@ import { ColorMap, DEFAULT_COLORMAP_RESOLUTION } from '@urban-toolkit/autk-core'; +import type { ColorRGB } from '@urban-toolkit/autk-core'; import { Renderer } from './renderer'; import { MapStyle } from './map-style'; @@ -354,6 +355,18 @@ export abstract class Pipeline { }); } + /** + * Resolves the fixed base color used by this pipeline. + * + * Subclasses can override this for alternate passes such as polygon borders. + * + * @param layer Layer instance whose render configuration is uploaded. + * @returns RGB color consumed by the shader uniform. + */ + protected resolveLayerColor(layer: Layer): ColorRGB { + return layer.layerRenderInfo.color ?? MapStyle.getColor(layer.layerInfo.typeLayer); + } + /** * Writes the current layer styling state into the shared render uniforms. * @@ -371,7 +384,7 @@ export abstract class Pipeline { && computedDomain.every(v => typeof v === 'string'); const colors = { - color: MapStyle.getColor(layer.layerInfo.typeLayer), + color: this.resolveLayerColor(layer), highlightColor: MapStyle.getHighlightColor(), invalidValueColor: MapStyle.getInvalidValueColor(), colorMap: ColorMap.getColorMap( diff --git a/autk-map/src/shaders/triangle-02.frag.wgsl b/autk-map/src/shaders/triangle-02.frag.wgsl index cd83a10f..ab7c2f80 100644 --- a/autk-map/src/shaders/triangle-02.frag.wgsl +++ b/autk-map/src/shaders/triangle-02.frag.wgsl @@ -1,8 +1,12 @@ +@group(0) @binding(0) var color : vec4f; +@group(0) @binding(6) var opacity : f32; + @fragment fn main(@location(0) inSkipped: f32) -> @location(0) vec4f { if (inSkipped > 0.0) { discard; } - return vec4f(0.0, 0.0, 0.0, 1.0); // solid black -} \ No newline at end of file + let outColor = vec4f(color.r / 255.0, color.g / 255.0, color.b / 255.0, color.a); + return vec4f(outColor.rgb * opacity, opacity); +} diff --git a/autk-map/src/types-layers.ts b/autk-map/src/types-layers.ts index 5a4c3f30..7afeb849 100644 --- a/autk-map/src/types-layers.ts +++ b/autk-map/src/types-layers.ts @@ -11,6 +11,7 @@ import type { ColorMapConfig, + ColorRGB, ResolvedDomain, LayerBorder, LayerBorderComponent, @@ -41,6 +42,10 @@ export interface LayerColormap { /** Mutable render state associated with a layer. */ export interface LayerRenderInfo { + /** Optional fixed layer color used when thematic color mapping is disabled. */ + color?: ColorRGB; + /** Optional fixed border/outline color used by layers with a border pass. */ + strokeColor?: ColorRGB; /** Layer opacity in the range `[0, 1]`. */ opacity: number; /** Enables thematic color interpolation when `true`. */ diff --git a/autk-runtime/README.md b/autk-runtime/README.md new file mode 100644 index 00000000..a56c77d1 --- /dev/null +++ b/autk-runtime/README.md @@ -0,0 +1,247 @@ +# @urban-toolkit/autk-runtime + +Runtime for executing declarative **AutarkSpec** specifications. + +## Overview + +The Autark runtime takes a JSON specification and executes it to create fully interactive urban visual analytics applications. This enables: + +- **Declarative workflows**: Define urban VA apps in JSON instead of imperative code +- **Python API**: Generate specs from Python (via `autark` package) +- **Agentic coding**: LLMs can generate specs more reliably than imperative code +- **Reproducibility**: Specs are portable and version-controlled + +## Installation + +```bash +npm install @urban-toolkit/autk-runtime +``` + +## Usage + +### From TypeScript/JavaScript + +```typescript +import { AutarkRuntime } from '@urban-toolkit/autk-runtime'; + +const spec = { + $schema: 'https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json', + version: '0.1', + data: [ + { + type: 'osm', + name: 'manhattan', + area: 'Manhattan, New York', + layers: ['buildings'] + } + ], + views: [ + { + type: 'map', + layers: [ + { + source: 'manhattan_buildings', + encoding: { + color: { field: 'properties.height' } + } + } + ] + } + ] +}; + +const runtime = await AutarkRuntime.fromSpec(spec, { + container: document.getElementById('app') +}); +``` + +### From Python + +```python +import autark as ak + +view = ( + ak.Spec() + .data(ak.OSM("manhattan", area="Manhattan, New York", layers=["buildings"])) + .views([ + ak.Map().layer( + ak.Layer("manhattan_buildings").encode(color="properties.height") + ) + ]) +) + +view.show() # Generates spec and embeds runtime in Jupyter +``` + +## Architecture + +The runtime follows a structured execution pipeline: + +1. **Validate**: Check spec against JSON Schema +2. **Verify references**: Ensure data sources, selections, and targets exist +3. **Initialize database**: Create AutkDb instance with workspace +4. **Load data**: Execute OSM/GeoJSON/CSV loaders +5. **Execute transforms**: Run spatial joins and other operations +6. **Create layout**: Generate DOM structure +7. **Render views**: Create maps and plots +8. **Connect links**: Wire up cross-view selections + +## API + +### AutarkRuntime + +Main runtime class. + +#### `AutarkRuntime.fromSpec(spec, options)` + +Execute a specification. + +**Parameters:** +- `spec: AutarkSpec` - The specification to execute +- `options?: RuntimeOptions` + - `container?: HTMLElement | string` - DOM container + - `validate?: boolean | 'strict'` - Validation mode (default: true) + - `onError?: 'throw' | 'warn' | 'silent'` - Error handling (default: 'throw') + - `onProgress?: (message, percent?) => void` - Progress callback + +**Returns:** `Promise` + +#### `runtime.getDb()` + +Get the AutkDb instance. + +#### `runtime.getMap(name?)` + +Get a map view by name (or first map if no name). + +#### `runtime.getPlot(name?)` + +Get a plot view by name (or first plot if no name). + +#### `runtime.destroy()` + +Clean up all resources. + +### SpecValidator + +JSON Schema validator for specs. + +#### `validator.validate(spec)` + +Validate a spec. Throws `SpecValidationError` if invalid. + +#### `validator.isValid(spec)` + +Check if spec is valid (returns boolean). + +## Locked Conventions (v0.1 MVP) + +### OSM Table Naming + +OSM data sources generate tables as `${name}_${layer}`: + +```json +{ + "data": [{"type": "osm", "name": "chicago", "layers": ["buildings", "roads"]}] +} +``` + +Creates tables: `chicago_buildings`, `chicago_roads` + +### Transform Mutation Semantics + +Spatial joins **mutate the root table in place** (MVP): + +```json +{ + "transforms": [ + {"type": "spatialJoin", "root": "neighborhoods", "join": "trees"} + ], + "views": [ + {"layers": [{"source": "neighborhoods"}]} // Use root table + ] +} +``` + +### Field Paths + +- **Map encodings**: Use full paths (`properties.height`) +- **Plot fields**: Use property names (`height`) + +```json +{ + "views": [ + {"type": "map", "layers": [{"encoding": {"color": {"field": "properties.height"}}}]}, + {"type": "histogram", "x": {"field": "height"}} + ] +} +``` + +### Link Targets + +Links target **layer IDs**, not data sources: + +```json +{ + "views": [ + {"type": "map", "layers": [{"source": "buildings", "id": "buildings_layer"}]} + ], + "links": [ + {"selection": "brush", "target": "buildings_layer", "action": "highlight"} + ] +} +``` + +## Status + +**Version:** 0.1.0 (MVP) - ✅ Complete + +**Implemented & Tested:** +- ✅ JSON Schema validation (AJV-based with strict mode disabled for conditional properties) +- ✅ Reference integrity validation +- ✅ Complete execution pipeline (8 phases) +- ✅ Data source loaders (OSM, GeoJSON, CSV) - fully working +- ✅ Spatial join transform with aggregations (count, sum, avg, min, max, collect) +- ✅ Map view rendering (camera, layers, encodings, styles) - **fully implemented** +- ✅ Histogram view rendering (bins, field mapping, selections) - **fully implemented** +- ✅ Link management (selection → layer highlight) - **fully implemented** +- ✅ Layout system (vertical, horizontal, grid) - **fully implemented** +- ✅ Field-based color encoding with scales (quantile, viridis, greens, etc.) +- ✅ Style-based constant properties (color, opacity, size, stroke) + +**Test Results:** +- ✅ 15/17 tests passing (2 OSM tests deferred for network strategy) +- ✅ GeoJSON map rendering validated +- ✅ CSV + histogram rendering validated +- ✅ Spatial join execution validated +- ✅ Selection/highlight linking validated +- ✅ Browser integration tests passing + +**Not Yet Implemented (Deferred to v0.2+):** +- Arrow/Parquet data sources +- GeoTIFF raster support +- Heatmap transform +- Scatterplot, bar chart, table views +- Filter link action (highlight works) +- Advanced layouts (nested, responsive) +- Expression-language compute (WGSL only) +- Render compute pipelines +- anywidget bidirectional communication + +## Development + +Build the package: + +```bash +npm run build +``` + +Watch mode: + +```bash +npm run watch +``` + +## License + +MIT diff --git a/autk-runtime/TESTING.md b/autk-runtime/TESTING.md new file mode 100644 index 00000000..de99e382 --- /dev/null +++ b/autk-runtime/TESTING.md @@ -0,0 +1,417 @@ +# Testing AutarkRuntime + +**Status:** Local runtime tests pass. OSM/HAR and interaction coverage remain. + +This document describes the **repeatable, tiered testing strategy** for the runtime. Tests are split into **CI-safe automated tests** and **manual network/browser tests**. + +--- + +## Quick Start + +```bash +# From repository root: + +# 0. One-time setup: Create symlinks for Vite dev server access +# (Playwright webServer runs from gallery/, needs access to tests/, autk-runtime/, etc.) +cd gallery/public +ln -s ../../tests tests +ln -s ../../autk-runtime autk-runtime +ln -s ../../examples examples +ln -s ../../schema schema +cd ../.. + +# 1. Validate all spec examples (positive cases) +npm run validate:specs + +# 2. Build runtime +cd autk-runtime && npm run build && cd .. + +# 3. Run automated runtime tests (local fixtures, no network) +npm run test:runtime + +# 4. Run all tests (including gallery) +npm test +``` + +**Note:** The symlinks in `gallery/public/` allow the Vite dev server (started by Playwright) to serve test fixtures and runtime build files. Without these symlinks, browser tests will get 404 errors. + +--- + +## Current Status + +### CI-Safe Automated Tests +- ✅ **Schema validation** - Positive and negative fixtures +- ✅ **Build** - TypeScript compilation +- ✅ **Runtime fixtures** - Local GeoJSON/CSV with Playwright +- ✅ **Local example spec** - Spatial join example executes end-to-end +- ✅ **OSM with HAR** - Recorded Overpass playback runs in CI without network + +### Manual Tests (Not CI-Safe) +- ⏸️ **Real Overpass** - Network-dependent, for development only +- ⏸️ **Visual inspection** - Manual browser testing + +### Known Gaps + +1. **OSM examples** - HAR-backed OSM runtime coverage is automated; live Overpass execution remains network-dependent manual validation. +2. **Selection/highlight interactions** - Event-level histogram-to-map highlight propagation is covered; full pointer-level brushing can be added later if needed. +3. **Style coverage** - Constant fill color, stroke color, line width, opacity, and point size are covered; polygon stroke width still requires a mesh-based outline path. + +--- + +## Test Tiers + +### Tier 1: Schema Validation + +**Command:** +```bash +npm run validate:specs +``` + +**What it does:** +- Validates `examples/specs/*.json` against `schema/autark-spec-v0.1.json` +- Runs positive test cases (all 3 examples should validate) + +**Negative test cases:** +```bash +npx playwright test tests/runtime/schema-validation.test.ts +``` + +**What negative tests cover:** +- Misspelled top-level keys (`metadta` instead of `metadata`) +- Unsafe identifiers (`my-name!` instead of `my_name`) +- PBF source without `url` +- Empty `near: {}` in spatial join +- Unknown link targets +- CSV without `geometry` field + +**Success criteria:** +- All positive examples validate +- All negative cases are rejected with errors + +--- + +### Tier 2: Build + +**Command:** +```bash +cd autk-runtime && npm run build +``` + +**Success criteria:** +- TypeScript compiles without errors +- `autk-runtime/dist/` directory created +- `dist/index.js` and `dist/index.d.ts` exist + +--- + +### Tier 3: Runtime with Local Fixtures (CI-Safe) + +**Command:** +```bash +npm run test:runtime +``` + +**What it tests:** +- Runtime initialization (`AutarkRuntime.fromSpec()`) +- Local GeoJSON loading +- Local CSV loading with geometry +- Map rendering (canvas visible, non-blank pixels) +- Plot creation +- Metadata APIs: `getDb().getTablesMetadata()`, `getMaps()`, `getPlots()` + +**Fixtures:** +- `tests/fixtures/runtime/simple-points.geojson` +- `tests/fixtures/runtime/simple-polygons.geojson` +- `tests/fixtures/runtime/simple-points.csv` +- `tests/fixtures/runtime/fixture-01-geojson-map.json` +- `tests/fixtures/runtime/fixture-02-csv-histogram.json` + +**Success criteria:** +- ✅ Status shows "Runtime loaded successfully" +- ✅ Tables have correct names and types +- ✅ Maps and plots are created +- ✅ Canvas is visible with non-blank pixels +- ✅ No console errors +- ✅ `getTablesMetadata()` returns correct metadata + +**Current status:** ✅ Passing in `npm run test:runtime` + +--- + +### Tier 4: Runtime with OSM HAR Fixtures (CI-Safe) + +**Command:** +```bash +npx playwright test tests/runtime/runtime-osm-har.test.ts +``` + +**What it tests:** +- OSM data loading with HAR-backed Overpass responses +- OSM layer table naming (`${name}_${layer}`) +- Multi-layer maps + +**HAR fixtures:** +- Uses existing `tests/data/*.har` files from gallery tests +- Or create new `tests/data/runtime-*.har` files + +**Success criteria:** +- ✅ OSM tables created with correct naming +- ✅ Multiple layers rendered on map +- ✅ No network calls (all routed through HAR) + +**Current status:** ⏸️ Skipped - OSM strategy still needs deterministic HAR-backed validation or manual-network documentation + +**To re-record HAR:** +```bash +HAR_UPDATE=1 npx playwright test tests/runtime/runtime-osm-har.test.ts +``` + +--- + +### Tier 5: Visual Regression (Future) + +**Command:** (Future) +```bash +npx playwright test tests/runtime/ --update-snapshots +``` + +**What it would test:** +- Screenshot comparison for fixture-based specs +- Canvas pixel checks for expected rendering +- Expected number of DOM elements (maps, plots, status) + +**Success criteria:** +- Canvas screenshots match baseline +- No visual regressions + +--- + +### Tier 6: Manual Network Tests (Not CI-Safe) + +**Purpose:** Development and real-world validation only. + +**How to run:** +1. Start Vite dev server: + ```bash + cd gallery && npm run dev + ``` + +2. Open test harness: + ``` + http://localhost:5173/tests/fixtures/runtime/runtime-test-harness.html?spec=/examples/specs/01-basic-osm-map.json + ``` + +3. Inspect: + - Browser console for logs + - Network tab for Overpass calls + - Canvas rendering + - Status messages + +**Manual checklist:** +- [ ] All 3 examples load without errors +- [ ] OSM data loads from Overpass (network tab shows requests) +- [ ] Maps render with visible features +- [ ] Plots render with expected marks +- [ ] Linked selections work (brush histogram → highlight map) +- [ ] No console errors + +--- + +## Test Structure + +``` +tests/ +├── runtime/ +│ ├── schema-validation.test.ts # Positive and negative schema tests +│ ├── runtime-fixtures.test.ts # Local GeoJSON/CSV tests (CI-safe) +│ └── runtime-osm-har.test.ts # OSM with HAR (CI-safe) +├── fixtures/ +│ └── runtime/ +│ ├── runtime-test-harness.html # Browser test page +│ ├── simple-points.geojson # Local fixture +│ ├── simple-polygons.geojson # Local fixture +│ ├── simple-points.csv # Local fixture +│ ├── fixture-01-geojson-map.json # Test spec +│ └── fixture-02-csv-histogram.json # Test spec +├── data/ +│ └── *.har # HAR recordings for Overpass +└── helpers/ + └── route-overpass-har.ts # HAR routing utility + +autk-runtime/ +├── src/ # Runtime implementation +├── dist/ # Build output +├── package.json # Runtime package with test scripts +└── TESTING.md # This file + +examples/specs/ # Example specs (not for automated tests) +├── 01-basic-osm-map.json # Requires network +├── 02-linked-map-histogram.json # Requires network +└── 03-spatial-join.json # Requires missing fixtures +``` + +--- + +## Package Scripts + +### Root `package.json` +```json +{ + "scripts": { + "validate:specs": "ajv validate -s schema/autark-spec-v0.1.json -d \"examples/specs/*.json\" --strict=false", + "test:runtime": "playwright test tests/runtime" + } +} +``` + +### Runtime `autk-runtime/package.json` +```json +{ + "scripts": { + "build": "tsc", + "watch": "tsc --watch", + "clean": "rm -rf dist", + "test": "cd .. && npm run test:runtime", + "validate": "cd .. && npm run validate:specs" + } +} +``` + +--- + +## Debugging Runtime Issues + +### 1. Check Database Tables + +In browser console (when using test harness): + +```javascript +const runtime = window.__autarkRuntime; +const db = runtime.getDb(); +const tables = db.getTablesMetadata(); +console.table(tables.map(t => ({ name: t.name, type: t.type, source: t.source }))); +``` + +### 2. Check Maps and Plots + +```javascript +const maps = runtime.getMaps(); +console.log('Maps:', maps.map(m => m.name)); + +const plots = runtime.getPlots(); +console.log('Plots:', plots.map(p => p.name)); +``` + +### 3. Check Spec Parsing + +```javascript +const spec = window.__autarkSpec; +console.log('Loaded spec:', spec); +``` + +### 4. Common Errors + +**"Table X not found"** +- Check data loader completed +- Verify table naming (OSM uses `${name}_${layer}`) + +**"Cannot read property 'addEventListener' of undefined"** +- Plot not initialized correctly +- Check plot event name mismatch (`brush` vs `brushend`) + +**"updateStyle is not a function"** +- AutkMap doesn't support constant style updates +- Workaround: Use encoding with constant values + +--- + +## Success Criteria Summary + +### Measurable Assertions + +Instead of "visually inspect map," tests check: + +- ✅ Status element has class `success` +- ✅ Status text is "Runtime loaded successfully" +- ✅ `window.__autarkRuntime` is defined +- ✅ `getTablesMetadata()` returns expected table names +- ✅ Table count matches expected (e.g., 1 for single-source spec) +- ✅ Maps array has expected length +- ✅ Plots array has expected length +- ✅ Canvas element is visible +- ✅ Canvas has non-transparent pixels (basic rendering check) +- ✅ No `pageerror` events fired +- ✅ Metadata display shows correct table/map/plot counts + +### For Interactive Tests (Manual) + +- [ ] Brush on histogram triggers map highlight +- [ ] Map layers are visible +- [ ] Plot marks are visible +- [ ] Expected plot interactions work (click, brush, brushX, brushY) + +--- + +## Next Steps + +### Immediate (Phase A) +1. ✅ Add `validate:specs` and `test:runtime` scripts +2. ✅ Create negative schema validation tests +3. ✅ Create local fixture tests + +### Short-term (Phase B) +1. ✅ Fix plot event names in runtime (`brushX`) +2. ✅ Fix database API calls (`getTablesMetadata()` not `listTables()`) +3. ✅ Unskip OSM HAR tests +4. Add visual regression snapshots + +### Long-term (Phase C) +1. Add unit tests for runtime modules (layout, validation, data loaders) +2. Add interaction tests (selection propagation) +3. Add performance benchmarks (load time, render time) +4. Add more negative fixtures (all validation failure modes) + +--- + +## Design Notes + +### Why Not `autk-runtime/test/`? + +The repo's Playwright config (`playwright.config.ts`) only discovers tests in `./tests/`. Moving runtime tests to `autk-runtime/test/` would require changing the config and potentially breaking existing gallery tests. + +### Why Not `npx http-server`? + +The repo is Vite-heavy. Adding another dev server tool increases dependencies and cognitive load. The existing Vite server (`npm run dev` in gallery) can serve test files. + +### Why Local Fixtures for Tier 3? + +Real Overpass data is fragile for CI: +- Rate limits +- Network timeouts +- Response variability + +Local fixtures guarantee: +- No network calls +- Instant execution +- Repeatable results + +Real Overpass tests are Tier 6 (manual only) or Tier 4 (HAR-backed). + +### Why HAR Instead of Mocks? + +HAR files: +- Capture real Overpass responses +- Work with existing `routeFromHAR()` helper +- Can be updated when APIs change +- Are portable across test frameworks + +--- + +## References + +- **Plot events:** [autk-plot/src/types-events.ts](../../autk-plot/src/types-events.ts) +- **Database API:** [autk-db/src/db.ts](../../autk-db/src/db.ts) (line 143: `getTablesMetadata()`) +- **Playwright config:** [playwright.config.ts](../../playwright.config.ts) +- **HAR routing helper:** [tests/helpers/route-overpass-har.ts](../../tests/helpers/route-overpass-har.ts) +- **Example specs:** [examples/specs/](../../examples/specs/) +- **Schema:** [schema/autark-spec-v0.1.json](../../schema/autark-spec-v0.1.json) diff --git a/autk-runtime/package.json b/autk-runtime/package.json new file mode 100644 index 00000000..29025720 --- /dev/null +++ b/autk-runtime/package.json @@ -0,0 +1,44 @@ +{ + "name": "@urban-toolkit/autk-runtime", + "version": "0.1.0", + "description": "Autark specification runtime - executes declarative AutarkSpec JSON", + "type": "module", + "main": "./dist/autk-runtime.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/autk-runtime.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "vite build && vite build --config vite.widget.config.js", + "build:widget": "vite build --config vite.widget.config.js", + "dev": "vite build --watch", + "clean": "rm -rf dist", + "test": "cd .. && npm run test:runtime", + "validate": "cd .. && npm run validate:specs" + }, + "dependencies": { + "@urban-toolkit/autk-core": "2.2.0", + "@urban-toolkit/autk-db": "2.3.0", + "@urban-toolkit/autk-compute": "2.3.0", + "@urban-toolkit/autk-map": "2.3.0", + "@urban-toolkit/autk-plot": "2.3.0", + "ajv": "^8.12.0" + }, + "devDependencies": { + "typescript": "^5.3.3", + "vite": "^5.0.0", + "vite-plugin-dts": "^3.6.0", + "vite-plugin-glsl": "^1.1.2" + }, + "keywords": [ + "autark", + "urban-toolkit", + "visualization", + "runtime", + "declarative" + ], + "license": "MIT" +} diff --git a/autk-runtime/src/autark-schema.d.ts b/autk-runtime/src/autark-schema.d.ts new file mode 100644 index 00000000..f5462ff6 --- /dev/null +++ b/autk-runtime/src/autark-schema.d.ts @@ -0,0 +1,11 @@ +/** + * Type declaration for the bundled Autark JSON Schema. + * + * The `@autark-schema` alias resolves to `schema/autark-spec-v0.1.json` at the + * repo root (see vite.config.js / vite.widget.config.js) so the schema is + * inlined into the bundle instead of fetched at runtime. + */ +declare module '@autark-schema' { + const schema: Record; + export default schema; +} diff --git a/autk-runtime/src/data-loader.ts b/autk-runtime/src/data-loader.ts new file mode 100644 index 00000000..d4996110 --- /dev/null +++ b/autk-runtime/src/data-loader.ts @@ -0,0 +1,207 @@ +/** + * DataLoader - Loads data sources into AutkDb + */ + +import { AutkDb } from '@urban-toolkit/autk-db'; +import type { DataSource, Workspace } from './types.js'; + +export class DataLoader { + /** + * Initialize database with workspace configuration. + */ + async initializeDatabase(workspace?: Workspace): Promise { + const db = new AutkDb(); + await db.init(); + + // Set workspace name if provided + if (workspace?.name) { + await db.setWorkspace(workspace.name); + } + + // TODO: Set coordinate format if needed + + return db; + } + + /** + * Load a single data source into the database. + */ + async loadDataSource(db: AutkDb, source: DataSource): Promise { + switch (source.type) { + case 'osm': + await this.loadOsmSource(db, source); + break; + case 'geojson': + await this.loadGeoJsonSource(db, source); + break; + case 'csv': + await this.loadCsvSource(db, source); + break; + case 'json': + await this.loadJsonSource(db, source); + break; + case 'geotiff': + await this.loadGeoTiffSource(db, source); + break; + default: + throw new Error(`Unknown data source type: ${(source as DataSource).type}`); + } + } + + /** + * Load OSM data source. + * + * IMPORTANT: Must pass outputTableName to match spec convention. + */ + private async loadOsmSource(db: AutkDb, source: Extract): Promise { + // Most specs can use a single area string. HAR-backed and larger OSM + // workflows can provide a parent geocode area plus exact relation names. + const geocodeArea = source.geocodeArea ?? source.area; + const areas = source.areas ?? [source.area]; + + // CRITICAL: Pass outputTableName to ensure tables are named ${name}_${layer} + await db.loadOsm({ + outputTableName: source.name, // e.g., "manhattan_osm" + queryArea: { + geocodeArea, + areas, + }, + autoLoadLayers: { + layers: source.layers, + coordinateFormat: source.coordinateFormat, + }, + pbfFileUrl: source.pbfFileUrl, + forceRefresh: false, + }); + + // Result: tables created as manhattan_osm_buildings, manhattan_osm_roads, etc. + } + + /** + * Load GeoJSON data source. + */ + private async loadGeoJsonSource(db: AutkDb, source: Extract): Promise { + if (source.url) { + await db.loadGeojson({ + geojsonFileUrl: source.url, + outputTableName: source.name, + layerType: source.layerType, + coordinateFormat: source.coordinateFormat, + }); + } else if (source.values) { + // Support inline GeoJSON values + // Type assertion: spec defines values as 'object', loadGeojson expects FeatureCollection + await db.loadGeojson({ + geojsonObject: source.values as any, // TODO: Tighten spec type to FeatureCollection + outputTableName: source.name, + layerType: source.layerType, + coordinateFormat: source.coordinateFormat, + }); + } else { + throw new Error('GeoJSON source must have either url or values'); + } + } + + /** + * Load CSV data source. + */ + private async loadCsvSource(db: AutkDb, source: Extract): Promise { + if (!source.url) { + throw new Error('CSV source must have url (inline values not yet supported)'); + } + + const csvOptions: Parameters[0] = { + csvFileUrl: source.url, + outputTableName: source.name, + delimiter: source.delimiter, + }; + + // Add geometry configuration if present + if (source.geometry) { + if (source.geometry.type === 'latlng') { + csvOptions.geometryColumns = { + latColumnName: source.geometry.latitude, + longColumnName: source.geometry.longitude, + coordinateFormat: source.geometry.coordinateFormat, + }; + } else if (source.geometry.type === 'wkt') { + csvOptions.geometryColumns = { + wktColumnName: source.geometry.column, + coordinateFormat: source.geometry.coordinateFormat, + }; + } + } + + await db.loadCsv(csvOptions); + } + + /** + * Load JSON data source. + */ + private async loadJsonSource(db: AutkDb, source: Extract): Promise { + const jsonOptions: Parameters[0] = { + outputTableName: source.name, + }; + + if (source.url) { + jsonOptions.jsonFileUrl = source.url; + } else if (source.values) { + // The loadJson expects an array, so ensure we have an array + // If it's an object, wrap it in an array + const jsonArray = Array.isArray(source.values) ? source.values : [source.values]; + jsonOptions.jsonObject = jsonArray; + } else { + throw new Error('JSON source must have either url or values'); + } + + if (source.geometry) { + if (source.geometry.type === 'latlng') { + jsonOptions.geometryColumns = { + latColumnName: source.geometry.latitude, + longColumnName: source.geometry.longitude, + coordinateFormat: source.geometry.coordinateFormat, + }; + } else if (source.geometry.type === 'wkt') { + jsonOptions.geometryColumns = { + wktColumnName: source.geometry.column, + coordinateFormat: source.geometry.coordinateFormat, + }; + } + } + + // Note: The 'flatten' option from the schema could be used in the future + // to control how nested structures are handled + + await db.loadJson(jsonOptions); + } + + /** + * Load GeoTIFF data source. + */ + private async loadGeoTiffSource(db: AutkDb, source: Extract): Promise { + if (!source.url) { + throw new Error('GeoTIFF source must have url'); + } + + const geotiffOptions: Parameters[0] = { + geotiffFileUrl: source.url, + outputTableName: source.name, + }; + + // Add coordinate format override if specified + if (source.coordinateFormat) { + geotiffOptions.coordinateFormat = source.coordinateFormat; + } + if (source.maxPixels) { + geotiffOptions.maxPixels = source.maxPixels; + } + + // Note: Band selection is specified in the schema but not yet supported in AutkDb. + // When it becomes available, use: source.band - 1 (convert 1-based to 0-based) + if (source.band && source.band !== 1) { + console.warn(`GeoTIFF band selection requested (band ${source.band}) but not yet supported. Using default band.`); + } + + await db.loadGeoTiff(geotiffOptions); + } +} diff --git a/autk-runtime/src/index.ts b/autk-runtime/src/index.ts new file mode 100644 index 00000000..bdd5c469 --- /dev/null +++ b/autk-runtime/src/index.ts @@ -0,0 +1,52 @@ +/** + * @urban-toolkit/autk-runtime + * + * Runtime for executing declarative AutarkSpec specifications. + */ + +export { AutarkRuntime } from './runtime.js'; +export { SpecValidator } from './validator.js'; + +// Export types +export type { + AutarkSpec, + Metadata, + Workspace, + DataSource, + OsmDataSource, + GeoJsonDataSource, + CsvDataSource, + CsvGeometry, + Transform, + SpatialJoinTransform, + HeatmapTransform, + GpgpuComputeTransform, + RenderComputeTransform, + Aggregation, + View, + MapView, + MapLayer, + Camera, + Encoding, + EncodingChannel, + FieldEncoding, + ValueEncoding, + Scale, + LayerStyle, + HistogramView, + ScatterplotView, + TableView, + PlotAxis, + Selection, + Link, + Layout, + RuntimeOptions, +} from './types.js'; + +// Export error types +export { + AutarkRuntimeError, + SpecValidationError, + ReferenceError, + DataLoadError, +} from './types.js'; diff --git a/autk-runtime/src/layout-manager.ts b/autk-runtime/src/layout-manager.ts new file mode 100644 index 00000000..e4a70859 --- /dev/null +++ b/autk-runtime/src/layout-manager.ts @@ -0,0 +1,132 @@ +/** + * LayoutManager - Creates and manages DOM layout structure for views + */ + +import type { Layout, View } from './types.js'; + +export class LayoutManager { + private rootContainer: HTMLElement | null = null; + private viewContainers = new Map(); + + /** + * Create layout structure for views. + * + * @param container - Root container element + * @param views - Array of views to layout + * @param layout - Layout configuration + * @returns Map of view index to container element + */ + createLayout( + container: HTMLElement | string, + views: View[], + layout?: Layout + ): Map { + // Resolve container + if (typeof container === 'string') { + const el = document.querySelector(container); + if (!el) { + throw new Error(`Container not found: ${container}`); + } + this.rootContainer = el as HTMLElement; + } else { + this.rootContainer = container; + } + + // Clear existing content + this.rootContainer.innerHTML = ''; + + // Apply layout type + const layoutType = layout?.type || 'vertical'; + this.applyLayoutStyles(this.rootContainer, layoutType, layout); + + // Create containers for each view + for (let i = 0; i < views.length; i++) { + const viewContainer = this.createViewContainer(i, views[i], layoutType); + this.rootContainer.appendChild(viewContainer); + this.viewContainers.set(i, viewContainer); + } + + return this.viewContainers; + } + + /** + * Apply layout styles to root container. + */ + private applyLayoutStyles( + container: HTMLElement, + layoutType: 'vertical' | 'horizontal' | 'grid', + layout?: Layout + ): void { + container.style.display = 'flex'; + container.style.width = '100%'; + container.style.height = '100%'; + + switch (layoutType) { + case 'vertical': + container.style.flexDirection = 'column'; + break; + case 'horizontal': + container.style.flexDirection = 'row'; + break; + case 'grid': + container.style.display = 'grid'; + container.style.gridTemplateColumns = `repeat(${layout?.columns || 2}, 1fr)`; + break; + } + + // Apply gap if specified + if (layout?.gap !== undefined) { + container.style.gap = `${layout.gap}px`; + } else { + container.style.gap = '10px'; // Default gap + } + } + + /** + * Create a container for a single view. + */ + private createViewContainer( + index: number, + view: View, + layoutType: string + ): HTMLElement { + const container = document.createElement('div'); + container.className = `autark-view-container autark-view-${view.type}`; + container.dataset.viewIndex = index.toString(); + if (view.name) { + container.dataset.viewName = view.name; + } + + // Set flex behavior based on view type + if (layoutType === 'vertical') { + container.style.flex = view.type === 'map' ? '1 1 auto' : '0 0 auto'; + container.style.minHeight = view.type === 'map' ? '400px' : '300px'; + } else if (layoutType === 'horizontal') { + container.style.flex = '1 1 0'; + container.style.minWidth = '0'; + } + + container.style.width = '100%'; + container.style.position = 'relative'; + + return container; + } + + /** + * Get container for a specific view index. + */ + getViewContainer(index: number): HTMLElement | undefined { + return this.viewContainers.get(index); + } + + /** + * Clean up all containers. + */ + destroy(): void { + if (this.rootContainer) { + this.rootContainer.innerHTML = ''; + } + this.viewContainers.clear(); + this.rootContainer = null; + } +} diff --git a/autk-runtime/src/link-manager.ts b/autk-runtime/src/link-manager.ts new file mode 100644 index 00000000..153a1c3b --- /dev/null +++ b/autk-runtime/src/link-manager.ts @@ -0,0 +1,170 @@ +/** + * LinkManager - Manages cross-view selections and links + */ + +import type { AutkMap } from '@urban-toolkit/autk-map'; +import type { AutkPlot } from '@urban-toolkit/autk-plot'; +import { PlotEvent } from '@urban-toolkit/autk-plot'; +import type { Link, View } from './types.js'; + +export class LinkManager { + private eventCleanupFns: Array<() => void> = []; + + /** + * Connect selections and links between views. + */ + connectLinks( + links: Link[], + spec: { views: View[] }, + maps: Map, + plots: Map + ): void { + for (const link of links) { + this.connectLink(link, spec, maps, plots); + } + } + + /** + * Connect a single link. + */ + private connectLink( + link: Link, + spec: { views: View[] }, + maps: Map, + plots: Map + ): void { + // Find the view that has the selection + const sourceView = this.findViewWithSelection(spec.views, link.selection); + if (!sourceView) { + console.warn(`Selection not found: ${link.selection}`); + return; + } + + // Find the source plot/map + const sourcePlot = this.findPlotWithSelection(plots, link.selection); + if (!sourcePlot) { + console.warn(`Plot with selection ${link.selection} not found`); + return; + } + + // Find the target map layer + const targetMap = this.findMapWithLayer(spec.views, maps, link.target); + if (!targetMap) { + console.warn(`Target layer ${link.target} not found`); + return; + } + + // Wire up the link based on action + switch (link.action) { + case 'highlight': + this.connectHighlightLink(sourcePlot, targetMap.map, targetMap.layerId); + break; + default: + console.warn(`Unsupported link action: ${link.action}`); + } + } + + /** + * Connect a highlight link from plot selection to map layer. + */ + private connectHighlightLink( + sourcePlot: AutkPlot, + targetMap: AutkMap, + targetLayerId: string + ): void { + // Listen to plot selection changes + // AutkPlot fires events through events.on() + const handler = () => { + const selection = sourcePlot.selection; + + // Apply highlight to map layer + // AutkMap.setHighlightedIds() method highlights features by ID + if (selection && selection.length > 0) { + targetMap.setHighlightedIds(targetLayerId, selection); + } else { + // Clear highlight when selection is empty + targetMap.clearHighlightedIds(targetLayerId); + } + }; + + // Listen to selection events. Histograms use horizontal brushes, while + // other plot types may emit the generic brush event. + sourcePlot.events.on(PlotEvent.CLICK, handler); + sourcePlot.events.on(PlotEvent.BRUSH, handler); + sourcePlot.events.on(PlotEvent.BRUSH_X, handler); + + // Store cleanup function + this.eventCleanupFns.push(() => { + sourcePlot.events.off(PlotEvent.CLICK, handler); + sourcePlot.events.off(PlotEvent.BRUSH, handler); + sourcePlot.events.off(PlotEvent.BRUSH_X, handler); + }); + } + + /** + * Find view with the given selection name. + */ + private findViewWithSelection(views: View[], selectionName: string): View | undefined { + return views.find((view) => { + if ( + (view.type === 'histogram' || view.type === 'scatterplot' || view.type === 'table') && + view.selection + ) { + return view.selection.name === selectionName; + } + return false; + }); + } + + /** + * Find plot that has the given selection. + */ + private findPlotWithSelection( + plots: Map, + selectionName: string + ): AutkPlot | undefined { + for (const plot of plots.values()) { + // Check if plot has selection config stored + const selectionConfig = (plot as any)._selectionConfig; + if (selectionConfig?.name === selectionName) { + return plot; + } + } + return undefined; + } + + /** + * Find map and layer ID for the given target layer ID. + */ + private findMapWithLayer( + views: View[], + maps: Map, + targetLayerId: string + ): { map: AutkMap; layerId: string } | undefined { + // Find which view contains the target layer + for (let i = 0; i < views.length; i++) { + const view = views[i]; + if (view.type === 'map') { + const layer = view.layers.find((l) => l.id === targetLayerId); + if (layer) { + // Get the map for this view + const map = view.name ? maps.get(view.name) : maps.get(`_view_${i}`); + if (map) { + return { map, layerId: targetLayerId }; + } + } + } + } + return undefined; + } + + /** + * Clean up all event listeners. + */ + destroy(): void { + for (const cleanup of this.eventCleanupFns) { + cleanup(); + } + this.eventCleanupFns = []; + } +} diff --git a/autk-runtime/src/runtime.ts b/autk-runtime/src/runtime.ts new file mode 100644 index 00000000..8810923a --- /dev/null +++ b/autk-runtime/src/runtime.ts @@ -0,0 +1,409 @@ +/** + * AutarkRuntime - Main runtime class for executing AutarkSpec specifications + */ + +import type { AutkDb } from '@urban-toolkit/autk-db'; +import type { AutkMap } from '@urban-toolkit/autk-map'; +import type { AutkPlot } from '@urban-toolkit/autk-plot'; + +import type { + AutarkSpec, + RuntimeOptions, + DataSource, + Transform, + View, + Link, +} from './types.js'; +import { SpecValidator } from './validator.js'; +import { DataLoader } from './data-loader.js'; +import { TransformExecutor } from './transform-executor.js'; +import { ViewRenderer } from './view-renderer.js'; +import { LinkManager } from './link-manager.js'; +import { LayoutManager } from './layout-manager.js'; + +export interface RuntimeMapView { + name: string; + map: AutkMap; +} + +export interface RuntimePlotView { + name: string; + plot: AutkPlot; +} + +/** + * Main runtime class for executing Autark specifications. + * + * Usage: + * ```ts + * const runtime = await AutarkRuntime.fromSpec(spec, { + * container: document.getElementById('app') + * }); + * ``` + */ +export class AutarkRuntime { + private db?: AutkDb; + private maps = new Map(); + private plots = new Map(); + private validator?: SpecValidator; + private dataLoader: DataLoader; + private transformExecutor: TransformExecutor; + private viewRenderer: ViewRenderer; + private linkManager: LinkManager; + private layoutManager: LayoutManager; + private viewContainers = new Map(); + + private constructor( + private spec: AutarkSpec, + private options: RuntimeOptions + ) { + this.dataLoader = new DataLoader(); + this.transformExecutor = new TransformExecutor(); + this.viewRenderer = new ViewRenderer(); + this.linkManager = new LinkManager(); + this.layoutManager = new LayoutManager(); + } + + /** + * Create and execute a runtime from a specification. + * + * @param spec - AutarkSpec to execute + * @param options - Runtime configuration options + * @returns Initialized runtime instance + */ + static async fromSpec( + spec: AutarkSpec, + options: RuntimeOptions = {} + ): Promise { + const runtime = new AutarkRuntime(spec, { + validate: true, + onError: 'throw', + ...options, + }); + + await runtime.execute(); + return runtime; + } + + /** + * Main execution pipeline. + */ + private async execute(): Promise { + // Phase 1: Validate specification + if (this.options.validate !== false) { + this.validator = new SpecValidator(); + await this.validator.validate(this.spec); + } + + // Phase 2: Validate reference integrity + this.validateReferences(); + + // Phase 3: Initialize database if needed + if (this.spec.data || this.spec.transforms) { + this.db = await this.dataLoader.initializeDatabase(this.spec.workspace); + } + + // Phase 4: Load data sources + if (this.spec.data && this.db) { + await this.loadDataSources(this.spec.data); + } + + // Phase 5: Execute transforms + if (this.spec.transforms && this.db) { + await this.executeTransforms(this.spec.transforms); + } + + // Phase 6: Create DOM layout + this.createLayout(); + + // Phase 7: Render views + await this.renderViews(this.spec.views); + + // Phase 8: Connect selections and links + if (this.spec.links) { + this.connectLinks(this.spec.links); + } + } + + /** + * Validate that all references (data sources, selections, targets) exist. + */ + private validateReferences(): void { + const dataSources = new Set(this.spec.data?.map((d) => d.name) || []); + + // Add OSM sub-layer table names + this.spec.data?.forEach((source) => { + if (source.type === 'osm') { + source.layers.forEach((layer) => { + dataSources.add(`${source.name}_${layer}`); + }); + } + }); + + // Validate transform references + this.spec.transforms?.forEach((transform) => { + if (transform.type === 'spatialJoin') { + if (!dataSources.has(transform.root)) { + throw new Error(`Transform references unknown root data source: ${transform.root}`); + } + if (!dataSources.has(transform.join)) { + throw new Error(`Transform references unknown join data source: ${transform.join}`); + } + } else if (transform.type === 'heatmap') { + if (!dataSources.has(transform.source)) { + throw new Error(`Heatmap transform references unknown source data source: ${transform.source}`); + } + dataSources.add(transform.output); + } else if (transform.type === 'gpgpuCompute') { + if (!dataSources.has(transform.source)) { + throw new Error(`GPGPU compute transform references unknown source data source: ${transform.source}`); + } + dataSources.add(transform.output); + } else if (transform.type === 'renderCompute') { + for (const layer of transform.layers) { + if (!dataSources.has(layer.source)) { + throw new Error(`Render compute transform references unknown layer source: ${layer.source}`); + } + } + if (!dataSources.has(transform.viewpoints.source)) { + throw new Error(`Render compute transform references unknown viewpoints source: ${transform.viewpoints.source}`); + } + dataSources.add(transform.output); + } + }); + + // Validate view source references + const layerIds = new Set(); + this.spec.views.forEach((view) => { + if (view.type === 'map') { + view.layers.forEach((layer) => { + if (!dataSources.has(layer.source)) { + throw new Error(`Map layer references unknown data source: ${layer.source}`); + } + if (layer.id) { + layerIds.add(layer.id); + } + }); + } else if (view.type === 'histogram') { + if (!dataSources.has(view.source)) { + throw new Error(`Histogram references unknown data source: ${view.source}`); + } + } else if (view.type === 'scatterplot') { + if (!dataSources.has(view.source)) { + throw new Error(`Scatterplot references unknown data source: ${view.source}`); + } + } else if (view.type === 'table') { + if (!dataSources.has(view.source)) { + throw new Error(`Table references unknown data source: ${view.source}`); + } + } + }); + + // Collect selection names + const selectionNames = new Set(); + this.spec.views.forEach((view) => { + if ( + (view.type === 'histogram' || view.type === 'scatterplot' || view.type === 'table') && + view.selection + ) { + selectionNames.add(view.selection.name); + } + }); + + // Validate link references + this.spec.links?.forEach((link) => { + if (!selectionNames.has(link.selection)) { + throw new Error(`Link references unknown selection: ${link.selection}`); + } + if (!layerIds.has(link.target)) { + throw new Error(`Link references unknown layer ID: ${link.target}`); + } + }); + } + + /** + * Load all data sources into the database. + */ + private async loadDataSources(sources: DataSource[]): Promise { + if (!this.db) throw new Error('Database not initialized'); + + for (const source of sources) { + this.reportProgress(`Loading ${source.name}...`); + await this.dataLoader.loadDataSource(this.db, source); + } + } + + /** + * Execute all transforms in order. + */ + private async executeTransforms(transforms: Transform[]): Promise { + if (!this.db) throw new Error('Database not initialized'); + + for (const transform of transforms) { + this.reportProgress(`Executing transform...`); + await this.transformExecutor.executeTransform(this.db, transform); + } + } + + /** + * Create DOM layout structure. + */ + private createLayout(): void { + if (!this.options.container) { + throw new Error('Container option required for rendering views'); + } + + this.viewContainers = this.layoutManager.createLayout( + this.options.container, + this.spec.views, + this.spec.layout + ); + } + + /** + * Render all views. + */ + private async renderViews(views: View[]): Promise { + for (let i = 0; i < views.length; i++) { + const view = views[i]; + const container = this.viewContainers.get(i); + + if (!container) { + throw new Error(`No container found for view ${i}`); + } + + this.reportProgress(`Rendering ${view.type} view...`); + + if (view.type === 'map') { + const map = await this.viewRenderer.renderMap(this.db!, view, container); + if (view.name) { + this.maps.set(view.name, map); + } + // Store by index as well for link management + this.maps.set(`_view_${i}`, map); + } else if (view.type === 'histogram') { + const plot = await this.viewRenderer.renderHistogram(this.db!, view, container); + if (view.name) { + this.plots.set(view.name, plot); + } + // Store by index as well for link management + this.plots.set(`_view_${i}`, plot); + } else if (view.type === 'scatterplot') { + const plot = await this.viewRenderer.renderScatterplot(this.db!, view, container); + if (view.name) { + this.plots.set(view.name, plot); + } + this.plots.set(`_view_${i}`, plot); + } else if (view.type === 'table') { + const plot = await this.viewRenderer.renderTable(this.db!, view, container); + if (view.name) { + this.plots.set(view.name, plot); + } + this.plots.set(`_view_${i}`, plot); + } + } + } + + /** + * Connect selections and links between views. + */ + private connectLinks(links: Link[]): void { + this.linkManager.connectLinks(links, this.spec, this.maps, this.plots); + } + + /** + * Report progress if callback is configured. + */ + private reportProgress(message: string, percent?: number): void { + if (this.options.onProgress) { + this.options.onProgress(message, percent); + } + } + + /** + * Update the specification and re-render. + */ + async updateSpec(spec: AutarkSpec): Promise { + // Destroy current instance + await this.destroy(); + + // Update spec and re-execute + this.spec = spec; + await this.execute(); + } + + /** + * Clean up all resources. + */ + async destroy(): Promise { + // Destroy maps + for (const map of this.maps.values()) { + map.destroy(); + } + this.maps.clear(); + + // Destroy plots (if they have cleanup methods) + this.plots.clear(); + + // Clean up links + this.linkManager.destroy(); + + // Clean up layout + this.layoutManager.destroy(); + this.viewContainers.clear(); + + // Close database + if (this.db) { + // AutkDb doesn't currently expose a close() method + // TODO: Add db.close() once implemented + this.db = undefined; + } + } + + /** + * Get the database instance. + */ + getDb(): AutkDb | undefined { + return this.db; + } + + /** + * Get a map view by name. + */ + getMap(name?: string): AutkMap | undefined { + if (!name) { + // Return first map if no name specified + return this.maps.values().next().value; + } + return this.maps.get(name); + } + + /** + * Get all named map views. + */ + getMaps(): RuntimeMapView[] { + return [...this.maps.entries()] + .filter(([name]) => !name.startsWith('_view_')) + .map(([name, map]) => ({ name, map })); + } + + /** + * Get a plot view by name. + */ + getPlot(name?: string): AutkPlot | undefined { + if (!name) { + // Return first plot if no name specified + return this.plots.values().next().value; + } + return this.plots.get(name); + } + + /** + * Get all named plot views. + */ + getPlots(): RuntimePlotView[] { + return [...this.plots.entries()] + .filter(([name]) => !name.startsWith('_view_')) + .map(([name, plot]) => ({ name, plot })); + } +} diff --git a/autk-runtime/src/transform-executor.ts b/autk-runtime/src/transform-executor.ts new file mode 100644 index 00000000..b83f3ba2 --- /dev/null +++ b/autk-runtime/src/transform-executor.ts @@ -0,0 +1,205 @@ +/** + * TransformExecutor - Executes data transformations + */ + +import type { AutkDb } from '@urban-toolkit/autk-db'; +import { AutkComputeEngine } from '@urban-toolkit/autk-compute'; +import type { Transform } from './types.js'; + +export class TransformExecutor { + private compute = new AutkComputeEngine(); + + /** + * Execute a single transform. + */ + async executeTransform(db: AutkDb, transform: Transform): Promise { + switch (transform.type) { + case 'spatialJoin': + await this.executeSpatialJoin(db, transform); + break; + case 'heatmap': + await this.executeHeatmap(db, transform); + break; + case 'gpgpuCompute': + await this.executeGpgpuCompute(db, transform); + break; + case 'renderCompute': + await this.executeRenderCompute(db, transform); + break; + default: + throw new Error(`Unknown transform type: ${(transform as Transform).type}`); + } + } + + /** + * Execute spatial join transform. + * + * IMPORTANT: This mutates the root table in place (MVP semantics). + */ + private async executeSpatialJoin( + db: AutkDb, + transform: Extract + ): Promise { + // Build aggregation config + // Map 'op' field to 'aggregateFn' as expected by SpatialQueryParams + const groupBy = transform.groupBy?.map((agg) => ({ + column: agg.column, + aggregateFn: agg.op, + normalize: agg.normalize, + })); + + // Build near config if present + const nearConfig = transform.near ? { + distance: transform.near.distance, + useCentroid: transform.near.useCentroid ?? true, + } : undefined; + + // Execute spatial join + // Note: This mutates the root table in place + await db.spatialQuery({ + tableRootName: transform.root, + tableJoinName: transform.join, + near: nearConfig, + groupBy, + }); + + // After this, the root table (e.g., "neighborhoods") has new properties: + // properties.sjoin.count.tree_count, etc. + } + + /** + * Execute heatmap transform. + * + * Creates a new heatmap table. Downstream views can reference transform.output. + */ + private async executeHeatmap( + db: AutkDb, + transform: Extract + ): Promise { + await db.buildHeatmap({ + tableJoinName: transform.source, + outputTableName: transform.output, + near: { + distance: transform.near.distance, + useCentroid: transform.near.useCentroid ?? true, + }, + grid: transform.grid, + groupBy: transform.groupBy?.map((agg) => ({ + column: agg.column, + aggregateFn: agg.op, + normalize: agg.normalize, + })), + }); + } + + /** + * Execute GPGPU compute transform. + * + * Runs feature-level WGSL compute and materializes the returned collection as + * a new GeoJSON table. + */ + private async executeGpgpuCompute( + db: AutkDb, + transform: Extract + ): Promise { + this.validateWgslBody(transform.wgslBody); + + const collection = await db.getLayer(transform.source); + if (!collection) { + throw new Error(`GPGPU compute source not found: ${transform.source}`); + } + + const result = await this.compute.gpgpuPipeline({ + collection, + variableMapping: transform.variableMapping, + attributeArrays: transform.attributeArrays, + attributeMatrices: transform.attributeMatrices, + uniforms: transform.uniforms, + uniformArrays: transform.uniformArrays, + uniformMatrices: transform.uniformMatrices, + wgslBody: transform.wgslBody, + resultField: transform.resultField, + outputColumns: transform.outputColumns, + }); + + await db.loadGeojson({ + geojsonObject: result, + outputTableName: transform.output, + layerType: transform.layerType, + coordinateFormat: transform.coordinateFormat, + }); + } + + /** + * Execute render compute transform. + * + * Runs render sampling and materializes the viewpoint result collection as a + * new GeoJSON table. + */ + private async executeRenderCompute( + db: AutkDb, + transform: Extract + ): Promise { + const layers = await Promise.all(transform.layers.map(async (layer) => { + const collection = await db.getLayer(layer.source); + if (!collection) { + throw new Error(`Render compute layer source not found: ${layer.source}`); + } + return { + id: layer.id, + collection, + type: layer.type, + objectIdProperty: layer.objectIdProperty, + }; + })); + + const viewpointCollection = await db.getLayer(transform.viewpoints.source); + if (!viewpointCollection) { + throw new Error(`Render compute viewpoints source not found: ${transform.viewpoints.source}`); + } + + const result = await this.compute.renderPipeline({ + layers, + viewpoints: { + collection: viewpointCollection, + strategy: transform.viewpoints.strategy, + sampling: transform.viewpoints.sampling, + }, + aggregation: transform.aggregation, + camera: transform.camera, + tileSize: transform.tileSize, + }); + + await db.loadGeojson({ + geojsonObject: result, + outputTableName: transform.output, + layerType: transform.layerType, + coordinateFormat: transform.coordinateFormat, + }); + } + + /** + * Validate the WGSL function body accepted by declarative specs. + * + * The compute engine validates generated identifiers and the browser compiler + * validates WGSL syntax. This guard keeps specs constrained to a function + * body instead of a full WGSL module. + */ + private validateWgslBody(wgslBody: string): void { + const body = wgslBody.trim(); + if (!body) { + throw new Error('GPGPU compute wgslBody must not be empty'); + } + if (!/\breturn\b/.test(body)) { + throw new Error('GPGPU compute wgslBody must return a value'); + } + + const forbiddenPatterns = [ + /@\s*(compute|vertex|fragment|group|binding)\b/, + /\b(var\s*<\s*(storage|uniform)|fn|enable|requires|override)\b/, + ]; + if (forbiddenPatterns.some((pattern) => pattern.test(body))) { + throw new Error('GPGPU compute wgslBody must be a function body, not a full WGSL module'); + } + } +} diff --git a/autk-runtime/src/types.ts b/autk-runtime/src/types.ts new file mode 100644 index 00000000..f6f351ec --- /dev/null +++ b/autk-runtime/src/types.ts @@ -0,0 +1,387 @@ +/** + * AutarkSpec TypeScript types - generated from autark-spec-v0.1.json + * + * These types match the JSON Schema exactly and represent the declarative + * specification format for Autark urban visual analytics applications. + */ + +// ============================================================================ +// Top-Level Spec +// ============================================================================ + +export interface AutarkSpec { + $schema: 'https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json'; + version: '0.1'; + metadata?: Metadata; + workspace?: Workspace; + data?: DataSource[]; + transforms?: Transform[]; + views: View[]; // Required - at least one view + links?: Link[]; + layout?: Layout; +} + +// ============================================================================ +// Metadata and Workspace +// ============================================================================ + +export interface Metadata { + title?: string; + description?: string; + authors?: string[]; + created?: string; // ISO 8601 format recommended +} + +export interface Workspace { + name?: string; // Must match ^[A-Za-z_][A-Za-z0-9_]*$ + coordinateFormat?: string; // Default: "EPSG:4326" +} + +// ============================================================================ +// Data Sources +// ============================================================================ + +export type DataSource = OsmDataSource | GeoJsonDataSource | CsvDataSource | JsonDataSource | GeoTiffDataSource; + +export interface OsmDataSource { + type: 'osm'; + name: string; // SQL-safe identifier + area: string; // Place name for Overpass query + geocodeArea?: string; // Parent area used to resolve Overpass relation areas + areas?: string[]; // Relation area names inside geocodeArea + layers: Array<'buildings' | 'roads' | 'parks' | 'water' | 'surface'>; + source?: 'overpass' | 'pbf'; // Default: "overpass" + pbfFileUrl?: string; // Required if source is "pbf" + coordinateFormat?: string; +} + +export interface GeoJsonDataSource { + type: 'geojson'; + name: string; // SQL-safe identifier + url?: string; + values?: object; // GeoJSON FeatureCollection + layerType?: 'polygons' | 'polylines' | 'points' | 'buildings'; + coordinateFormat?: string; // Default: "EPSG:4326" +} + +export interface CsvDataSource { + type: 'csv'; + name: string; // SQL-safe identifier + url?: string; + values?: unknown[][]; // Array of arrays + delimiter?: string; // Default: "," + geometry?: CsvGeometry; +} + +export type CsvGeometry = CsvLatLngGeometry | CsvWktGeometry; + +export interface CsvLatLngGeometry { + type: 'latlng'; + latitude: string; // Column name + longitude: string; // Column name + coordinateFormat?: string; // Default: "EPSG:4326" +} + +export interface CsvWktGeometry { + type: 'wkt'; + column: string; // Column name containing WKT + coordinateFormat?: string; +} + +export interface JsonDataSource { + type: 'json'; + name: string; // SQL-safe identifier + url?: string; + values?: object | unknown[]; // JSON object or array + flatten?: boolean; // Default: false + geometry?: CsvGeometry; +} + +export interface GeoTiffDataSource { + type: 'geotiff'; + name: string; // SQL-safe identifier + url: string; // Required + band?: number; // Default: 1 (1-based index) + coordinateFormat?: string; // Override CRS + maxPixels?: number; +} + +// ============================================================================ +// Transforms +// ============================================================================ + +export type Transform = + | SpatialJoinTransform + | HeatmapTransform + | GpgpuComputeTransform + | RenderComputeTransform; + +export interface SpatialJoinTransform { + type: 'spatialJoin'; + root: string; // Data source name (mutated in place) + join: string; // Data source name + near?: { + distance: number; // exclusiveMinimum: 0 + useCentroid?: boolean; // Default: true + }; + groupBy?: Aggregation[]; +} + +export interface Aggregation { + column: string; // Use "*" for count + op: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'weighted' | 'collect'; + as?: string; // Output property name + normalize?: boolean; // Default: false +} + +export interface HeatmapTransform { + type: 'heatmap'; + source: string; + output: string; + near: { + distance: number; + useCentroid?: boolean; + }; + grid: { + rows: number; + columns: number; + }; + groupBy?: Array & { + op?: Exclude; + }>; +} + +export interface GpgpuComputeTransform { + type: 'gpgpuCompute'; + source: string; + output: string; + layerType?: 'polygons' | 'polylines' | 'points' | 'buildings'; + coordinateFormat?: string; + variableMapping: Record; + attributeArrays?: Record; + attributeMatrices?: Record; + uniforms?: Record; + uniformArrays?: Record; + uniformMatrices?: Record; + wgslBody: string; + resultField?: string; + outputColumns?: string[]; +} + +export interface RenderComputeTransform { + type: 'renderCompute'; + output: string; + layerType?: 'polygons' | 'polylines' | 'points' | 'buildings'; + coordinateFormat?: string; + layers: Array<{ + id: string; + source: string; + type: 'buildings' | 'roads' | 'polygons' | 'polylines' | 'points'; + objectIdProperty?: string; + }>; + viewpoints: { + source: string; + strategy?: { type: 'centroid' } | { type: 'building-windows'; floors: number }; + sampling?: { + directions?: number; + azimuthOffsetDeg?: number; + pitchDeg?: number; + }; + }; + aggregation: + | { + type: 'classes'; + includeBackground?: boolean; + backgroundLayerType?: string; + } + | { + type: 'objects'; + }; + camera?: { + fov?: number; + clip?: { + near?: number; + far?: number; + }; + }; + tileSize?: number; +} + +// ============================================================================ +// Views +// ============================================================================ + +export type View = MapView | HistogramView | ScatterplotView | TableView; + +export interface MapView { + type: 'map'; + name?: string; // SQL-safe identifier + camera?: Camera; + style?: Record; // Runtime-defined properties + layers: MapLayer[]; // At least one layer +} + +export interface Camera { + pitch?: number; // 0-90 degrees + bearing?: number; // 0-360 degrees + zoom?: number; // minimum: 0 +} + +export interface MapLayer { + source: string; // Data source name (e.g., "manhattan_osm_buildings") + id?: string; // SQL-safe identifier, required for link targets + type?: 'buildings' | 'roads' | 'polygons' | 'polylines' | 'points'; + encoding?: Encoding; + style?: LayerStyle; +} + +export interface Encoding { + color?: EncodingChannel; + opacity?: EncodingChannel; + size?: EncodingChannel; + height?: EncodingChannel; +} + +export type EncodingChannel = FieldEncoding | ValueEncoding; + +export interface FieldEncoding { + field: string; // Property path + scale?: Scale; +} + +export interface ValueEncoding { + value: string | number | boolean; // Constant value +} + +export interface Scale { + type?: 'linear' | 'quantile' | 'categorical'; // Default: "linear" + scheme?: string; // Color scheme name + domain?: unknown[]; + domainStrategy?: 'minMax' | 'percentile' | 'user'; // Default: "minMax" +} + +export interface LayerStyle { + opacity?: number; // 0-1 + color?: string; // Hex or CSS color + strokeColor?: string; + strokeWidth?: number; // minimum: 0 + width?: number; // Line width, minimum: 0 + size?: number; // Point size, minimum: 0 +} + +export interface HistogramView { + type: 'histogram'; + name?: string; // SQL-safe identifier + source: string; // Data source name + x: PlotAxis; + y?: PlotAxis; + bins?: number; // Default: 30, minimum: 1 + selection?: Selection; +} + +export interface ScatterplotView { + type: 'scatterplot'; + name?: string; // SQL-safe identifier + source: string; // Data source name + x: PlotAxis; + y: PlotAxis; + color?: PlotAxis; + selection?: Selection; +} + +export interface TableView { + type: 'table'; + name?: string; // SQL-safe identifier + source: string; // Data source name + columns: PlotAxis[]; + selection?: Selection; + sort?: { + column?: string; + direction?: 'asc' | 'desc'; + }; +} + +export interface PlotAxis { + field: string; // Property name (short form, no "properties." prefix) + scale?: Scale; +} + +// ============================================================================ +// Selections and Links +// ============================================================================ + +export interface Selection { + name: string; // SQL-safe identifier + type: 'point' | 'interval' | 'multi'; + fields?: string[]; +} + +export interface Link { + selection: string; // Selection name + target: string; // Layer ID + action: 'highlight'; // MVP: only "highlight", "filter" deferred +} + +// ============================================================================ +// Layout +// ============================================================================ + +export interface Layout { + type?: 'vertical' | 'horizontal' | 'grid'; // Default: "vertical" + columns?: number; // minimum: 1 + gap?: number; // minimum: 0 +} + +// ============================================================================ +// Runtime Options +// ============================================================================ + +export interface RuntimeOptions { + /** DOM container for rendering (required for browser context) */ + container?: HTMLElement | string; + + /** Validation mode */ + validate?: boolean | 'strict'; // Default: true + + /** Error handling strategy */ + onError?: 'throw' | 'warn' | 'silent'; // Default: "throw" + + /** Progress callback for long-running operations */ + onProgress?: (message: string, percent?: number) => void; +} + +// ============================================================================ +// Runtime Error Types +// ============================================================================ + +export class AutarkRuntimeError extends Error { + constructor( + message: string, + public code: string, + public context?: unknown + ) { + super(message); + this.name = 'AutarkRuntimeError'; + } +} + +export class SpecValidationError extends AutarkRuntimeError { + constructor(message: string, public errors: unknown[]) { + super(message, 'SPEC_VALIDATION_ERROR', { errors }); + this.name = 'SpecValidationError'; + } +} + +export class ReferenceError extends AutarkRuntimeError { + constructor(message: string, public ref: string) { + super(message, 'REFERENCE_ERROR', { ref }); + this.name = 'ReferenceError'; + } +} + +export class DataLoadError extends AutarkRuntimeError { + constructor(message: string, public source: string) { + super(message, 'DATA_LOAD_ERROR', { source }); + this.name = 'DataLoadError'; + } +} diff --git a/autk-runtime/src/validator.ts b/autk-runtime/src/validator.ts new file mode 100644 index 00000000..f5bfd173 --- /dev/null +++ b/autk-runtime/src/validator.ts @@ -0,0 +1,79 @@ +/** + * SpecValidator - Validates AutarkSpec against JSON Schema + */ + +import Ajv from 'ajv'; +// Bundled at build time (see the @autark-schema alias in the vite configs). +// Bundling instead of fetching keeps validation working when the runtime is +// loaded from a blob:/data: URL (e.g. as an anywidget module) or offline. +import schema from '@autark-schema'; +import type { AutarkSpec } from './types.js'; +import { SpecValidationError } from './types.js'; + +export class SpecValidator { + private ajv: Ajv; + private validateFn: ReturnType | null = null; + + constructor() { + this.ajv = new Ajv({ + allErrors: true, + verbose: true, + strict: false, // Disable strict mode to allow conditional required properties + }); + + this.validateFn = this.ajv.compile(schema); + } + + /** + * Ensure schema is loaded before validation. + */ + private async ensureSchemaLoaded(): Promise { + if (!this.validateFn) { + throw new Error('Schema validation function not initialized'); + } + } + + /** + * Validate a spec against the JSON Schema. + * Throws SpecValidationError if validation fails. + */ + async validate(spec: AutarkSpec): Promise { + await this.ensureSchemaLoaded(); + + const valid = this.validateFn!(spec); + + if (!valid && this.validateFn!.errors) { + const errorMessages = this.validateFn!.errors.map((err) => { + const path = err.instancePath || 'root'; + const message = err.message || 'Unknown error'; + return `${path}: ${message}`; + }); + + throw new SpecValidationError( + `Spec validation failed:\n${errorMessages.join('\n')}`, + this.validateFn!.errors + ); + } + } + + /** + * Check if a spec is valid without throwing. + */ + async isValid(spec: AutarkSpec): Promise { + try { + await this.validate(spec); + return true; + } catch { + return false; + } + } + + /** + * Get validation errors for a spec. + */ + async getErrors(spec: AutarkSpec): Promise { + await this.ensureSchemaLoaded(); + this.validateFn!(spec); + return this.validateFn!.errors || null; + } +} diff --git a/autk-runtime/src/view-renderer.ts b/autk-runtime/src/view-renderer.ts new file mode 100644 index 00000000..fc49b10c --- /dev/null +++ b/autk-runtime/src/view-renderer.ts @@ -0,0 +1,615 @@ +/** + * ViewRenderer - Renders map and plot views + */ + +import type { AutkDb } from '@urban-toolkit/autk-db'; +import { AutkMap } from '@urban-toolkit/autk-map'; +import { AutkPlot, PlotEvent } from '@urban-toolkit/autk-plot'; +import { ColorMap, ColorMapDomainStrategy } from '@urban-toolkit/autk-core'; +import type { ColorHEX, ColorRGB } from '@urban-toolkit/autk-core'; +import type { MapView, HistogramView, ScatterplotView, TableView, EncodingChannel, Selection } from './types.js'; + +export class ViewRenderer { + /** + * Render a map view. + */ + async renderMap(db: AutkDb, view: MapView, container: HTMLElement): Promise { + // Create canvas element + const canvas = document.createElement('canvas'); + canvas.style.width = '100%'; + canvas.style.height = '100%'; + container.appendChild(canvas); + + const map = new AutkMap(canvas); + await map.init(); + + // Load and render layers, collecting their data for camera centering + const collections: any[] = []; + for (const layerSpec of view.layers) { + const collection = await this.renderMapLayer(db, map, layerSpec); + collections.push(collection); + } + + console.log('About to apply camera settings...'); + + // Apply camera settings AFTER loading data so we can auto-center + try { + if (view.camera) { + console.log('Applying camera settings from spec'); + this.applyCameraSettings(map, view.camera, collections); + } else { + // No camera specified - auto-center on data + console.log('Auto-centering camera on data'); + this.autoCenterCamera(map, collections); + } + console.log('Camera settings applied successfully'); + } catch (error) { + console.error('Error applying camera settings:', error); + throw error; + } + + console.log('About to call map.draw()...'); + + // Trigger rendering! + console.log('Calling map.draw() to render', collections.length, 'collections'); + console.log('Layer manager has', map.layerManager.layers.length, 'layers'); + map.draw(); + console.log('map.draw() completed'); + + return map; + } + + /** + * Auto-center camera on loaded data. + */ + private autoCenterCamera(map: AutkMap, collections: any[]): void { + const bounds = this.calculateBounds(collections); + if (!bounds) { + console.warn('No bounds calculated for auto-centering'); + return; + } + + console.log('Calculated bounds:', bounds); + + const camera = map.camera; + const centerX = (bounds.minX + bounds.maxX) / 2; + const centerY = (bounds.minY + bounds.maxY) / 2; + const origin = map.layerManager.origin; + const localCenterX = centerX - origin[0]; + const localCenterY = centerY - origin[1]; + + console.log(`Setting camera center to [${localCenterX}, ${localCenterY}]`); + + // Position the camera above the data and point it at the same XY center. + // Moving only the eye leaves the default lookAt at [0, 0, 0], which points + // the camera away from projected urban datasets. + // @ts-ignore - reading protected member + const currentZ = camera.wEye[2]; + camera.resetCamera([0, 1, 0], [localCenterX, localCenterY, 0], [localCenterX, localCenterY, currentZ]); + camera.update(); + + // @ts-ignore - accessing protected member for logging + const eyeX = camera.wEye[0]; + // @ts-ignore - accessing protected member for logging + const eyeY = camera.wEye[1]; + // @ts-ignore - accessing protected member for logging + const eyeZ = camera.wEye[2]; + console.log(`Camera eye after update: [${eyeX}, ${eyeY}, ${eyeZ}]`); + } + + /** + * Calculate bounding box of all features in collections. + */ + private calculateBounds(collections: any[]): { minX: number; maxX: number; minY: number; maxY: number } | null { + if (!collections || collections.length === 0) return null; + + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + + for (const collection of collections) { + if (!collection?.features) continue; + + for (const feature of collection.features) { + if (!feature?.geometry?.coordinates) continue; + + const coords = feature.geometry.coordinates; + const geomType = feature.geometry.type; + + // Extract coordinates based on geometry type + if (geomType === 'Point') { + const [x, y] = coords; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } else if (geomType === 'LineString' || geomType === 'MultiPoint') { + for (const [x, y] of coords) { + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } else if (geomType === 'Polygon' || geomType === 'MultiLineString') { + for (const ring of coords) { + for (const [x, y] of ring) { + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + } else if (geomType === 'MultiPolygon') { + for (const polygon of coords) { + for (const ring of polygon) { + for (const [x, y] of ring) { + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + } + } + } + } + + if (!isFinite(minX)) return null; + + return { minX, maxX, minY, maxY }; + } + + /** + * Apply camera settings to map. + * + * Note: The spec uses absolute pitch/bearing/zoom values, but Camera API + * uses relative deltas. For MVP, we apply deltas from default state. + * TODO: Add absolute camera positioning methods to Camera class. + */ + private applyCameraSettings( + map: AutkMap, + cameraSpec: { pitch?: number; bearing?: number; zoom?: number }, + collections: any[] + ): void { + const camera = map.camera; + + // Auto-center on data first (sets X and Y) + this.autoCenterCamera(map, collections); + + // Now apply zoom AFTER auto-centering (set Z coordinate) + console.log('Applying zoom, spec value:', cameraSpec.zoom); + + if (cameraSpec.zoom !== undefined) { + const zoomScale = Math.pow(2, cameraSpec.zoom - 10); + const targetZ = 10000 / zoomScale; + + console.log(` Zoom scale: ${zoomScale}, target Z: ${targetZ}`); + + // @ts-ignore - accessing protected member + const currentX = camera.wEye[0]; + // @ts-ignore - accessing protected member + const currentY = camera.wEye[1]; + camera.resetCamera([0, 1, 0], [currentX, currentY, 0], [currentX, currentY, targetZ]); + + console.log(' Reset camera Z to', targetZ); + } else { + console.log(' No zoom specified, keeping default Z'); + } + + // Apply pitch and bearing + if (cameraSpec.pitch !== undefined && cameraSpec.pitch !== 0) { + const pitchRadians = (cameraSpec.pitch * Math.PI) / 180; + camera.pitch(pitchRadians); + } + + if (cameraSpec.bearing !== undefined && cameraSpec.bearing !== 0) { + const bearingRadians = (cameraSpec.bearing * Math.PI) / 180; + camera.yaw(bearingRadians); + } + + // Final camera update after all changes + console.log('Calling final camera.update()...'); + camera.update(); + + // @ts-ignore - logging final position + const finalX = camera.wEye[0]; + // @ts-ignore + const finalY = camera.wEye[1]; + // @ts-ignore + const finalZ = camera.wEye[2]; + console.log(`Camera final position: [${finalX}, ${finalY}, ${finalZ}]`); + } + + /** + * Render a single map layer. + * Returns the collection for camera positioning. + */ + private async renderMapLayer( + db: AutkDb, + map: AutkMap, + layerSpec: MapView['layers'][0] + ): Promise { + // getLayer() returns FeatureCollection directly + const collection = await db.getLayer(layerSpec.source); + + if (!collection) { + throw new Error(`Failed to load layer data: ${layerSpec.source}`); + } + + const layerId = layerSpec.id || layerSpec.source; + + // Determine layer type + const layerType = layerSpec.type || this.inferLayerType(collection); + + console.log(`Loading layer "${layerId}" with type "${layerType}"`); + console.log(` Feature count: ${collection.features?.length || 0}`); + console.log(` First feature geometry type: ${collection.features?.[0]?.geometry?.type}`); + + // Load collection into map + // loadCollection signature: (id: string, params: { collection, type?, ... }) + map.loadCollection(layerId, { + collection, + type: layerType, + lineWidth: layerSpec.style?.width, + }); + + console.log(` Layer "${layerId}" loaded successfully`); + + // Apply encoding if specified + if (layerSpec.encoding) { + await this.applyEncoding(map, layerId, collection, layerSpec.encoding); + } + + // Apply constant styles if specified + if (layerSpec.style) { + this.applyStyle(map, layerId, layerSpec.style); + } + + // Return collection for camera positioning + return collection; + } + + /** + * Infer layer rendering type from data. + */ + private inferLayerType(_collection: any): 'buildings' | 'polygons' | 'points' | 'polylines' | null { + // TODO: Inspect geometry types from _collection to infer layer type + // For now, default to buildings for 3D + return 'buildings'; + } + + /** + * Apply visual encodings to a layer. + */ + private async applyEncoding( + map: AutkMap, + layerId: string, + collection: any, + encoding: MapView['layers'][0]['encoding'] + ): Promise { + if (!encoding) return; + + // Apply color encoding + if (encoding.color) { + await this.applyColorEncoding(map, layerId, collection, encoding.color); + } + + // Apply opacity encoding + if (encoding.opacity) { + // TODO: Implement opacity encoding + console.warn('Opacity encoding not yet implemented'); + } + + // Apply size encoding + if (encoding.size) { + // TODO: Implement size encoding + console.warn('Size encoding not yet implemented'); + } + + // Apply height encoding + if (encoding.height) { + // TODO: Implement height encoding (for 3D extrusion) + console.warn('Height encoding not yet implemented'); + } + } + + /** + * Apply color encoding channel. + */ + private async applyColorEncoding( + map: AutkMap, + layerId: string, + collection: any, + colorChannel: EncodingChannel + ): Promise { + if ('field' in colorChannel) { + // Field-based encoding + const field = colorChannel.field; + const scale = colorChannel.scale; + + // Convert field name to property path (e.g., "value" -> "properties.value") + // AutkMap expects dot-path notation like "properties.fieldName" + const propertyPath = field.startsWith('properties.') ? field : `properties.${field}`; + + // Step 1: Update thematic mapping (assigns values to features) + map.updateThematic(layerId, { + collection, + property: propertyPath, + }); + + // Step 2: Update color map configuration (sets color scheme and domain strategy) + if (scale) { + const domainSpec = this.createDomainSpec(scale.type); + map.updateColorMap(layerId, { + colorMap: { + // Map scale type to domain strategy + ...(domainSpec && { domainSpec }), + // Map scheme name to interpolator + // TODO: Add proper scheme name to ColorMapInterpolator mapping + // For now, this will use the default interpolator + }, + }); + } + } else if ('value' in colorChannel) { + const color = this.parseColorValue(colorChannel.value); + if (color) { + map.updateRenderInfo(layerId, { color, isColorMap: false }); + } else { + console.warn(`Layer ${layerId}: color encoding value is not a supported color (${String(colorChannel.value)})`); + } + } + } + + /** + * Create ColorMapDomainSpec from spec scale type. + * + * ColorMapDomainSpec is a discriminated union that requires different shapes + * for each strategy type. + */ + private createDomainSpec( + scaleType?: 'linear' | 'quantile' | 'categorical' + ): { type: ColorMapDomainStrategy.MIN_MAX } | { type: ColorMapDomainStrategy.PERCENTILE } | undefined { + switch (scaleType) { + case 'linear': + return { type: ColorMapDomainStrategy.MIN_MAX }; + case 'quantile': + return { type: ColorMapDomainStrategy.PERCENTILE }; + case 'categorical': + // AutkMap doesn't have a categorical strategy yet, use minMax + return { type: ColorMapDomainStrategy.MIN_MAX }; + default: + return undefined; // Let updateColorMap use its default + } + } + + /** + * Apply constant style properties to a layer. + */ + private applyStyle( + map: AutkMap, + layerId: string, + style: MapView['layers'][0]['style'] + ): void { + if (!style) return; + + // Get the layer from the layer manager + const layer = map.layerManager.searchByLayerId(layerId); + if (!layer) { + console.warn(`Layer ${layerId}: not found in layer manager`); + return; + } + + // Apply opacity via updateRenderInfo + if (style.opacity !== undefined) { + map.updateRenderInfo(layerId, { opacity: style.opacity }); + console.log(`Layer ${layerId}: set opacity to ${style.opacity}`); + } + + // Apply point size directly to SpriteLayer + if (style.size !== undefined) { + // Check if this is a SpriteLayer (points) + if ('_pointSize' in layer) { + // @ts-ignore - accessing private member + layer._pointSize = style.size; + console.log(`Layer ${layerId}: set point size to ${style.size}`); + } else { + console.warn(`Layer ${layerId}: size style only applies to point layers`); + } + } + + // Constant color needs color mapping disabled and uniform color + if (style.color) { + const color = this.parseColorValue(style.color); + if (color) { + map.updateRenderInfo(layerId, { color, isColorMap: false }); + console.log(`Layer ${layerId}: set constant color to ${style.color}`); + } else { + console.warn(`Layer ${layerId}: unsupported color style (${style.color})`); + } + } + + // Stroke styles + if (style.strokeColor) { + const strokeColor = this.parseColorValue(style.strokeColor); + if (strokeColor) { + map.updateRenderInfo(layerId, { strokeColor }); + console.log(`Layer ${layerId}: set stroke color to ${style.strokeColor}`); + } else { + console.warn(`Layer ${layerId}: unsupported stroke color style (${style.strokeColor})`); + } + } + + if (style.strokeWidth !== undefined) { + console.warn(`Layer ${layerId}: strokeWidth style not yet supported (requested: ${style.strokeWidth})`); + } + + // Width for lines + if (style.width) { + if (layer.layerInfo.typeLayer === 'roads' || layer.layerInfo.typeLayer === 'polylines') { + console.log(`Layer ${layerId}: set line width to ${style.width}`); + } else { + console.warn(`Layer ${layerId}: width style only applies to road/polyline layers`); + } + } + } + + /** + * Convert a runtime color value into the map renderer's RGB uniform payload. + */ + private parseColorValue(value: unknown): ColorRGB | null { + if (typeof value !== 'string' || value.trim().length === 0) { + return null; + } + + try { + const color = ColorMap.hexToRgb(value.trim() as ColorHEX); + if ([color.r, color.g, color.b].some(channel => Number.isNaN(channel))) { + return null; + } + return color; + } catch (_error) { + return null; + } + } + + /** + * Render a histogram view. + */ + async renderHistogram( + db: AutkDb, + view: HistogramView, + container: HTMLElement + ): Promise { + // getLayer() returns FeatureCollection directly + const collection = await db.getLayer(view.source); + + if (!collection) { + throw new Error(`Failed to load data for histogram: ${view.source}`); + } + + const xField = view.x.field; + + const plot = new AutkPlot(container, { + type: 'barchart', + collection, + attributes: { + axis: [xField, '@transform'], + }, + transform: { + preset: 'binning-1d', + options: { + bins: view.bins || 30, + }, + }, + events: [PlotEvent.BRUSH_X], + labels: { + axis: [xField, 'count'], + ...(view.name && { title: view.name }), + }, + }); + + // Store selection configuration for link management + if (view.selection) { + // Selection setup will be handled by LinkManager + // Store selection metadata on plot for retrieval + (plot as any)._selectionConfig = view.selection; + } + + return plot; + } + + /** + * Render a scatterplot view. + */ + async renderScatterplot( + db: AutkDb, + view: ScatterplotView, + container: HTMLElement + ): Promise { + const collection = await db.getLayer(view.source); + + if (!collection) { + throw new Error(`Failed to load data for scatterplot: ${view.source}`); + } + + const xField = view.x.field; + const yField = view.y.field; + + const plot = new AutkPlot(container, { + type: 'scatterplot', + collection, + attributes: { + axis: [xField, yField], + ...(view.color && { color: view.color.field }), + }, + events: this.eventsForSelection(view.selection, [PlotEvent.CLICK]), + labels: { + axis: [xField, yField], + ...(view.name && { title: view.name }), + }, + }); + + if (view.selection) { + (plot as any)._selectionConfig = view.selection; + } + + return plot; + } + + /** + * Render a table view. + */ + async renderTable( + db: AutkDb, + view: TableView, + container: HTMLElement + ): Promise { + const collection = await db.getLayer(view.source); + + if (!collection) { + throw new Error(`Failed to load data for table: ${view.source}`); + } + + const columns = view.columns.map((column) => column.field); + + const plot = new AutkPlot(container, { + type: 'table', + collection, + attributes: { + axis: columns, + }, + events: this.eventsForSelection(view.selection, [PlotEvent.CLICK]), + labels: { + axis: columns, + ...(view.name && { title: view.name }), + }, + transform: { + preset: 'sort', + options: { + column: view.sort?.column ?? columns[0], + direction: view.sort?.direction ?? 'asc', + }, + }, + }); + + if (view.selection) { + (plot as any)._selectionConfig = view.selection; + } + + return plot; + } + + /** + * Select plot events according to the requested selection type. + */ + private eventsForSelection(selection: Selection | undefined, fallback: PlotEvent[]): PlotEvent[] { + if (!selection) { + return fallback; + } + if (selection.type === 'interval') { + return [PlotEvent.BRUSH]; + } + return [PlotEvent.CLICK]; + } +} diff --git a/autk-runtime/src/widget.ts b/autk-runtime/src/widget.ts new file mode 100644 index 00000000..ba40e40b --- /dev/null +++ b/autk-runtime/src/widget.ts @@ -0,0 +1,154 @@ +/** + * anywidget entry point for the Autark runtime. + * + * Built as a self-contained ESM bundle (see vite.widget.config.js) that ships + * inside the Python package (`python/autark/static/autark-widget.js`). Loaded + * by anywidget in Jupyter/JupyterLab/VS Code/Colab without any local server: + * DuckDB assets are fetched from the jsDelivr CDN (see autk-db) and the spec + * schema is bundled inline (see validator.ts). + * + * Selections made in plots (histogram brushes) are synced back to the kernel + * through the `selections` trait: `{ [selectionName]: number[] }`. + */ + +import { PlotEvent } from '@urban-toolkit/autk-plot'; + +import { AutarkRuntime } from './runtime.js'; +import type { AutarkSpec } from './types.js'; + +interface AnyModel { + get(name: string): unknown; + on(event: string, callback: () => void): void; + off(event: string, callback: () => void): void; + // Present on real anywidget models; absent in the static fake model used by + // embedded HTML exports (see python/autark/display.py). + set?(name: string, value: unknown): void; + save_changes?(): void; +} + +interface RenderContext { + model: AnyModel; + el: HTMLElement; +} + +function showError(container: HTMLElement, error: unknown): void { + const pre = document.createElement('pre'); + pre.style.whiteSpace = 'pre-wrap'; + pre.style.color = '#b00020'; + pre.textContent = error instanceof Error ? (error.stack ?? error.message) : String(error); + container.replaceChildren(pre); +} + +/** + * Subscribes to plot selection events and mirrors them into the model's + * `selections` trait so the Python kernel can observe brushes. + * + * @param runtime Initialized runtime for the current spec. + * @param spec Spec the runtime was created from (provides selection names). + * @param model anywidget model; a no-op if it cannot sync (`set` missing). + * @returns Cleanup functions removing the event subscriptions. + */ +function connectSelectionSync( + runtime: AutarkRuntime, + spec: AutarkSpec, + model: AnyModel, +): Array<() => void> { + if (typeof model.set !== 'function' || typeof model.save_changes !== 'function') { + return []; + } + + const cleanups: Array<() => void> = []; + const current: Record = {}; + + spec.views.forEach((view, index) => { + if (view.type !== 'histogram' || !view.selection) { + return; + } + const plot = (view.name ? runtime.getPlot(view.name) : undefined) ?? runtime.getPlot(`_view_${index}`); + if (!plot) { + return; + } + + const selectionName = view.selection.name; + const handler = () => { + current[selectionName] = [...(plot.selection ?? [])]; + model.set!('selections', { ...current }); + model.save_changes!(); + }; + + plot.events.on(PlotEvent.BRUSH, handler); + plot.events.on(PlotEvent.BRUSH_X, handler); + cleanups.push(() => { + plot.events.off(PlotEvent.BRUSH, handler); + plot.events.off(PlotEvent.BRUSH_X, handler); + }); + }); + + return cleanups; +} + +async function render({ model, el }: RenderContext) { + const container = document.createElement('div'); + container.style.width = '100%'; + container.style.height = (model.get('height') as string) || '640px'; + el.appendChild(container); + + let runtime: AutarkRuntime | undefined; + let selectionCleanups: Array<() => void> = []; + let generation = 0; + + const clearSelectionSync = () => { + for (const cleanup of selectionCleanups) { + cleanup(); + } + selectionCleanups = []; + }; + + const execute = async () => { + const current = ++generation; + try { + clearSelectionSync(); + if (runtime) { + await runtime.destroy(); + runtime = undefined; + } + container.replaceChildren(); + const spec = model.get('spec') as AutarkSpec; + const next = await AutarkRuntime.fromSpec(spec, { container }); + if (current !== generation) { + // A newer spec arrived while this one was loading. + await next.destroy(); + return; + } + runtime = next; + selectionCleanups = connectSelectionSync(next, spec, model); + // Exposed for tests and debugging. + (el as HTMLElement & { __autarkRuntime?: AutarkRuntime }).__autarkRuntime = next; + } catch (error) { + if (current === generation) { + showError(container, error); + } + } + }; + + const onSpecChange = () => { + void execute(); + }; + const onHeightChange = () => { + container.style.height = (model.get('height') as string) || '640px'; + }; + + model.on('change:spec', onSpecChange); + model.on('change:height', onHeightChange); + await execute(); + + return () => { + model.off('change:spec', onSpecChange); + model.off('change:height', onHeightChange); + clearSelectionSync(); + void runtime?.destroy(); + runtime = undefined; + }; +} + +export default { render }; diff --git a/autk-runtime/tsconfig.json b/autk-runtime/tsconfig.json new file mode 100644 index 00000000..2b86033d --- /dev/null +++ b/autk-runtime/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + + /* Emit declarations for published package */ + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"] +} diff --git a/autk-runtime/vite.config.js b/autk-runtime/vite.config.js new file mode 100644 index 00000000..c12d5d83 --- /dev/null +++ b/autk-runtime/vite.config.js @@ -0,0 +1,58 @@ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import glsl from 'vite-plugin-glsl'; + +function duckdbAssets() { + const assets = [ + 'duckdb-mvp.wasm', + 'duckdb-browser-mvp.worker.js', + 'duckdb-eh.wasm', + 'duckdb-browser-eh.worker.js', + ]; + + return { + name: 'duckdb-assets', + generateBundle() { + for (const fileName of assets) { + this.emitFile({ + type: 'asset', + fileName, + source: readFileSync(resolve(__dirname, '../node_modules/@duckdb/duckdb-wasm/dist', fileName)), + }); + } + }, + }; +} + +export default defineConfig({ + resolve: { + alias: { + '@autark-schema': resolve(__dirname, '../schema/autark-spec-v0.1.json'), + '@urban-toolkit/autk-core': resolve(__dirname, '../autk-core/src/index.ts'), + '@urban-toolkit/autk-db': resolve(__dirname, '../autk-db/src/index.ts'), + '@urban-toolkit/autk-compute': resolve(__dirname, '../autk-compute/src/index.ts'), + '@urban-toolkit/autk-map': resolve(__dirname, '../autk-map/src/index.ts'), + '@urban-toolkit/autk-plot': resolve(__dirname, '../autk-plot/src/index.ts'), + }, + }, + plugins: [glsl(), duckdbAssets(), dts({ insertTypesEntry: true })], + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'AutkRuntime', + fileName: 'autk-runtime', + formats: ['es'], + }, + copyPublicDir: false, + emptyOutDir: true, + sourcemap: true, + // Bundle all dependencies for browser use + // This makes the runtime self-contained for browser/notebook embedding + rollupOptions: { + // Don't externalize any dependencies - bundle everything + external: [], + }, + }, +}); diff --git a/autk-runtime/vite.widget.config.js b/autk-runtime/vite.widget.config.js new file mode 100644 index 00000000..e7b2fc27 --- /dev/null +++ b/autk-runtime/vite.widget.config.js @@ -0,0 +1,45 @@ +import { resolve } from 'path'; +import { defineConfig } from 'vite'; +import glsl from 'vite-plugin-glsl'; + +/** + * Builds the anywidget ESM bundle shipped inside the Python package. + * + * Differences from the main runtime build (vite.config.js): + * - Single self-contained file (dynamic imports inlined) because anywidget + * loads the module from a blob:/data: URL where relative chunk imports and + * asset URLs cannot resolve. + * - No DuckDB worker/wasm assets are emitted: autk-db falls back to the + * jsDelivr CDN bundles when the module origin is not http(s). + * - Output goes to python/autark/static/ so it is packaged with the wheel. + */ +export default defineConfig({ + resolve: { + alias: { + '@autark-schema': resolve(__dirname, '../schema/autark-spec-v0.1.json'), + '@urban-toolkit/autk-core': resolve(__dirname, '../autk-core/src/index.ts'), + '@urban-toolkit/autk-db': resolve(__dirname, '../autk-db/src/index.ts'), + '@urban-toolkit/autk-compute': resolve(__dirname, '../autk-compute/src/index.ts'), + '@urban-toolkit/autk-map': resolve(__dirname, '../autk-map/src/index.ts'), + '@urban-toolkit/autk-plot': resolve(__dirname, '../autk-plot/src/index.ts'), + }, + }, + plugins: [glsl()], + build: { + lib: { + entry: resolve(__dirname, 'src/widget.ts'), + fileName: () => 'autark-widget.js', + formats: ['es'], + }, + outDir: resolve(__dirname, '../python/autark/static'), + copyPublicDir: false, + emptyOutDir: true, + sourcemap: false, + rollupOptions: { + external: [], + output: { + inlineDynamicImports: true, + }, + }, + }, +}); diff --git a/cors_server.py b/cors_server.py new file mode 100755 index 00000000..d75d75f7 --- /dev/null +++ b/cors_server.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +""" +Simple HTTP server with CORS enabled for local development. +This allows Jupyter (running on a different port) to load the Autark runtime. +""" +from http.server import HTTPServer, SimpleHTTPRequestHandler +import sys + + +class CORSRequestHandler(SimpleHTTPRequestHandler): + def end_headers(self): + # Add CORS headers to allow cross-origin requests + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Headers', '*') + self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate') + super().end_headers() + + def do_OPTIONS(self): + # Handle preflight requests + self.send_response(200) + self.end_headers() + + +def run(port=8000): + server_address = ('', port) + httpd = HTTPServer(server_address, CORSRequestHandler) + print(f"🚀 CORS-enabled server running on http://localhost:{port}") + print(f" Press Ctrl+C to stop") + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\n🛑 Server stopped") + sys.exit(0) + + +if __name__ == '__main__': + port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000 + run(port) diff --git a/examples/data/neighborhoods.geojson b/examples/data/neighborhoods.geojson new file mode 100644 index 00000000..e3979f1b --- /dev/null +++ b/examples/data/neighborhoods.geojson @@ -0,0 +1,53 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "West Square" + }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [-73.9900, 40.7400], + [-73.9800, 40.7400], + [-73.9800, 40.7500], + [-73.9900, 40.7500], + [-73.9900, 40.7400] + ]] + } + }, + { + "type": "Feature", + "properties": { + "name": "East Square" + }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [-73.9800, 40.7400], + [-73.9700, 40.7400], + [-73.9700, 40.7500], + [-73.9800, 40.7500], + [-73.9800, 40.7400] + ]] + } + }, + { + "type": "Feature", + "properties": { + "name": "North Square" + }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [-73.9900, 40.7500], + [-73.9800, 40.7500], + [-73.9800, 40.7600], + [-73.9900, 40.7600], + [-73.9900, 40.7500] + ]] + } + } + ] +} diff --git a/examples/data/trees.csv b/examples/data/trees.csv new file mode 100644 index 00000000..f00b897c --- /dev/null +++ b/examples/data/trees.csv @@ -0,0 +1,5 @@ +id,species,latitude,longitude +1,oak,40.7484,-73.9857 +2,maple,40.7514,-73.9897 +3,elm,40.7454,-73.9837 +4,oak,40.7460,-73.9760 diff --git a/examples/specs/01-basic-osm-map.json b/examples/specs/01-basic-osm-map.json new file mode 100644 index 00000000..1285665f --- /dev/null +++ b/examples/specs/01-basic-osm-map.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "Manhattan Buildings - Basic OSM Map", + "description": "Simple example loading OSM buildings and rendering them with height-based color encoding", + "created": "2026-06-07" + }, + "workspace": { + "name": "manhattan_demo", + "coordinateFormat": "EPSG:4326" + }, + "data": [ + { + "type": "osm", + "name": "manhattan_osm", + "area": "Manhattan, New York", + "layers": ["buildings", "roads"], + "source": "overpass" + } + ], + "views": [ + { + "type": "map", + "name": "main_map", + "camera": { + "pitch": 55, + "bearing": 20, + "zoom": 13 + }, + "layers": [ + { + "source": "manhattan_osm_buildings", + "id": "buildings_layer", + "type": "buildings", + "encoding": { + "color": { + "field": "properties.height", + "scale": { + "type": "linear", + "scheme": "viridis", + "domainStrategy": "minMax" + } + } + }, + "style": { + "opacity": 0.9 + } + }, + { + "source": "manhattan_osm_roads", + "id": "roads_layer", + "type": "roads", + "style": { + "color": "#444444", + "width": 1.5 + } + } + ] + } + ], + "layout": { + "type": "vertical" + } +} diff --git a/examples/specs/02-linked-map-histogram.json b/examples/specs/02-linked-map-histogram.json new file mode 100644 index 00000000..ec3bfa83 --- /dev/null +++ b/examples/specs/02-linked-map-histogram.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "Linked Map and Histogram - Building Heights", + "description": "Demonstrates linked selection between a map and histogram. Brushing the histogram highlights buildings on the map.", + "created": "2026-06-07" + }, + "workspace": { + "name": "linked_views_demo", + "coordinateFormat": "EPSG:4326" + }, + "data": [ + { + "type": "osm", + "name": "chicago_osm", + "area": "Chicago, Illinois", + "layers": ["buildings"], + "source": "overpass" + } + ], + "views": [ + { + "type": "map", + "name": "building_map", + "camera": { + "pitch": 60, + "bearing": 0, + "zoom": 14 + }, + "layers": [ + { + "source": "chicago_osm_buildings", + "id": "buildings_layer", + "type": "buildings", + "encoding": { + "color": { + "field": "properties.height", + "scale": { + "type": "linear", + "scheme": "plasma", + "domainStrategy": "minMax" + } + } + }, + "style": { + "opacity": 0.85 + } + } + ] + }, + { + "type": "histogram", + "name": "height_histogram", + "source": "chicago_osm_buildings", + "x": { + "field": "height", + "scale": { + "type": "linear" + } + }, + "bins": 30, + "selection": { + "name": "height_brush", + "type": "interval", + "fields": ["height"] + } + } + ], + "links": [ + { + "selection": "height_brush", + "target": "buildings_layer", + "action": "highlight" + } + ], + "layout": { + "type": "vertical" + } +} diff --git a/examples/specs/03-spatial-join.json b/examples/specs/03-spatial-join.json new file mode 100644 index 00000000..66f324a8 --- /dev/null +++ b/examples/specs/03-spatial-join.json @@ -0,0 +1,117 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "Spatial Join - Trees per Neighborhood", + "description": "Demonstrates spatial join between neighborhoods and tree points, aggregating tree counts and visualizing on a thematic map.", + "created": "2026-06-07" + }, + "workspace": { + "name": "spatial_join_demo", + "coordinateFormat": "EPSG:4326" + }, + "data": [ + { + "type": "geojson", + "name": "neighborhoods", + "url": "/examples/data/neighborhoods.geojson", + "coordinateFormat": "EPSG:4326", + "layerType": "polygons" + }, + { + "type": "csv", + "name": "trees", + "url": "/examples/data/trees.csv", + "geometry": { + "type": "latlng", + "latitude": "latitude", + "longitude": "longitude", + "coordinateFormat": "EPSG:4326" + } + } + ], + "transforms": [ + { + "type": "spatialJoin", + "root": "neighborhoods", + "join": "trees", + "groupBy": [ + { + "column": "*", + "op": "count" + }, + { + "column": "species", + "op": "collect", + "as": "tree_species" + } + ] + } + ], + "views": [ + { + "type": "map", + "name": "neighborhood_map", + "camera": { + "pitch": 0, + "bearing": 0, + "zoom": 12 + }, + "layers": [ + { + "source": "neighborhoods", + "id": "neighborhoods_layer", + "type": "polygons", + "encoding": { + "color": { + "field": "properties.sjoin.count.trees", + "scale": { + "type": "quantile", + "scheme": "greens", + "domainStrategy": "minMax" + } + } + }, + "style": { + "opacity": 0.8, + "strokeColor": "#333333", + "strokeWidth": 1 + } + }, + { + "source": "trees", + "id": "trees_layer", + "type": "points", + "style": { + "color": "#228B22", + "size": 3, + "opacity": 0.6 + } + } + ] + }, + { + "type": "histogram", + "name": "tree_count_histogram", + "source": "neighborhoods", + "x": { + "field": "sjoin.count.trees" + }, + "bins": 20, + "selection": { + "name": "tree_count_brush", + "type": "interval" + } + } + ], + "links": [ + { + "selection": "tree_count_brush", + "target": "neighborhoods_layer", + "action": "highlight" + } + ], + "layout": { + "type": "vertical" + } +} diff --git a/examples/specs/04-geojson-input.json b/examples/specs/04-geojson-input.json new file mode 100644 index 00000000..7560cd8f --- /dev/null +++ b/examples/specs/04-geojson-input.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "GeoJSON Input - Neighborhood Polygons", + "description": "Minimal example loading polygons from a GeoJSON file and rendering them with constant styling.", + "created": "2026-06-10" + }, + "workspace": { + "name": "geojson_input_demo", + "coordinateFormat": "EPSG:4326" + }, + "data": [ + { + "type": "geojson", + "name": "neighborhoods", + "url": "/examples/data/neighborhoods.geojson", + "coordinateFormat": "EPSG:4326", + "layerType": "polygons" + } + ], + "views": [ + { + "type": "map", + "name": "neighborhoods_map", + "camera": { + "pitch": 0, + "bearing": 0, + "zoom": 12 + }, + "layers": [ + { + "source": "neighborhoods", + "id": "neighborhoods_layer", + "type": "polygons", + "style": { + "color": "#4a90e2", + "opacity": 0.7, + "strokeColor": "#2c5aa0", + "strokeWidth": 2 + } + } + ] + } + ] +} diff --git a/examples/specs/05-csv-points.json b/examples/specs/05-csv-points.json new file mode 100644 index 00000000..accb8068 --- /dev/null +++ b/examples/specs/05-csv-points.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "CSV Points - Trees with Histogram", + "description": "Loads point data from a CSV file using lat/lng geometry columns and links a map with a latitude histogram.", + "created": "2026-06-10" + }, + "workspace": { + "name": "csv_points_demo", + "coordinateFormat": "EPSG:4326" + }, + "data": [ + { + "type": "csv", + "name": "trees", + "url": "/examples/data/trees.csv", + "geometry": { + "type": "latlng", + "latitude": "latitude", + "longitude": "longitude", + "coordinateFormat": "EPSG:4326" + } + } + ], + "views": [ + { + "type": "map", + "name": "trees_map", + "camera": { + "pitch": 0, + "bearing": 0, + "zoom": 13 + }, + "layers": [ + { + "source": "trees", + "id": "trees_layer", + "type": "points", + "style": { + "color": "#228B22", + "size": 5, + "opacity": 0.8 + } + } + ] + }, + { + "type": "histogram", + "name": "latitude_histogram", + "source": "trees", + "x": { + "field": "latitude" + }, + "bins": 20, + "selection": { + "name": "latitude_brush", + "type": "interval" + } + } + ], + "links": [ + { + "selection": "latitude_brush", + "target": "trees_layer", + "action": "highlight" + } + ], + "layout": { + "type": "vertical" + } +} diff --git a/examples/specs/06-multiple-layers.json b/examples/specs/06-multiple-layers.json new file mode 100644 index 00000000..52983077 --- /dev/null +++ b/examples/specs/06-multiple-layers.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "Multiple Layers - Buildings, Roads, and Parks", + "description": "Composes several OSM sub-layers on one map: extruded buildings colored by height over roads and parks.", + "created": "2026-06-10" + }, + "workspace": { + "name": "multiple_layers_demo", + "coordinateFormat": "EPSG:4326" + }, + "data": [ + { + "type": "osm", + "name": "manhattan_osm", + "area": "Manhattan, New York", + "layers": ["buildings", "roads", "parks"], + "source": "overpass" + } + ], + "views": [ + { + "type": "map", + "name": "layered_map", + "camera": { + "pitch": 45, + "bearing": 15, + "zoom": 13 + }, + "layers": [ + { + "source": "manhattan_osm_parks", + "id": "parks_layer", + "type": "polygons", + "style": { + "color": "#7cb342", + "opacity": 0.6 + } + }, + { + "source": "manhattan_osm_roads", + "id": "roads_layer", + "type": "roads", + "style": { + "color": "#444444", + "width": 1.5 + } + }, + { + "source": "manhattan_osm_buildings", + "id": "buildings_layer", + "type": "buildings", + "encoding": { + "color": { + "field": "properties.height", + "scale": { + "type": "linear", + "scheme": "viridis", + "domainStrategy": "minMax" + } + } + }, + "style": { + "opacity": 0.9 + } + } + ] + } + ], + "layout": { + "type": "vertical" + } +} diff --git a/examples/specs/07-json-data.json b/examples/specs/07-json-data.json new file mode 100644 index 00000000..f2500514 --- /dev/null +++ b/examples/specs/07-json-data.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "JSON Data Source Example", + "description": "Example demonstrating JSON data loading with points visualization" + }, + "workspace": { + "name": "json_example", + "coordinateFormat": "EPSG:4326" + }, + "data": [ + { + "type": "json", + "name": "events", + "geometry": { + "type": "latlng", + "latitude": "lat", + "longitude": "lon", + "coordinateFormat": "EPSG:4326" + }, + "values": [ + {"id": 1, "name": "Event A", "lat": 40.7128, "lon": -74.0060, "attendance": 150}, + {"id": 2, "name": "Event B", "lat": 40.7580, "lon": -73.9855, "attendance": 200}, + {"id": 3, "name": "Event C", "lat": 40.7489, "lon": -73.9680, "attendance": 175}, + {"id": 4, "name": "Event D", "lat": 40.7614, "lon": -73.9776, "attendance": 225}, + {"id": 5, "name": "Event E", "lat": 40.7282, "lon": -73.9942, "attendance": 190} + ] + } + ], + "views": [ + { + "type": "scatterplot", + "name": "scatter", + "source": "events", + "x": { + "field": "id" + }, + "y": { + "field": "attendance" + }, + "color": { + "field": "name" + } + }, + { + "type": "table", + "name": "table", + "source": "events", + "columns": [ + {"field": "id"}, + {"field": "name"}, + {"field": "lat"}, + {"field": "lon"}, + {"field": "attendance"} + ] + } + ], + "layout": { + "type": "grid", + "columns": 1 + } +} diff --git a/examples/specs/08-geotiff-raster.json b/examples/specs/08-geotiff-raster.json new file mode 100644 index 00000000..92fc9bb9 --- /dev/null +++ b/examples/specs/08-geotiff-raster.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "GeoTIFF Data Example", + "description": "Example demonstrating GeoTIFF raster data loading and table inspection" + }, + "workspace": { + "name": "raster_example", + "coordinateFormat": "EPSG:4326" + }, + "data": [ + { + "type": "geotiff", + "name": "temperature", + "url": "https://example.com/data/land_surface_temperature.tif", + "band": 1, + "maxPixels": 1000000 + } + ], + "views": [ + { + "type": "table", + "name": "temperature_table", + "source": "temperature", + "columns": [ + {"field": "x"}, + {"field": "y"}, + {"field": "value"} + ], + "sort": { + "column": "value", + "direction": "desc" + } + } + ] +} diff --git a/examples/specs/README.md b/examples/specs/README.md new file mode 100644 index 00000000..59cc2e0a --- /dev/null +++ b/examples/specs/README.md @@ -0,0 +1,183 @@ +# Autark Spec Examples + +This directory contains hand-authored JSON specifications for the AutarkSpec v0.1 format. These examples were created to validate the schema design before implementing the TypeScript runtime and Python API. + +## Purpose + +These examples serve multiple purposes: + +1. **Schema validation** - Identify missing fields, inconsistencies, or unclear semantics +2. **Runtime design** - Guide the implementation of `AutarkRuntime.fromSpec()` +3. **Python API design** - Inform what the Python builders need to generate +4. **Documentation** - Provide reference implementations for users + +## Examples + +### 01-basic-osm-map.json + +**Purpose:** Simplest possible example - load OSM data and render it on a map. + +**Features demonstrated:** +- OSM data source with Overpass API +- Multiple OSM layers (buildings, roads) +- Map view with camera configuration +- Color encoding based on building height +- Linear color scale with Viridis scheme +- Layer styling (opacity, color, width) + +**Key design questions exposed:** +- How are OSM sub-layers referenced? (e.g., `manhattan_osm.buildings`) +- What are valid layer type values? (`buildings`, `roads`, `polygons`, etc.) +- What camera parameters are required vs optional? +- What encoding types are supported? (field-based vs value-based) + +### 02-linked-map-histogram.json + +**Purpose:** Demonstrate coordinated views with selection and linking. + +**Features demonstrated:** +- Map view +- Histogram plot view +- Interval selection on histogram +- Link from selection to map layer (highlight action) +- Vertical layout composition + +**Key design questions exposed:** +- How are selections defined? (name, type, fields) +- How are links specified? (selection name, target layer id, action) +- What are valid link actions? (`highlight`, `filter`, `focus`) +- How does selection propagate across views? +- What is the layout model? + +### 03-spatial-join.json + +**Purpose:** Demonstrate spatial data transformation and multi-source composition. + +**Features demonstrated:** +- GeoJSON data source (neighborhoods) +- CSV data source with lat/lng geometry (trees) +- Spatial join transform with aggregation +- Count aggregation +- Collect aggregation (for species) +- Thematic mapping of join results +- Multiple layers on one map (polygons + points) +- Filter action for linked selection + +**Key design questions exposed:** +- How are transforms named and referenced? +- What is the output of a spatial join? (new data source?) +- How are join results accessed? (`properties.sjoin.count.tree_count`) +- Can transforms reference other transforms? +- How are CSV geometry specifications structured? +- What aggregation operations are supported? +- How are point layers styled? + +## ✅ Executable Conventions (Locked 2026-06-07) + +**All examples have been updated to use runtime-executable syntax.** These conventions are locked for MVP implementation. + +### 1. OSM Sub-Layer Table Naming +Use underscore concatenation: `${name}_${layer}` + +```json +{"data": [{"type": "osm", "name": "chicago_osm", "layers": ["buildings"]}], + "views": [{"layers": [{"source": "chicago_osm_buildings"}]}]} +``` + +### 2. Transform Mutation Semantics (MVP) +Spatial joins mutate root table in place. Reference root table after transform. + +```json +{"transforms": [{"type": "spatialJoin", "root": "neighborhoods", "join": "trees"}], + "views": [{"layers": [{"source": "neighborhoods"}]}]} +``` + +### 3. Field Path Normalization +- **Map encodings:** Use full paths (`properties.height`) +- **Plot fields:** Use property names only (`height`) + +```json +{"views": [ + {"type": "map", "layers": [{"encoding": {"color": {"field": "properties.height"}}}]}, + {"type": "histogram", "x": {"field": "height"}} +]} +``` + +### 4. Link Target Semantics +Links target **layer IDs**, not data sources. + +```json +{"links": [{"selection": "height_brush", "target": "buildings_layer"}]} +``` + +### 5. Schema Key +Use `"$schema"` (JSON Schema standard). + +### 6. Data File References +Examples use placeholder paths. Runtime tests will point to actual fixtures. + +--- + +## Design Resolution Summary + +All major design questions raised by these examples have been resolved and locked as executable conventions: + +1. **OSM layer references** → Use `${name}_${layer}` (e.g., `manhattan_osm_buildings`) +2. **Transform outputs** → Mutate root table in place for MVP +3. **Field paths** → Full paths for maps (`properties.height`), short for plots (`height`) +4. **Link targets** → Layer IDs, not data sources +5. **Link actions** → `highlight` for MVP, `filter` deferred +6. **Layout** → Simple vertical/horizontal for MVP, composition operators post-MVP + +See [PYTHON_API_IMPLEMENTATION.md](../../PYTHON_API_IMPLEMENTATION.md) "Locked Executable Spec Conventions" for full details. + +## Schema Gaps Identified + +Based on these examples, the JSON Schema needs: + +### Required Types +- [x] Top-level `AutarkSpec` +- [x] `Metadata` object +- [x] `Workspace` object +- [x] Data sources: `OsmDataSpec`, `GeoJsonDataSpec`, `CsvDataSpec` +- [x] Transform: `SpatialJoinSpec` with aggregation options +- [x] Views: `MapViewSpec`, `HistogramSpec` +- [x] `MapLayerSpec` with encoding and style +- [x] `EncodingSpec` with field/value variants +- [x] `ScaleSpec` with type and scheme +- [x] `SelectionSpec` +- [x] `LinkSpec` +- [x] `LayoutSpec` (basic) + +### Missing Specifications +- [ ] Camera configuration details (pitch, bearing, zoom ranges) +- [ ] Complete list of valid layer types +- [ ] Complete list of valid color schemes +- [ ] Scale domain strategy options +- [ ] Link action semantics +- [ ] Error handling configuration +- [ ] Loading strategy options (for future) + +### Validation Rules +- [ ] Data source names must be unique +- [ ] Transform names must be unique +- [ ] Layer ids must be unique within a map +- [ ] Selection names must be unique +- [ ] Link targets must reference existing layers or data sources +- [ ] Field references must be valid property paths + +## Next Steps + +1. **Create JSON Schema** - Define `autark-spec-v0.1.json` based on these examples +2. **Validate examples** - Ensure all three examples validate against the schema +3. **Implement runtime** - Build `AutarkRuntime.fromSpec()` to execute these specs +4. **Fix transform semantics** - Add `outputTableName` support to `spatialQuery()` +5. **Build Python API** - Create builders that generate equivalent JSON +6. **Add more examples** - Scatterplot, multiple selections, etc. + +## Notes + +- All examples use `"version": "0.1"` and reference schema at `https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json` +- Examples use realistic URLs/paths but don't include actual data files yet +- Examples focus on MVP features only (no compute, no GeoTIFF, no advanced layouts) +- Examples intentionally expose design ambiguities that need resolution diff --git a/gallery/public/autk-runtime b/gallery/public/autk-runtime new file mode 120000 index 00000000..70b2e93c --- /dev/null +++ b/gallery/public/autk-runtime @@ -0,0 +1 @@ +../../autk-runtime \ No newline at end of file diff --git a/gallery/public/examples b/gallery/public/examples new file mode 120000 index 00000000..d15735c1 --- /dev/null +++ b/gallery/public/examples @@ -0,0 +1 @@ +../../examples \ No newline at end of file diff --git a/gallery/public/schema b/gallery/public/schema new file mode 120000 index 00000000..56c062f7 --- /dev/null +++ b/gallery/public/schema @@ -0,0 +1 @@ +../../schema \ No newline at end of file diff --git a/gallery/public/tests b/gallery/public/tests new file mode 120000 index 00000000..c2ebfe53 --- /dev/null +++ b/gallery/public/tests @@ -0,0 +1 @@ +../../tests \ No newline at end of file diff --git a/osmnx-autark.md b/osmnx-autark.md new file mode 100644 index 00000000..7185ae37 --- /dev/null +++ b/osmnx-autark.md @@ -0,0 +1,167 @@ +• OSMnx is a mature Python geospatial/network analysis library built on GeoPandas, NetworkX, Shapely, and pandas. I’ll + inspect the key public APIs for graph/geometries conversion so I can give concrete Autark integration examples. + +• OSMnx and Autark are complementary, not competitors. + + What OSMnx Is Good At + OSMnx is a Python analysis library for OpenStreetMap data. Its core strengths are: + + - Downloading OSM street networks by place, point, bbox, polygon. + - Modeling street networks as NetworkX MultiDiGraphs. + - Simplifying street topology. + - Computing network measures: intersections, street length, bearings, connectivity, circuity, etc. + - Routing and travel-time analysis. + - Converting graphs to GeoDataFrames with ox.graph_to_gdfs(). + - Querying arbitrary OSM features into GeoDataFrames with ox.features_from_*(). + + It is fundamentally a Python geospatial/network analysis tool. + + What Autark Is Good At + Autark is a browser/runtime visualization and analytics system: + + - WebGPU map rendering. + - DuckDB-WASM spatial data loading/querying in-browser. + - Linked maps/plots/tables. + - Declarative AutarkSpec. + - Python builders that serialize to JSON/HTML/widgets. + - Browser-side spatial joins, heatmaps, compute transforms, and interaction. + + It is fundamentally a visual analytics runtime and rendering system. + + Best Way To Use Them Together + Use OSMnx for Python-side OSM/network analysis, then hand the resulting GeoDataFrames to Autark for interactive + visualization. + + Example: analyze street network in OSMnx, visualize in Autark. + + import osmnx as ox + import autark as ak + + # 1. Download/analyze with OSMnx + G = ox.graph_from_place("Manhattan, New York, USA", network_type="walk") + G = ox.add_edge_speeds(G) + G = ox.add_edge_travel_times(G) + + nodes, edges = ox.graph_to_gdfs(G) + + # 2. Convert OSMnx GeoDataFrames into Autark sources + edges_source = ak.GeoJSON.from_geopandas( + "walk_edges", + edges.reset_index(), + layer_type="polylines", + ) + + nodes_source = ak.GeoJSON.from_geopandas( + "walk_nodes", + nodes.reset_index(), + layer_type="points", + ) + + # 3. Visualize with Autark + spec = ak.Spec( + data=[edges_source, nodes_source], + views=[ + ak.Map( + layers=[ + ak.Layer(edges_source, type="polylines").encode( + color=ak.field( + "travel_time", + scale=ak.Scale(type="quantile", scheme="viridis"), + ) + ), + ak.Layer(nodes_source, type="points").style( + color="#222222", + size=2, + opacity=0.5, + ), + ] + ), + ak.Histogram(edges_source, x="travel_time", bins=30), + ], + layout=ak.Layout(type="vertical"), + ) + + Another good workflow: use OSMnx to compute metrics, then use Autark plots/tables. + + import osmnx as ox + import autark as ak + + G = ox.graph_from_place("Cambridge, Massachusetts, USA", network_type="drive") + stats = ox.basic_stats(G) + + # Convert scalar stats to a table-like inline GeoJSON or simple DataFrame workflow + nodes, edges = ox.graph_to_gdfs(G) + edges = edges.reset_index() + edges["speed_kph"] = edges["speed_kph"].fillna(0) + + edges_source = ak.GeoJSON.from_geopandas( + "edges", + edges, + layer_type="polylines", + ) + + spec = ak.Spec( + data=[edges_source], + views=[ + ak.Map( + layers=[ + ak.Layer(edges_source, type="polylines").encode( + color=ak.field("speed_kph") + ) + ] + ), + ak.Table(edges_source, columns=["highway", "length", "speed_kph", "travel_time"]), + ], + ) + + Division Of Labor + A clean model is: + + OSMnx: + acquire OSM data + build/simplify street network + compute graph metrics + compute routes + enrich GeoDataFrames + + Autark: + render maps interactively + show linked plots/tables + brush/highlight selections + export HTML/widgets + run browser-side spatial joins/heatmaps/compute + + Where Autark Should Not Try To Compete + Autark should probably not reimplement OSMnx-style network science soon: + + - shortest paths + - graph simplification + - street orientation entropy + - circuity/connectivity metrics + - nearest nodes/edges + - routing + - network centrality workflows + + Those are mature in OSMnx and NetworkX. + + Potential Integration Features + The most useful additions to our Python API would be small adapters, not a big dependency: + + 1. GeoJSON.from_geopandas() already covers most OSMnx output. + 2. Add docs/examples: “OSMnx to Autark.” + 3. Optional helper later: + + ak.GeoJSON.from_osmnx_graph("streets", G, layer="edges") + ak.GeoJSON.from_osmnx_graph("intersections", G, layer="nodes") + + 4. Maybe convenience function returning both: + + edges, nodes = ak.sources_from_osmnx_graph(G, name="walk_network") + + But I would keep OSMnx as an optional integration. Do not make it a core dependency unless we commit to network- + analysis workflows as first-class. + + Recommendation + Use OSMnx as the Python-side analytical engine for street networks, and Autark as the interactive visualization/export + layer. The first concrete thing to add should be an example notebook/script in python/examples/, not a new + abstraction. \ No newline at end of file diff --git a/package.json b/package.json index 548fe4dd..b0b0dbfa 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "test": "playwright test", - "test:ui": "playwright test --ui" + "test:ui": "playwright test --ui", + "validate:specs": "ajv validate -s schema/autark-spec-v0.1.json -d \"examples/specs/*.json\" --strict=false", + "test:runtime": "playwright test tests/runtime --workers=1" }, "workspaces": [ "autk", @@ -14,6 +16,7 @@ "autk-map", "autk-compute", "autk-plot", + "autk-runtime", "gallery", "usecases" ], @@ -23,6 +26,8 @@ "@types/node": "^25.6.0", "@typescript-eslint/eslint-plugin": "^8.58.1", "@typescript-eslint/parser": "^8.58.1", + "ajv": "^8.20.0", + "ajv-cli": "^5.0.0", "eslint": "^10.1.0", "globals": "^17.4.0" } diff --git a/python/QUICKSTART.md b/python/QUICKSTART.md new file mode 100644 index 00000000..2bf4fe2c --- /dev/null +++ b/python/QUICKSTART.md @@ -0,0 +1,169 @@ +# Autark Python API - Quick Start + +Declarative Python API for creating urban visual analytics specifications. + +## Installation + +**Note:** The Python package is not yet published to PyPI. For now, install from source: + +```bash +# Clone the repository +git clone https://github.com/urban-toolkit/autark.git +cd autark/python + +# Install in development mode +pip install -e . + +# Optional: Install with validation support +pip install -e ".[validation]" + +# Optional: zero-server Jupyter widget (anywidget) +pip install -e ".[widget]" +``` + +## Quick Start + +```python +import autark as ak + +# Load GeoJSON data +neighborhoods = ak.GeoJSON( + "neighborhoods", + url="/path/to/neighborhoods.geojson", + coordinate_format="EPSG:4326", + layer_type="polygons" +) + +# Create a map +spec = ak.Spec( + metadata=ak.Metadata(title="My First Map"), + workspace=ak.Workspace(coordinate_format="EPSG:4326"), + data=[neighborhoods], + views=[ + ak.Map( + camera=ak.Camera(zoom=12), + layers=[ + ak.Layer(neighborhoods, type="polygons") + .style(color="#3498db", opacity=0.7) + ] + ) + ] +) + +# Export to JSON +spec.save_json("my_map.json") + +# Export to HTML +spec.save_html("my_map.html") + +# Display in Jupyter +spec # Automatically calls _repr_html_() +``` + +## Core Concepts + +### 1. Data Sources + +```python +# GeoJSON +geojson = ak.GeoJSON("name", url="/path/to/file.geojson", coordinate_format="EPSG:4326", layer_type="polygons") + +# CSV with lat/lng +csv_points = ak.CSV("name", url="/path/to/file.csv", geometry=ak.latlng("lat", "lon")) + +# OpenStreetMap +osm = ak.OSM("name", area="Manhattan, New York", layers=["buildings", "roads"]) +``` + +### 2. Views + +```python +# Map +map_view = ak.Map( + camera=ak.Camera(zoom=12), + layers=[ak.Layer(data, type="polygons").style(color="#3498db")] +) + +# Histogram +hist = ak.Histogram(source=data, x="attribute", bins=30) +``` + +### 3. Transforms + +```python +# Spatial join +joined = ak.SpatialJoin( + root=polygons, + join=points, + group_by=[ak.count(), ak.total("value")] +) +``` + +### 4. Interactions + +```python +# Selection + Link +brush = ak.interval("my_brush") +hist = ak.Histogram(source=data, x="field", selection=brush) +link = ak.Link(brush, target="layer_id", action="highlight") +``` + +### 5. Complete Spec + +```python +spec = ak.Spec( + metadata=ak.Metadata(title="My Visualization"), + workspace=ak.Workspace(coordinate_format="EPSG:4326"), + data=[data1, data2], + transforms=[transform], + views=[map_view, histogram], + links=[link], + layout=ak.Layout(type="vertical") +) +``` + +## Examples + +See [examples/](examples/) for complete working examples: +- [simple_geojson_map.py](examples/simple_geojson_map.py) +- [csv_points_map.py](examples/csv_points_map.py) +- [spatial_join.py](examples/spatial_join.py) + +## Jupyter Integration + +### Option 1: Widget (no server needed) + +```python +spec = ak.Spec(...) +spec.widget() # Bundled runtime; works in Jupyter, JupyterLab, VS Code, Colab +``` + +Requires `pip install -e ".[widget]"`. DuckDB WebAssembly assets load from the +jsDelivr CDN on first use. Data sources must use absolute URLs or inline +`values` (there is no local server to resolve relative paths against). + +### Option 2: Dev-server display + +```python +spec # Calls _repr_html_() automatically; loads runtime from localhost:8000 +``` + +**Setup required:** +```bash +# Terminal 1: Start CORS-enabled server +cd /path/to/autark +python cors_server.py 8000 + +# Terminal 2: Jupyter +jupyter notebook +``` + +Useful during development: serves local data files and freshly built runtimes. + +See [../PYTHON_API.md](../PYTHON_API.md) for Jupyter widget and development-server details. + +## Next Steps + +- Try the examples in [examples/](examples/) +- Read the consolidated API guide in [PYTHON_API.md](../PYTHON_API.md) +- Check [PYTHON_API_IMPLEMENTATION.md](../PYTHON_API_IMPLEMENTATION.md) for current implementation status diff --git a/python/README.md b/python/README.md new file mode 100644 index 00000000..9b8c5029 --- /dev/null +++ b/python/README.md @@ -0,0 +1,158 @@ +# Autark Python API + +Declarative Python API for creating urban visual analytics specifications. + +## Quick Example + +```python +import autark as ak + +# Load GeoJSON data +neighborhoods = ak.GeoJSON( + "neighborhoods", + url="/data/neighborhoods.geojson", + coordinate_format="EPSG:4326", + layer_type="polygons" +) + +# Create a map with styled layers +spec = ak.Spec( + metadata=ak.Metadata(title="Neighborhood Map"), + workspace=ak.Workspace(coordinate_format="EPSG:4326"), + data=[neighborhoods], + views=[ + ak.Map( + camera=ak.Camera(zoom=12), + layers=[ + ak.Layer(neighborhoods, type="polygons") + .style(color="#3498db", opacity=0.7) + ] + ) + ] +) + +# Export +spec.save_json("spec.json") +spec.save_html("map.html") + +# Validate +spec.validate() + +# Display in Jupyter without any local server (requires autark[widget]) +spec.widget() +``` + +## Features + +- **Declarative specs** - Python objects to JSON to TypeScript runtime +- **Data sources** - GeoJSON, CSV, OpenStreetMap, plus Python builders for JSON and GeoTIFF +- **Spatial operations** - Spatial joins and heatmaps +- **Compute transforms** - GPGPU and render-compute spec builders +- **Interactive visualizations** - Maps, histograms, scatterplots, tables, linked selections +- **Jupyter integration** - HTML display and bundled anywidget rendering +- **Selections back in Python** - observe `widget.selections` for brushes +- **pandas/GeoPandas input** - `GeoJSON.from_dataframe()` / `from_geopandas()` +- **Standalone HTML export** - No Python runtime required + +## Installation + +**Note:** Not yet published to PyPI. Install from source: + +```bash +cd autark/python +pip install -e . +pip install -e ".[validation]" # Optional: JSON Schema validation +pip install -e ".[widget]" # Optional: zero-server Jupyter widget (anywidget) +``` + +Package builds use Hatch and `hatch-jupyter-builder` to run +`npm run build:widget` in `autk-runtime` and include +`autark/static/autark-widget.js` automatically. + +### Displaying specs in Jupyter + +Two options: + +1. **Widget (recommended)** - `spec.widget()` renders with the runtime bundled + inside the Python package. No local server needed; works in Jupyter, + JupyterLab, VS Code, and Colab. DuckDB WebAssembly assets are fetched from + the jsDelivr CDN on first use, so network access is required. Data sources + must use absolute URLs or inline `values`. + + Plot selections sync back to the kernel: + + ```python + w = spec.widget() + w # display; brush the histogram, then: + w.selections # {'latitude_brush': [0, 2]} + w.observe(lambda change: print(change["new"]), names="selections") + ``` + + DataFrames can be passed directly as inline data: + + ```python + trees = ak.GeoJSON.from_dataframe("trees", df, latitude="lat", longitude="lon") + neighborhoods = ak.GeoJSON.from_geopandas("neighborhoods", gdf) # reprojects to EPSG:4326 + ``` + +2. **Dev-server display** - evaluating `spec` in a cell uses `_repr_html_()`, + which loads the runtime from `http://localhost:8000`. Start it from the + repo root with `python cors_server.py 8000`. Useful during development + since it picks up freshly built runtimes and serves local data files. + +## Documentation + +- **[QUICKSTART.md](QUICKSTART.md)** - Quick start guide +- **[../PYTHON_API.md](../PYTHON_API.md)** - Full API guide and current status +- **[examples/](examples/)** - Working examples + - [simple_geojson_map.py](examples/simple_geojson_map.py) - Basic map + - [csv_points_map.py](examples/csv_points_map.py) - CSV points + histogram + - [spatial_join.py](examples/spatial_join.py) - Spatial join workflow + - [geopandas_workflow.py](examples/geopandas_workflow.py) - GeoPandas workflow +- **[../PYTHON_API_IMPLEMENTATION.md](../PYTHON_API_IMPLEMENTATION.md)** - Implementation tracker + +## Design + +The Autark Python API follows the Vega-Lite/Altair pattern: + +1. Python authors **structured specifications** +2. Specifications **serialize to JSON** +3. TypeScript runtime **executes specs in browser** +4. All rendering/compute happens in **WebGPU/DuckDB-WASM** + +Benefits: +- Shareable as JSON or HTML +- Jupyter-friendly +- LLM/agent-friendly +- No Python runtime needed for visualization + +## Current Status + +Core features implemented: +- Data: GeoJSON, CSV, OSM +- Python builders: JSON, GeoTIFF +- Views: Map, Histogram, Scatterplot, Table +- Transforms: Spatial join, Heatmap, GPGPU compute, Render compute +- Interactions: Highlight linking +- Export: JSON, HTML, Jupyter widget + +See [PYTHON_API.md](../PYTHON_API.md) for usage and current limitations. + +## Testing + +```bash +# Run Python tests +python -m unittest discover -s tests + +# Type check (pip install -e ".[dev]") +python -m mypy + +# Validate examples +python examples/simple_geojson_map.py --validate +python examples/csv_points_map.py --validate +python examples/spatial_join.py --validate +``` + +## License + +MIT License diff --git a/python/autark/__init__.py b/python/autark/__init__.py new file mode 100644 index 00000000..2236bb86 --- /dev/null +++ b/python/autark/__init__.py @@ -0,0 +1,79 @@ +"""Python builders for Autark declarative specifications.""" + +from .data import CSV, OSM, GeoJSON, GeoTIFF, JSON, latlng, wkt +from .display import to_embedded_html, to_html +from .encodings import Encoding, Field, Scale, Value, field, value +from .widget import create_widget +from .links import Link +from .selections import Selection, interval, multi, point +from .spec import AutarkSpec, Metadata, Spec, Workspace +from .transforms import ( + Aggregation, + Compute, + Heatmap, + HeatmapGrid, + Near, + SpatialJoin, + avg, + collect, + count, + maximum, + max, + minimum, + min, + sum, + total, + weighted, +) +from .views import Camera, Histogram, Layer, Layout, Map, Scatterplot, Table + +__all__ = [ + "Aggregation", + "AutarkSpec", + "CSV", + "Camera", + "Compute", + "Encoding", + "Field", + "GeoJSON", + "GeoTIFF", + "Heatmap", + "HeatmapGrid", + "Histogram", + "JSON", + "Layer", + "Layout", + "Link", + "Map", + "Metadata", + "Near", + "OSM", + "Scale", + "Scatterplot", + "Selection", + "Spec", + "SpatialJoin", + "Table", + "Value", + "Workspace", + "avg", + "collect", + "count", + "create_widget", + "field", + "interval", + "latlng", + "maximum", + "max", + "minimum", + "min", + "multi", + "point", + "sum", + "to_embedded_html", + "to_html", + "total", + "value", + "weighted", + "wkt", +] diff --git a/python/autark/_serialise.py b/python/autark/_serialise.py new file mode 100644 index 00000000..a4cdec5e --- /dev/null +++ b/python/autark/_serialise.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from dataclasses import fields, is_dataclass +from pathlib import Path +from typing import Any, Mapping + + +class Serializable: + """Mixin for objects that serialize into an Autark JSON spec fragment.""" + + def to_dict(self) -> dict[str, Any]: + plain = to_plain(self) + assert isinstance(plain, dict) + return plain + + +def to_plain(value: Any) -> Any: + if isinstance(value, Serializable) and type(value).to_dict is not Serializable.to_dict: + return value.to_dict() + if is_dataclass(value) and not isinstance(value, type): + out: dict[str, Any] = {} + for item in fields(value): + key = item.metadata.get("json", item.name) + raw = getattr(value, item.name) + if raw is None: + continue + plain = to_plain(raw) + if plain == {} or plain == []: + continue + out[key] = plain + return out + if isinstance(value, Serializable): + return value.to_dict() + if isinstance(value, Mapping): + mapped: dict[str, Any] = {} + for key, item in value.items(): + if item is None: + continue + plain = to_plain(item) + if plain == {} or plain == []: + continue + mapped[str(key)] = plain + return mapped + if isinstance(value, (list, tuple)): + return [to_plain(item) for item in value if item is not None] + if isinstance(value, Path): + return str(value) + return value + + +def ref_name(value: Any) -> str: + if isinstance(value, str): + return value + name = getattr(value, "name", None) + if isinstance(name, str): + return name + raise TypeError(f"Expected a string name or object with a name, got {type(value).__name__}") + + +def exactly_one(label: str, first: Any, second: Any, first_name: str, second_name: str) -> None: + has_first = first is not None + has_second = second is not None + if has_first == has_second: + raise ValueError(f"{label} requires exactly one of {first_name} or {second_name}") diff --git a/python/autark/data.py b/python/autark/data.py new file mode 100644 index 00000000..f07eee29 --- /dev/null +++ b/python/autark/data.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +import math +from dataclasses import dataclass, field as dataclass_field +from typing import Any, Literal, Mapping, Sequence + +from ._serialise import Serializable, exactly_one + +OsmLayer = Literal["buildings", "roads", "parks", "water", "surface"] + +LayerType = Literal["polygons", "polylines", "points", "buildings"] + +_GEOMETRY_LAYER_TYPES: dict[str, LayerType] = { + "Polygon": "polygons", + "MultiPolygon": "polygons", + "LineString": "polylines", + "MultiLineString": "polylines", + "Point": "points", + "MultiPoint": "points", +} + + +def _jsonable(value: Any) -> Any: + """Convert pandas/numpy-laden structures into plain JSON-safe values. + + numpy scalars are unwrapped, non-finite floats become null (JSON.parse in + browsers rejects NaN/Infinity), tuples become lists, and unknown objects + fall back to their string representation. + """ + if value is None or isinstance(value, (str, bool, int)): + return value + if isinstance(value, float): + return value if math.isfinite(value) else None + if isinstance(value, Mapping): + return {str(key): _jsonable(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [_jsonable(item) for item in value] + item_getter = getattr(value, "item", None) + if callable(item_getter): + # numpy scalar (e.g. int64/float64/bool_) + return _jsonable(item_getter()) + return str(value) + + +def _infer_layer_type(features: Sequence[Mapping[str, Any]]) -> LayerType: + geometry_types = { + str((feature.get("geometry") or {}).get("type")) for feature in features + } + layer_types = {_GEOMETRY_LAYER_TYPES.get(g) for g in geometry_types} + layer_types.discard(None) + if len(layer_types) != 1: + raise ValueError( + f"Cannot infer layer_type from geometry types {sorted(geometry_types)}; " + "pass layer_type explicitly" + ) + (layer_type,) = layer_types + assert layer_type is not None + return layer_type + + +@dataclass(frozen=True) +class LatLngGeometry(Serializable): + latitude: str + longitude: str + coordinate_format: str | None = dataclass_field(default=None, metadata={"json": "coordinateFormat"}) + type: Literal["latlng"] = "latlng" + + +@dataclass(frozen=True) +class WktGeometry(Serializable): + column: str + coordinate_format: str | None = dataclass_field(default=None, metadata={"json": "coordinateFormat"}) + type: Literal["wkt"] = "wkt" + + +def latlng(latitude: str, longitude: str, coordinate_format: str | None = None) -> LatLngGeometry: + return LatLngGeometry(latitude=latitude, longitude=longitude, coordinate_format=coordinate_format) + + +def wkt(column: str, coordinate_format: str | None = None) -> WktGeometry: + return WktGeometry(column=column, coordinate_format=coordinate_format) + + +@dataclass(frozen=True) +class OSM(Serializable): + name: str + area: str + layers: list[OsmLayer] + geocode_area: str | None = dataclass_field(default=None, metadata={"json": "geocodeArea"}) + areas: list[str] | None = None + source: Literal["overpass", "pbf"] | None = None + pbf_file_url: str | None = dataclass_field(default=None, metadata={"json": "pbfFileUrl"}) + coordinate_format: str | None = dataclass_field(default=None, metadata={"json": "coordinateFormat"}) + type: Literal["osm"] = "osm" + + def __post_init__(self) -> None: + if not self.layers: + raise ValueError("OSM requires at least one layer") + if self.areas is not None and not self.areas: + raise ValueError("OSM areas must not be empty") + if self.source == "pbf" and not self.pbf_file_url: + raise ValueError("OSM with source='pbf' requires pbf_file_url") + + def layer_source(self, layer: OsmLayer) -> str: + return f"{self.name}_{layer}" + + +@dataclass(frozen=True) +class GeoJSON(Serializable): + name: str + url: str | None = None + values: dict[str, Any] | None = None + layer_type: LayerType | None = dataclass_field( + default=None, + metadata={"json": "layerType"}, + ) + coordinate_format: str | None = dataclass_field(default=None, metadata={"json": "coordinateFormat"}) + type: Literal["geojson"] = "geojson" + + def __post_init__(self) -> None: + exactly_one("GeoJSON", self.url, self.values, "url", "values") + + @classmethod + def from_geopandas( + cls, + name: str, + gdf: Any, + *, + layer_type: LayerType | None = None, + coordinate_format: str | None = None, + ) -> "GeoJSON": + """Create an inline GeoJSON source from a GeoDataFrame. + + The GeoDataFrame is reprojected to EPSG:4326 when its CRS is known and + differs, and serialized through ``__geo_interface__``, so this works + with geopandas without autark depending on it. + + @param name: Data source name referenced by views/transforms. + @param gdf: A geopandas GeoDataFrame (or anything exposing + ``__geo_interface__``; ``crs``/``to_crs`` are used when present). + @param layer_type: Layer type; inferred from the geometries if omitted. + @param coordinate_format: Coordinate format; defaults to EPSG:4326 when + the CRS is known (after reprojection). + @returns A GeoJSON data source with inline values. + @example + gdf = geopandas.read_file("neighborhoods.shp") + source = ak.GeoJSON.from_geopandas("neighborhoods", gdf) + """ + crs = getattr(gdf, "crs", None) + if crs is not None: + epsg = crs.to_epsg() if hasattr(crs, "to_epsg") else None + if epsg != 4326 and callable(getattr(gdf, "to_crs", None)): + gdf = gdf.to_crs(epsg=4326) + if coordinate_format is None: + coordinate_format = "EPSG:4326" + + geo = getattr(gdf, "__geo_interface__", None) + if not isinstance(geo, Mapping): + raise TypeError( + f"from_geopandas expects an object with __geo_interface__, got {type(gdf).__name__}" + ) + collection = _jsonable(geo) + features = collection.get("features", []) + if layer_type is None: + layer_type = _infer_layer_type(features) + return cls( + name, + values={"type": "FeatureCollection", "features": features}, + layer_type=layer_type, + coordinate_format=coordinate_format, + ) + + @classmethod + def from_dataframe( + cls, + name: str, + df: Any, + *, + latitude: str, + longitude: str, + properties: Sequence[str] | None = None, + coordinate_format: str = "EPSG:4326", + ) -> "GeoJSON": + """Create an inline points source from a DataFrame with lat/lng columns. + + Rows are converted into GeoJSON Point features (the runtime supports + inline GeoJSON, unlike inline CSV). Works with pandas without autark + depending on it. + + @param name: Data source name referenced by views/transforms. + @param df: A pandas DataFrame (or anything with ``to_dict("records")``). + @param latitude: Column holding latitudes. + @param longitude: Column holding longitudes. + @param properties: Columns to keep as feature properties; defaults to + every column except the coordinate columns. + @param coordinate_format: Coordinate format of the lat/lng values. + @returns A GeoJSON data source with inline point features. + @example + df = pandas.read_csv("trees.csv") + source = ak.GeoJSON.from_dataframe("trees", df, latitude="lat", longitude="lon") + """ + to_dict = getattr(df, "to_dict", None) + if not callable(to_dict): + raise TypeError( + f"from_dataframe expects an object with to_dict('records'), got {type(df).__name__}" + ) + records = to_dict("records") + + features = [] + for row in records: + if latitude not in row or longitude not in row: + raise KeyError( + f"Row is missing coordinate column {latitude!r} or {longitude!r}" + ) + keys = properties if properties is not None else [k for k in row if k not in (latitude, longitude)] + features.append( + { + "type": "Feature", + "properties": {str(key): _jsonable(row[key]) for key in keys}, + "geometry": { + "type": "Point", + "coordinates": [_jsonable(row[longitude]), _jsonable(row[latitude])], + }, + } + ) + return cls( + name, + values={"type": "FeatureCollection", "features": features}, + layer_type="points", + coordinate_format=coordinate_format, + ) + + +@dataclass(frozen=True) +class CSV(Serializable): + name: str + url: str | None = None + values: list[list[Any]] | None = None + delimiter: str | None = None + geometry: LatLngGeometry | WktGeometry | None = None + type: Literal["csv"] = "csv" + + def __post_init__(self) -> None: + exactly_one("CSV", self.url, self.values, "url", "values") + + +@dataclass(frozen=True) +class JSON(Serializable): + name: str + url: str | None = None + values: list[Any] | dict[str, Any] | None = None + flatten: bool | None = None + geometry: LatLngGeometry | WktGeometry | None = None + type: Literal["json"] = "json" + + def __post_init__(self) -> None: + exactly_one("JSON", self.url, self.values, "url", "values") + + +@dataclass(frozen=True) +class GeoTIFF(Serializable): + name: str + url: str + band: int | None = None # 1-based index, default to first band + coordinate_format: str | None = dataclass_field(default=None, metadata={"json": "coordinateFormat"}) + max_pixels: int | None = dataclass_field(default=None, metadata={"json": "maxPixels"}) + type: Literal["geotiff"] = "geotiff" diff --git a/python/autark/display.py b/python/autark/display.py new file mode 100644 index 00000000..640dd262 --- /dev/null +++ b/python/autark/display.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import html +import json +import uuid +from typing import Any +from urllib.parse import urlparse + +DEFAULT_RUNTIME_URL = "http://localhost:8000/autk-runtime/dist/autk-runtime.js" + + +def _script_safe_json(value: Any, **kwargs: Any) -> str: + """Serialize to JSON that is safe to inline inside a + + +""" + + +def to_html( + spec: Any, + *, + runtime_url: str = DEFAULT_RUNTIME_URL, + height: str = "640px", +) -> str: + spec_dict = spec.to_dict() if hasattr(spec, "to_dict") else spec + spec_dict = _resolve_data_urls(spec_dict, runtime_url) + container_id = f"autark-{uuid.uuid4().hex}" + json_spec = json.dumps(spec_dict) + escaped_spec = html.escape(json.dumps(spec_dict, indent=2)) + return f"""
+ +
+ Autark spec +
{escaped_spec}
+
""" diff --git a/python/autark/encodings.py b/python/autark/encodings.py new file mode 100644 index 00000000..8c9d0979 --- /dev/null +++ b/python/autark/encodings.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from dataclasses import dataclass, field as dataclass_field +from typing import Any, Literal + +from ._serialise import Serializable + + +@dataclass(frozen=True) +class Scale(Serializable): + type: Literal["linear", "quantile", "categorical"] | None = None + scheme: str | None = None + domain: list[Any] | None = None + domain_strategy: Literal["minMax", "percentile", "user"] | None = dataclass_field( + default=None, + metadata={"json": "domainStrategy"}, + ) + + +@dataclass(frozen=True) +class Field(Serializable): + field: str + scale: Scale | None = None + + +@dataclass(frozen=True) +class Value(Serializable): + value: str | int | float | bool + + +@dataclass(frozen=True) +class Encoding(Serializable): + color: Field | Value | None = None + opacity: Field | Value | None = None + size: Field | Value | None = None + height: Field | Value | None = None + + +def field(name: str, scale: Scale | None = None) -> Field: + return Field(field=name, scale=scale) + + +def value(raw: str | int | float | bool) -> Value: + return Value(value=raw) diff --git a/python/autark/links.py b/python/autark/links.py new file mode 100644 index 00000000..2e65f033 --- /dev/null +++ b/python/autark/links.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +from ._serialise import Serializable, ref_name + + +@dataclass(frozen=True) +class Link(Serializable): + selection: object + target: object + action: Literal["filter", "highlight", "skip", "color"] | None = "highlight" + + def to_dict(self) -> dict[str, object]: + out: dict[str, object] = { + "selection": ref_name(self.selection), + "target": ref_name(self.target), + } + if self.action is not None: + out["action"] = self.action + return out diff --git a/python/autark/selections.py b/python/autark/selections.py new file mode 100644 index 00000000..60836bb4 --- /dev/null +++ b/python/autark/selections.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +from ._serialise import Serializable + + +@dataclass(frozen=True) +class Selection(Serializable): + name: str + type: Literal["point", "interval", "multi"] + fields: list[str] | None = None + + +def point(name: str, fields: list[str] | None = None) -> Selection: + return Selection(name=name, type="point", fields=fields) + + +def interval(name: str, fields: list[str] | None = None) -> Selection: + return Selection(name=name, type="interval", fields=fields) + + +def multi(name: str, fields: list[str] | None = None) -> Selection: + return Selection(name=name, type="multi", fields=fields) diff --git a/python/autark/spec.py b/python/autark/spec.py new file mode 100644 index 00000000..4f589f3e --- /dev/null +++ b/python/autark/spec.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import json +from dataclasses import dataclass, field as dataclass_field +from pathlib import Path +from typing import Any + +from ._serialise import Serializable, to_plain +from .display import DEFAULT_RUNTIME_URL, to_embedded_html, to_html + +SCHEMA_URL = "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json" +VERSION = "0.1" + + +@dataclass(frozen=True) +class Metadata(Serializable): + title: str | None = None + description: str | None = None + authors: list[str] | None = None + created: str | None = None + + +@dataclass(frozen=True) +class Workspace(Serializable): + name: str | None = None + coordinate_format: str | None = dataclass_field(default=None, metadata={"json": "coordinateFormat"}) + + +@dataclass(frozen=True) +class AutarkSpec(Serializable): + views: list[Serializable] + data: list[Serializable] | None = None + transforms: list[Serializable] | None = None + links: list[Serializable] | None = None + layout: Serializable | None = None + metadata: Metadata | dict[str, Any] | None = None + workspace: Workspace | dict[str, Any] | None = None + + def __post_init__(self) -> None: + if not self.views: + raise ValueError("AutarkSpec requires at least one view") + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = { + "$schema": SCHEMA_URL, + "version": VERSION, + } + optional = { + "metadata": self.metadata, + "workspace": self.workspace, + "data": self.data, + "transforms": self.transforms, + "views": self.views, + "links": self.links, + "layout": self.layout, + } + for key, value in optional.items(): + plain = to_plain(value) + if plain is not None and plain != [] and plain != {}: + out[key] = plain + return out + + def to_json(self, *, indent: int | None = 2) -> str: + return json.dumps(self.to_dict(), indent=indent) + + def save_json(self, path: str | Path, *, indent: int | None = 2) -> None: + Path(path).write_text(self.to_json(indent=indent) + "\n", encoding="utf-8") + + def validate(self, schema_path: str | Path | None = None) -> None: + try: + import jsonschema + except ModuleNotFoundError as exc: + raise RuntimeError( + "Install autark[validation] or jsonschema to validate Autark specs" + ) from exc + + path = Path(schema_path) if schema_path is not None else _default_schema_path() + if not path.exists(): + raise FileNotFoundError(f"Autark schema not found: {path}") + schema = json.loads(path.read_text(encoding="utf-8")) + jsonschema.Draft7Validator.check_schema(schema) + jsonschema.validate(self.to_dict(), schema) + + def to_html(self, *, runtime_url: str = DEFAULT_RUNTIME_URL, height: str = "640px") -> str: + return to_html(self, runtime_url=runtime_url, height=height) + + def save_html( + self, + path: str | Path, + *, + runtime_url: str | None = None, + height: str = "640px", + title: str | None = None, + ) -> None: + """Save the spec as an HTML file. + + By default the file is fully self-contained: the runtime bundle is + embedded inline, so it opens anywhere (file://, web host) without a + local server. DuckDB assets load from the jsDelivr CDN, and data + sources must use absolute URLs or inline values. + + Pass ``runtime_url`` to instead produce a lightweight file that loads + the runtime from a dev server (useful during development). + """ + if runtime_url is not None: + content = self.to_html(runtime_url=runtime_url, height=height) + else: + content = to_embedded_html(self, height=height, title=title) + Path(path).write_text(content, encoding="utf-8") + + def widget(self, *, height: str = "640px") -> Any: + """Render the spec as a Jupyter widget using the bundled runtime. + + Requires the ``widget`` extra (``pip install autark[widget]``). Works + without a local server; DuckDB assets load from the jsDelivr CDN. + Data sources must use absolute URLs or inline values. + """ + from .widget import create_widget + + return create_widget(self, height=height) + + def _repr_html_(self) -> str: + return self.to_html() + + +def _default_schema_path() -> Path: + return Path(__file__).resolve().parents[2] / "schema" / "autark-spec-v0.1.json" + + +Spec = AutarkSpec diff --git a/python/autark/transforms.py b/python/autark/transforms.py new file mode 100644 index 00000000..66711403 --- /dev/null +++ b/python/autark/transforms.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +from dataclasses import dataclass, field as dataclass_field +from typing import Any, Literal, Mapping, Sequence + +from ._serialise import Serializable, ref_name, to_plain + +AggregationOp = Literal["count", "sum", "avg", "min", "max", "weighted", "collect"] +HeatmapAggregationOp = Literal["count", "sum", "avg", "min", "max", "weighted"] +LayerType = Literal["polygons", "polylines", "points", "buildings"] +RenderLayerType = Literal["buildings", "roads", "polygons", "polylines", "points"] + + +@dataclass(frozen=True) +class Near(Serializable): + distance: float + use_centroid: bool | None = dataclass_field(default=None, metadata={"json": "useCentroid"}) + + def __post_init__(self) -> None: + if self.distance <= 0: + raise ValueError("Near distance must be greater than zero") + + +@dataclass(frozen=True) +class Aggregation(Serializable): + column: str + op: AggregationOp + as_: str | None = dataclass_field(default=None, metadata={"json": "as"}) + normalize: bool | None = None + + +def count(column: str = "*", as_: str | None = None, normalize: bool | None = None) -> Aggregation: + return Aggregation(column=column, op="count", as_=as_, normalize=normalize) + + +def total(column: str, as_: str | None = None, normalize: bool | None = None) -> Aggregation: + return Aggregation(column=column, op="sum", as_=as_, normalize=normalize) + + +def sum(column: str, as_: str | None = None, normalize: bool | None = None) -> Aggregation: + return total(column=column, as_=as_, normalize=normalize) + + +def avg(column: str, as_: str | None = None, normalize: bool | None = None) -> Aggregation: + return Aggregation(column=column, op="avg", as_=as_, normalize=normalize) + + +def minimum(column: str, as_: str | None = None, normalize: bool | None = None) -> Aggregation: + return Aggregation(column=column, op="min", as_=as_, normalize=normalize) + + +def min(column: str, as_: str | None = None, normalize: bool | None = None) -> Aggregation: + return minimum(column=column, as_=as_, normalize=normalize) + + +def maximum(column: str, as_: str | None = None, normalize: bool | None = None) -> Aggregation: + return Aggregation(column=column, op="max", as_=as_, normalize=normalize) + + +def max(column: str, as_: str | None = None, normalize: bool | None = None) -> Aggregation: + return maximum(column=column, as_=as_, normalize=normalize) + + +def weighted(column: str, as_: str | None = None, normalize: bool | None = None) -> Aggregation: + return Aggregation(column=column, op="weighted", as_=as_, normalize=normalize) + + +def collect(column: str, as_: str | None = None, normalize: bool | None = None) -> Aggregation: + return Aggregation(column=column, op="collect", as_=as_, normalize=normalize) + + +@dataclass(frozen=True) +class SpatialJoin(Serializable): + root: object + join: object + near: Near | None = None + group_by: list[Aggregation] | None = dataclass_field(default=None, metadata={"json": "groupBy"}) + type: Literal["spatialJoin"] = "spatialJoin" + + def to_dict(self) -> dict[str, object]: + out: dict[str, object] = { + "type": self.type, + "root": ref_name(self.root), + "join": ref_name(self.join), + } + if self.near is not None: + out["near"] = self.near.to_dict() + if self.group_by: + out["groupBy"] = [item.to_dict() for item in self.group_by] + return out + + +@dataclass(frozen=True) +class HeatmapGrid(Serializable): + rows: int + columns: int + + def __post_init__(self) -> None: + if self.rows < 1 or self.columns < 1: + raise ValueError("Heatmap grid rows and columns must be greater than zero") + + +@dataclass(frozen=True) +class Heatmap(Serializable): + source: object + output: str + near: Near + grid: HeatmapGrid | Mapping[str, int] + group_by: list[Aggregation] | None = dataclass_field(default=None, metadata={"json": "groupBy"}) + type: Literal["heatmap"] = "heatmap" + + def __post_init__(self) -> None: + if self.group_by: + unsupported = [item.op for item in self.group_by if item.op == "collect"] + if unsupported: + raise ValueError("Heatmap group_by does not support collect aggregation") + + def to_dict(self) -> dict[str, object]: + grid = to_plain(self.grid) + if not isinstance(grid, dict): + raise TypeError("Heatmap grid must serialize to an object") + out: dict[str, object] = { + "type": self.type, + "source": ref_name(self.source), + "output": self.output, + "near": self.near.to_dict(), + "grid": grid, + } + if self.group_by: + out["groupBy"] = [item.to_dict() for item in self.group_by] + return out + + +@dataclass(frozen=True) +class Compute(Serializable): + spec: Mapping[str, Any] + + @staticmethod + def gpgpu( + source: object, + *, + output: str, + variable_mapping: Mapping[str, str], + wgsl_body: str, + result_field: str | None = None, + output_columns: Sequence[str] | None = None, + layer_type: LayerType | None = None, + coordinate_format: str | None = None, + attribute_arrays: Mapping[str, int] | None = None, + attribute_matrices: Mapping[str, Mapping[str, int | str]] | None = None, + uniforms: Mapping[str, float] | None = None, + uniform_arrays: Mapping[str, Sequence[float]] | None = None, + uniform_matrices: Mapping[str, Mapping[str, Any]] | None = None, + ) -> "Compute": + if not variable_mapping: + raise ValueError("Compute.gpgpu requires at least one variable mapping") + if not wgsl_body.strip(): + raise ValueError("Compute.gpgpu requires a WGSL body") + if (result_field is None) == (output_columns is None): + raise ValueError("Compute.gpgpu requires exactly one of result_field or output_columns") + + spec: dict[str, Any] = { + "type": "gpgpuCompute", + "source": ref_name(source), + "output": output, + "variableMapping": dict(variable_mapping), + "wgslBody": wgsl_body, + } + optional = { + "layerType": layer_type, + "coordinateFormat": coordinate_format, + "attributeArrays": dict(attribute_arrays) if attribute_arrays is not None else None, + "attributeMatrices": dict(attribute_matrices) if attribute_matrices is not None else None, + "uniforms": dict(uniforms) if uniforms is not None else None, + "uniformArrays": {key: list(value) for key, value in uniform_arrays.items()} if uniform_arrays is not None else None, + "uniformMatrices": dict(uniform_matrices) if uniform_matrices is not None else None, + "resultField": result_field, + "outputColumns": list(output_columns) if output_columns is not None else None, + } + spec.update({key: value for key, value in optional.items() if value is not None}) + return Compute(spec) + + @staticmethod + def render( + *, + output: str, + layers: Sequence[Mapping[str, Any]], + viewpoints: Mapping[str, Any], + aggregation: Mapping[str, Any], + layer_type: LayerType | None = None, + coordinate_format: str | None = None, + camera: Mapping[str, Any] | None = None, + tile_size: int | None = None, + ) -> "Compute": + if not layers: + raise ValueError("Compute.render requires at least one layer") + if tile_size is not None and (tile_size < 8 or tile_size % 8 != 0): + raise ValueError("Compute.render tile_size must be a multiple of 8") + + spec: dict[str, Any] = { + "type": "renderCompute", + "output": output, + "layers": [dict(layer) for layer in layers], + "viewpoints": dict(viewpoints), + "aggregation": dict(aggregation), + } + optional = { + "layerType": layer_type, + "coordinateFormat": coordinate_format, + "camera": dict(camera) if camera is not None else None, + "tileSize": tile_size, + } + spec.update({key: value for key, value in optional.items() if value is not None}) + return Compute(spec) + + def to_dict(self) -> dict[str, Any]: + plain = to_plain(self.spec) + assert isinstance(plain, dict) + return plain diff --git a/python/autark/views.py b/python/autark/views.py new file mode 100644 index 00000000..acf0e094 --- /dev/null +++ b/python/autark/views.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +from dataclasses import dataclass, field as dataclass_field, replace +from typing import Any, Literal + +from ._serialise import Serializable, ref_name, to_plain +from .encodings import Encoding, Field, Scale, Value, field, value +from .selections import Selection + + +@dataclass(frozen=True) +class Camera(Serializable): + pitch: float | None = None + bearing: float | None = None + zoom: float | None = None + + +@dataclass(frozen=True) +class Layer(Serializable): + source: object + id: str | None = None + type: Literal["buildings", "roads", "polygons", "polylines", "points"] | None = None + encoding: Encoding | None = None + style_values: dict[str, Any] | None = dataclass_field(default=None, metadata={"json": "style"}) + + def encode( + self, + *, + color: Field | Value | str | int | float | bool | None = None, + opacity: Field | Value | int | float | bool | None = None, + size: Field | Value | int | float | bool | None = None, + height: Field | Value | str | int | float | bool | None = None, + ) -> "Layer": + return replace( + self, + encoding=Encoding( + color=_encoding_channel(color), + opacity=_encoding_channel(opacity), + size=_encoding_channel(size), + height=_encoding_channel(height), + ), + ) + + def style(self, **values: Any) -> "Layer": + merged = dict(self.style_values or {}) + merged.update(values) + return replace(self, style_values=merged) + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = {"source": ref_name(self.source)} + if self.id is not None: + out["id"] = self.id + if self.type is not None: + out["type"] = self.type + if self.encoding is not None: + out["encoding"] = self.encoding.to_dict() + if self.style_values: + out["style"] = to_plain(self.style_values) + return out + + +def _encoding_channel(value_: Field | Value | str | int | float | bool | None) -> Field | Value | None: + if value_ is None: + return None + if isinstance(value_, (Field, Value)): + return value_ + if isinstance(value_, str): + return field(value_) + return value(value_) + + +@dataclass(frozen=True) +class Map(Serializable): + layers: list[Layer] + name: str | None = None + camera: Camera | None = None + style: dict[str, Any] | None = None + type: Literal["map"] = "map" + + def __post_init__(self) -> None: + if not self.layers: + raise ValueError("Map requires at least one layer") + + +@dataclass(frozen=True) +class Histogram(Serializable): + source: object + x: Field | str + name: str | None = None + y: Field | str | None = None + bins: int | None = None + selection: Selection | None = None + type: Literal["histogram"] = "histogram" + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = { + "type": self.type, + "source": ref_name(self.source), + "x": _axis(self.x), + } + if self.name is not None: + out["name"] = self.name + if self.y is not None: + out["y"] = _axis(self.y) + if self.bins is not None: + out["bins"] = self.bins + if self.selection is not None: + out["selection"] = self.selection.to_dict() + return out + + +@dataclass(frozen=True) +class Scatterplot(Serializable): + source: object + x: Field | str + y: Field | str + name: str | None = None + color: Field | str | None = None + selection: Selection | None = None + type: Literal["scatterplot"] = "scatterplot" + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = { + "type": self.type, + "source": ref_name(self.source), + "x": _axis(self.x), + "y": _axis(self.y), + } + if self.name is not None: + out["name"] = self.name + if self.color is not None: + out["color"] = _axis(self.color) + if self.selection is not None: + out["selection"] = self.selection.to_dict() + return out + + +@dataclass(frozen=True) +class Table(Serializable): + source: object + columns: list[Field | str] + name: str | None = None + selection: Selection | None = None + sort: dict[str, Any] | None = None + type: Literal["table"] = "table" + + def __post_init__(self) -> None: + if not self.columns: + raise ValueError("Table requires at least one column") + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = { + "type": self.type, + "source": ref_name(self.source), + "columns": [_axis(column) for column in self.columns], + } + if self.name is not None: + out["name"] = self.name + if self.selection is not None: + out["selection"] = self.selection.to_dict() + if self.sort is not None: + out["sort"] = to_plain(self.sort) + return out + + +def _axis(axis: Field | str) -> dict[str, Any]: + if isinstance(axis, Field): + return axis.to_dict() + return {"field": axis} + + +@dataclass(frozen=True) +class Layout(Serializable): + type: Literal["vertical", "horizontal", "grid"] = "vertical" + columns: int | None = None + gap: int | None = None diff --git a/python/autark/widget.py b/python/autark/widget.py new file mode 100644 index 00000000..7e3c03d9 --- /dev/null +++ b/python/autark/widget.py @@ -0,0 +1,99 @@ +"""anywidget-based Jupyter display for Autark specs. + +Unlike :func:`autark.display.to_html` (which loads the runtime from a local +dev server), the widget ships a fully bundled runtime inside the Python +package, so it works in Jupyter, JupyterLab, VS Code, and Colab without any +local server. DuckDB WebAssembly assets are fetched from the jsDelivr CDN on +first use (network access required). + +Note: because there is no local data server in this mode, data sources must +use absolute URLs or inline ``values``; root-relative URLs (``/examples/...``) +cannot be resolved. +""" + +from __future__ import annotations + +import warnings +from pathlib import Path +from typing import Any + +_STATIC_DIR = Path(__file__).parent / "static" +_WIDGET_ESM = _STATIC_DIR / "autark-widget.js" + +_widget_class: type | None = None + + +def _read_widget_esm() -> str: + """Return the bundled widget runtime source, or raise with a build hint.""" + if not _WIDGET_ESM.exists(): + raise RuntimeError( + f"Bundled widget runtime not found: {_WIDGET_ESM}. " + "Build it with: cd autk-runtime && npm run build:widget" + ) + return _WIDGET_ESM.read_text(encoding="utf-8") + + +def _build_widget_class() -> type: + try: + import anywidget + import traitlets + except ModuleNotFoundError as exc: + raise RuntimeError( + "The Autark widget requires anywidget. Install it with: pip install autark[widget]" + ) from exc + + if not _WIDGET_ESM.exists(): + raise RuntimeError( + f"Bundled widget runtime not found: {_WIDGET_ESM}. " + "Build it with: cd autk-runtime && npm run build:widget" + ) + + class AutarkWidget(anywidget.AnyWidget): + """Jupyter widget that renders an Autark spec with the bundled runtime. + + Attributes: + spec: The Autark spec dict being rendered (re-renders on change). + height: CSS height of the widget container. + selections: Updated by the frontend whenever a plot selection + changes: ``{selection_name: [component_id, ...]}``. Observe it + with ``widget.observe(handler, names="selections")``. + """ + + _esm = _WIDGET_ESM + spec = traitlets.Dict({}).tag(sync=True) + height = traitlets.Unicode("640px").tag(sync=True) + selections = traitlets.Dict({}).tag(sync=True) + + return AutarkWidget + + +def _warn_on_relative_urls(spec_dict: dict[str, Any]) -> None: + data = spec_dict.get("data") + if not isinstance(data, list): + return + for entry in data: + url = entry.get("url") if isinstance(entry, dict) else None + if isinstance(url, str) and url.startswith("/") and not url.startswith("//"): + warnings.warn( + f"Data source URL {url!r} is root-relative. The widget has no local " + "server to resolve it against; use an absolute URL or inline values.", + UserWarning, + stacklevel=3, + ) + + +def create_widget(spec: Any, *, height: str = "640px") -> Any: + """Create an anywidget instance rendering the given spec. + + @param spec: An AutarkSpec (or plain dict spec). + @param height: CSS height of the widget container. + @returns An ``anywidget.AnyWidget`` subclass instance; display it in a + notebook cell or compose it with other ipywidgets. + """ + global _widget_class + if _widget_class is None: + _widget_class = _build_widget_class() + + spec_dict = spec.to_dict() if hasattr(spec, "to_dict") else spec + _warn_on_relative_urls(spec_dict) + return _widget_class(spec=spec_dict, height=height) diff --git a/python/examples/JUPYTER_TESTING.md b/python/examples/JUPYTER_TESTING.md new file mode 100644 index 00000000..09f69400 --- /dev/null +++ b/python/examples/JUPYTER_TESTING.md @@ -0,0 +1,196 @@ +# Jupyter Integration Testing Guide + +This guide walks through testing the Autark Python API in Jupyter. + +> **Tip:** if you just want to display a spec without any local server, use +> the widget instead: `pip install anywidget`, then `spec.widget()` in a cell. +> It ships a bundled runtime inside the Python package and loads DuckDB assets +> from the jsDelivr CDN (data must use absolute URLs or inline values). The +> steps below cover the dev-server display path, which serves local data files +> and freshly built runtimes. + +## Prerequisites + +1. **Jupyter installed** (already verified: `/opt/miniconda3/bin/jupyter`) +2. **Autark runtime built** - The TypeScript runtime needs to be compiled +3. **Local web server** - To serve the runtime module and data files + +## Setup Steps + +### 1. Build the TypeScript Runtime + +From the repo root: + +```bash +cd autk-runtime +npm install +npm run build +``` + +This creates `autk-runtime/dist/autk-runtime.js`. + +### 2. Start a Local Development Server + +You need to serve the runtime and data files with CORS enabled, because the +Jupyter page (e.g. port 8888) loads them cross-origin. From the repo root: + +```bash +# Start CORS-enabled server on port 8000 +python cors_server.py 8000 +``` + +(Or run `./start_jupyter_test.sh` to start the server and Jupyter together.) + +The key is that: +- `/autk-runtime/dist/autk-runtime.js` should be accessible +- `/examples/data/` should be accessible + +Notes on cross-origin handling (no action needed, just context): +- The runtime creates its DuckDB Web Worker via a same-origin `blob:` URL + (`autk-db/src/duckdb.ts`), so the browser's cross-origin Worker + restriction does not apply. +- Root-relative data URLs in specs (e.g. `/examples/data/trees.csv`) are + automatically resolved against the runtime server's origin when a spec is + displayed or exported (`python/autark/display.py`). +- A regression test for the cross-origin scenario lives at + `tests/runtime/cross-origin-worker.test.ts` (uses `test-cross-origin.html` + and spawns its own servers; runs with `npm run test:runtime`). + +### 3. Use a Custom Runtime URL (if needed) + +If your server runs on a different port, pass `runtime_url` when displaying +or exporting: + +```python +spec.save_html("output.html", runtime_url="http://localhost:8765/autk-runtime/dist/autk-runtime.js") +``` + +## Running the Test Notebook + +### Option 1: Jupyter Notebook + +```bash +cd python/examples +jupyter notebook test_jupyter_integration.ipynb +``` + +This will open the notebook in your browser. Run each cell and check: +- ✅ Cells execute without Python errors +- ✅ Visualizations render (you should see maps/plots) +- ✅ No JavaScript errors in browser console (F12) + +### Option 2: JupyterLab + +```bash +cd python/examples +jupyter lab test_jupyter_integration.ipynb +``` + +### Option 3: VS Code Notebooks + +Open `test_jupyter_integration.ipynb` in VS Code with the Jupyter extension installed. + +## Expected Behavior + +Each spec display should show: + +1. **Interactive visualization** - Rendered using the TypeScript runtime + - Maps should show data layers + - Histograms should render + - Selection/linking should work (brush on histogram highlights map) + +2. **Collapsible spec viewer** - Click "Autark spec" to see the JSON + +3. **Error handling** - If the runtime fails to load, you'll see a red error message + +## Troubleshooting + +### Issue: "Module not found" error in browser console + +**Solution:** The runtime JavaScript file isn't accessible. Check: +- Is the dev server running? +- Is the runtime built? (`autk-runtime/dist/autk-runtime.js` exists?) +- Is the `runtime_url` correct in `display.py`? + +### Issue: "Failed to fetch" errors for data files + +**Solution:** Data files aren't accessible. Check: +- Are the data files at `/examples/data/`? +- Is the dev server serving from the repo root? +- Try absolute URLs instead of relative paths + +### Issue: Visualization doesn't render, but no errors + +**Solution:** +- Check browser console for warnings +- Verify the runtime module loaded (look for network requests) +- Try opening the saved HTML file directly + +### Issue: CORS errors + +**Solution:** +- Make sure you're serving files via HTTP, not using `file://` protocol +- If using a different server setup, ensure CORS headers are set + +## Manual Testing Checklist + +- [ ] Test 1: Simple GeoJSON map renders +- [ ] Test 2: CSV points + histogram render +- [ ] Test 3: Spatial join example renders with linked selection +- [ ] Test 4: JSON output looks correct +- [ ] Test 5: HTML export works and opens in browser +- [ ] Browser console has no errors +- [ ] Selection/linking works (brush on histogram highlights map) +- [ ] Data loads correctly (no 404s) + +## Next Steps After Successful Test + +Once Jupyter integration is confirmed working: + +1. ✅ Mark "Display works in Jupyter" as complete in implementation plan +2. Create more example notebooks (simple map, CSV visualization, etc.) +3. Test in different environments (JupyterLab, VS Code, Colab if possible) +4. Document any runtime URL configuration needed for different setups +5. Consider bundling the runtime for offline use + +## Alternative: Test Without Jupyter First + +If you want to test the HTML generation without Jupyter: + +```python +# In a regular Python script +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +import autark as ak + +# Create a simple spec +neighborhoods = ak.GeoJSON( + "neighborhoods", + url="/examples/data/neighborhoods.geojson", + coordinate_format="EPSG:4326", + layer_type="polygons" +) + +spec = ak.Spec( + workspace=ak.Workspace(name="test", coordinate_format="EPSG:4326"), + data=[neighborhoods], + views=[ + ak.Map( + name="test_map", + layers=[ak.Layer(neighborhoods, type="polygons")] + ) + ] +) + +# Save HTML +spec.save_html("/tmp/test_autark.html") +print("Saved to /tmp/test_autark.html") + +# Open in browser +import webbrowser +webbrowser.open("file:///tmp/test_autark.html") +``` + +This tests the HTML generation without needing Jupyter running. diff --git a/python/examples/README.md b/python/examples/README.md new file mode 100644 index 00000000..eb9256eb --- /dev/null +++ b/python/examples/README.md @@ -0,0 +1,405 @@ +# Autark Python API Examples + +This directory contains working examples demonstrating the Autark Python API. + +## Available Examples + +### 1. Simple GeoJSON Map ([simple_geojson_map.py](simple_geojson_map.py)) + +**What it demonstrates:** +- Loading GeoJSON data from a URL +- Creating a basic map view +- Styling polygon layers (color, opacity, stroke) +- Setting camera position (pitch, bearing, zoom) + +**Usage:** +```bash +# Print JSON spec to stdout +python simple_geojson_map.py + +# Validate against schema +python simple_geojson_map.py --validate + +# Save to JSON file +python simple_geojson_map.py --output /tmp/simple_map.json + +# Save to HTML file +python simple_geojson_map.py --html /tmp/simple_map.html +``` + +**Generated spec:** ~40 lines of JSON + +--- + +### 2. CSV Points Visualization ([csv_points_map.py](csv_points_map.py)) + +**What it demonstrates:** +- Loading CSV data with latitude/longitude columns +- Creating point geometries from coordinates +- Visualizing points on a map +- Styling point markers (color, size, opacity) +- Adding a histogram of point attributes +- Using vertical layout to stack views + +**Usage:** +```bash +# Print JSON spec to stdout +python csv_points_map.py + +# Validate against schema +python csv_points_map.py --validate + +# Save to JSON file +python csv_points_map.py --output /tmp/csv_points.json + +# Save to HTML file +python csv_points_map.py --html /tmp/csv_points.html +``` + +**Generated spec:** ~60 lines of JSON + +--- + +### 3. Spatial Join Workflow ([spatial_join.py](spatial_join.py)) + +**What it demonstrates:** +- Loading multiple data sources (GeoJSON polygons + CSV points) +- Performing spatial join with aggregation +- Counting points within polygons +- Collecting attribute values per polygon +- Thematic mapping based on aggregated values +- Linked selection between histogram and map +- Highlight action to emphasize selected features + +**Usage:** +```bash +# Print JSON spec to stdout +python spatial_join.py + +# Save to JSON file +python spatial_join.py --output /tmp/spatial_join.json +``` + +**Generated spec:** ~90 lines of JSON + +--- + +### 4. GeoPandas Workflow ([geopandas_workflow.py](geopandas_workflow.py)) + +**What it demonstrates:** +- Loading polygon data with GeoPandas +- Loading and filtering tabular point data with pandas +- Computing per-neighborhood tree counts with GeoPandas/pandas +- Serializing processed GeoDataFrames with `GeoJSON.from_geopandas()` +- Visualizing polygons and points with a map and histogram + +**Usage:** +```bash +# Requires pandas/geopandas, for example: +conda run -n urban python geopandas_workflow.py --validate + +# Save to JSON file +conda run -n urban python geopandas_workflow.py --output /tmp/geopandas_workflow.json + +# Save to HTML file +conda run -n urban python geopandas_workflow.py --html /tmp/geopandas_workflow.html +``` + +**Generated spec:** inline GeoJSON for processed neighborhoods and tree points + +--- + +### 5. OSMnx to Autark Workflow ([osmnx_autark_workflow.py](osmnx_autark_workflow.py)) + +**What it demonstrates:** +- Downloading a small street network with OSMnx +- Computing edge speeds and travel times +- Converting an OSMnx graph to GeoPandas node/edge GeoDataFrames +- Serializing those GeoDataFrames with `GeoJSON.from_geopandas()` +- Visualizing the network with an Autark map and edge table + +**Usage:** +```bash +# Requires OSMnx/GeoPandas and network access to Overpass. +# The local uv environment created during testing is: +.venv-osmnx/bin/python python/examples/osmnx_autark_workflow.py --validate + +# Save to HTML file +.venv-osmnx/bin/python python/examples/osmnx_autark_workflow.py --html /tmp/osmnx_autark.html +``` + +**Jupyter:** +Use the `Python (Autark OSMnx)` kernel, then: + +```python +from osmnx_autark_workflow import build_spec + +spec = build_spec() +spec.widget(height="700px") +``` + +**Generated spec:** inline GeoJSON for OSMnx street edges and nodes + +--- + +## Testing the Examples + +### Option 1: Generate and Inspect JSON + +```bash +# Validate that the Python API generates valid JSON +python simple_geojson_map.py --validate +python csv_points_map.py --validate +conda run -n urban python geopandas_workflow.py --validate +.venv-osmnx/bin/python python/examples/osmnx_autark_workflow.py --validate +``` + +This checks that the generated spec matches the JSON Schema. + +### Option 2: Generate HTML and View in Browser + +```bash +# Generate HTML files +python simple_geojson_map.py --html /tmp/simple_map.html +python csv_points_map.py --html /tmp/csv_points.html +conda run -n urban python geopandas_workflow.py --html /tmp/geopandas_workflow.html +.venv-osmnx/bin/python python/examples/osmnx_autark_workflow.py --html /tmp/osmnx_autark.html +python spatial_join.py --output /tmp/spatial_join.json + +# Start a local server from the repo root +cd /Users/csilva/src/autark +python -m http.server 8000 + +# Open the HTML in your browser +open /tmp/simple_map.html +open /tmp/csv_points.html +``` + +**Note:** The HTML files reference the runtime at `/autk-runtime/dist/autk-runtime.js`, so you need to serve them from a server that has access to the runtime and data files. + +### Option 3: Use in Jupyter Notebook + +See [test_jupyter_integration.ipynb](test_jupyter_integration.ipynb) for a working notebook that demonstrates all examples. + +```bash +# Start server (terminal 1) +cd /Users/csilva/src/autark +python -m http.server 8000 + +# Start Jupyter (terminal 2) +cd /Users/csilva/src/autark/python/examples +jupyter notebook test_jupyter_integration.ipynb +``` + +--- + +## Data Files + +The examples use sample data files located at: +- `/examples/data/neighborhoods.geojson` - 3 neighborhood polygons +- `/examples/data/trees.csv` - Street tree points with lat/lng coordinates + +These are small fixtures for testing. For real-world usage, you would use your own data files. + +--- + +## Python API Quick Reference + +### Data Sources + +```python +import autark as ak + +# GeoJSON from URL +neighborhoods = ak.GeoJSON( + "neighborhoods", + url="/path/to/data.geojson", + coordinate_format="EPSG:4326", + layer_type="polygons" +) + +# CSV with lat/lng +points = ak.CSV( + "points", + url="/path/to/data.csv", + geometry=ak.latlng("lat_col", "lng_col", coordinate_format="EPSG:4326") +) + +# CSV with WKT geometry +shapes = ak.CSV( + "shapes", + url="/path/to/data.csv", + geometry=ak.wkt("geom_col", coordinate_format="EPSG:4326") +) + +# GeoPandas GeoDataFrame as inline GeoJSON +neighborhoods = ak.GeoJSON.from_geopandas("neighborhoods", gdf) + +# pandas DataFrame with lat/lng as inline point GeoJSON +trees = ak.GeoJSON.from_dataframe("trees", df, latitude="lat", longitude="lon") + +# OSM data (requires Overpass API or PBF file) +buildings = ak.OSM( + "manhattan_osm", + area="Manhattan, New York", + layers=["buildings", "roads"] +) +``` + +### Views + +```python +# Map with styled layers +map_view = ak.Map( + name="my_map", + camera=ak.Camera(pitch=0, bearing=0, zoom=12), + layers=[ + ak.Layer(neighborhoods, id="neighborhoods_layer", type="polygons") + .style(color="#3498db", opacity=0.7) + ] +) + +# Histogram +hist = ak.Histogram( + name="my_histogram", + source=points, + x="attribute_name", + bins=30 +) +``` + +### Transforms + +```python +# Spatial join with aggregation +joined = ak.SpatialJoin( + root=neighborhoods, + join=points, + group_by=[ + ak.count(), + ak.total("value_col"), + ak.collect("name_col", as_="names") + ] +) +``` + +### Interactions + +```python +# Selection +brush = ak.interval("my_brush") + +# Link selection to layer +link = ak.Link(brush, target="layer_id", action="highlight") + +# Use in histogram +hist = ak.Histogram(source=data, x="field", selection=brush) +``` + +### Complete Spec + +```python +spec = ak.Spec( + metadata=ak.Metadata(title="My Visualization"), + workspace=ak.Workspace(name="my_workspace", coordinate_format="EPSG:4326"), + data=[data_source1, data_source2], + transforms=[transform1], + views=[map_view, histogram], + links=[link], + layout=ak.Layout(type="vertical") +) + +# Export +spec.save_json("output.json") +spec.save_html("output.html") +spec.validate() # Check against schema +``` + +--- + +## Common Patterns + +### Thematic Mapping (Color by Attribute) + +```python +ak.Layer(neighborhoods, type="polygons") + .encode( + color=ak.field( + "properties.value", + scale=ak.Scale(type="quantile", scheme="viridis") + ) + ) +``` + +### Linked Brushing + +```python +brush = ak.interval("brush") + +views = [ + ak.Histogram(source=data, x="field", selection=brush), + ak.Map(layers=[ + ak.Layer(data, id="layer1") + ]) +] + +links = [ak.Link(brush, target="layer1", action="highlight")] +``` + +### Multi-Layer Maps + +```python +ak.Map( + layers=[ + ak.Layer(polygons, type="polygons").style(color="#aaa", opacity=0.5), + ak.Layer(points, type="points").style(color="#f00", size=4) + ] +) +``` + +--- + +## Troubleshooting + +### "Module not found" error + +Make sure you're running from the examples directory or have added the parent directory to your Python path: + +```python +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parents[1])) +import autark as ak +``` + +### "Schema validation failed" + +Check that your spec structure matches the schema. Common issues: +- Missing required fields (name, type, etc.) +- Wrong field names (e.g., `latitudeColumn` vs `latitude`) +- Invalid values (e.g., zoom outside valid range) + +Run with `--validate` to get detailed error messages. + +### HTML doesn't render in browser + +Make sure: +1. You're serving the HTML via HTTP (not opening as `file://`) +2. The runtime JavaScript is accessible at `/autk-runtime/dist/autk-runtime.js` +3. Data files are accessible at their specified URLs +4. Check browser console (F12) for errors + +--- + +## Next Steps + +- Try modifying the examples with your own data +- Combine multiple examples (e.g., spatial join + linked brushing) +- Explore more encoding options (size, opacity, height) +- Add multiple views with different layouts +- Test in Jupyter notebooks for interactive exploration + +For more details, see: +- [PYTHON_API.md](../../PYTHON_API.md) - Full Python API guide +- [PYTHON_API_IMPLEMENTATION.md](../../PYTHON_API_IMPLEMENTATION.md) - Implementation tracker +- [schema/autark-spec-v0.1.json](../../schema/autark-spec-v0.1.json) - JSON Schema reference diff --git a/python/examples/__init__.py b/python/examples/__init__.py new file mode 100644 index 00000000..288de4ae --- /dev/null +++ b/python/examples/__init__.py @@ -0,0 +1 @@ +"""Example Autark specs built with the Python API.""" diff --git a/python/examples/csv_points_map.py b/python/examples/csv_points_map.py new file mode 100644 index 00000000..828c5adf --- /dev/null +++ b/python/examples/csv_points_map.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +CSV Points Visualization Example + +Demonstrates: +- Loading CSV data with lat/lng columns +- Creating point geometries from coordinates +- Visualizing points on a map +- Styling point markers (color, size, opacity) +- Adding a histogram of point attributes +""" +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import autark as ak + + +def build_spec() -> ak.Spec: + """Build a CSV points visualization specification.""" + # Define the CSV data source with lat/lng geometry + trees = ak.CSV( + "trees", + url="/examples/data/trees.csv", + geometry=ak.latlng( + "latitude", + "longitude", + coordinate_format="EPSG:4326" + ) + ) + + # Create the specification with map and histogram + return ak.Spec( + metadata=ak.Metadata( + title="CSV Points Visualization", + description="Street trees shown as points on a map with a latitude distribution histogram.", + created="2026-06-10", + ), + workspace=ak.Workspace( + name="csv_points", + coordinate_format="EPSG:4326" + ), + data=[trees], + views=[ + # Map view with point layer + ak.Map( + name="tree_map", + camera=ak.Camera( + pitch=0, + bearing=0, + zoom=13 + ), + layers=[ + ak.Layer(trees, id="trees_layer", type="points") + .style( + color="#27ae60", # Green + size=5, + opacity=0.8 + ) + ] + ), + # Histogram of latitude values + ak.Histogram( + name="latitude_histogram", + source=trees, + x="latitude", + bins=25 + ) + ], + # Stack views vertically + layout=ak.Layout(type="vertical") + ) + + +def main() -> None: + """Generate and optionally save the specification.""" + parser = argparse.ArgumentParser( + description="Generate a CSV points visualization example spec." + ) + parser.add_argument( + "--output", + type=Path, + help="Write the generated JSON spec to this path." + ) + parser.add_argument( + "--html", + type=Path, + help="Write the generated HTML to this path." + ) + parser.add_argument( + "--validate", + action="store_true", + help="Validate the spec against the JSON schema." + ) + args = parser.parse_args() + + # Build the spec + spec = build_spec() + + # Validate if requested + if args.validate: + try: + schema_path = Path(__file__).parents[2] / "schema" / "autark-spec-v0.1.json" + spec.validate(schema_path) + print("✅ Spec validation passed!") + except Exception as e: + print(f"❌ Validation failed: {e}") + sys.exit(1) + + # Save to JSON if requested + if args.output: + spec.save_json(args.output) + print(f"✅ JSON spec saved to: {args.output}") + + # Save to HTML if requested + if args.html: + spec.save_html(args.html) + print(f"✅ HTML saved to: {args.html}") + + # Otherwise print to stdout + if not args.output and not args.html: + print(spec.to_json()) + + +if __name__ == "__main__": + main() diff --git a/python/examples/geopandas_workflow.py b/python/examples/geopandas_workflow.py new file mode 100644 index 00000000..de901bf9 --- /dev/null +++ b/python/examples/geopandas_workflow.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +""" +GeoPandas Workflow Example + +Demonstrates: +- Loading polygon data with GeoPandas +- Loading and filtering tabular point data with pandas +- Joining derived pandas/geospatial results back to a GeoDataFrame +- Visualizing processed GeoDataFrames with Autark +""" +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import autark as ak + +REPO_ROOT = Path(__file__).resolve().parents[2] +DATA_DIR = REPO_ROOT / "examples" / "data" + + +def _load_processed_data() -> tuple[object, object]: + """Load sample data and return processed neighborhood/tree GeoDataFrames.""" + try: + import geopandas as gpd + import pandas as pd + except ModuleNotFoundError as exc: + raise RuntimeError( + "This example requires pandas and geopandas. " + "Use the 'urban' conda environment or install them in your Python environment." + ) from exc + + neighborhoods = gpd.read_file(DATA_DIR / "neighborhoods.geojson") + if neighborhoods.crs is None: + neighborhoods = neighborhoods.set_crs("EPSG:4326") + + trees_df = pd.read_csv(DATA_DIR / "trees.csv") + + # pandas processing step: keep common street-tree species and add a simple + # analysis field that can be plotted or inspected downstream. + focus_species = {"oak", "maple", "elm"} + trees_df = trees_df[trees_df["species"].isin(focus_species)].copy() + trees_df["species_rank"] = trees_df["species"].map({"oak": 3, "maple": 2, "elm": 1}).astype(int) + + trees = gpd.GeoDataFrame( + trees_df, + geometry=gpd.points_from_xy(trees_df["longitude"], trees_df["latitude"]), + crs="EPSG:4326", + ) + + joined = gpd.sjoin( + trees[["id", "species", "species_rank", "geometry"]], + neighborhoods[["name", "geometry"]], + how="left", + predicate="within", + ) + counts = ( + joined.dropna(subset=["name"]) + .groupby("name") + .agg(tree_count=("id", "count"), avg_species_rank=("species_rank", "mean")) + .reset_index() + ) + + neighborhoods = neighborhoods.merge(counts, on="name", how="left") + neighborhoods["tree_count"] = neighborhoods["tree_count"].fillna(0).astype(int) + neighborhoods["avg_species_rank"] = neighborhoods["avg_species_rank"].fillna(0.0) + neighborhoods = neighborhoods[neighborhoods["tree_count"] > 0].copy() + + return neighborhoods, trees + + +def build_spec() -> ak.Spec: + """Build a GeoPandas-driven Autark specification.""" + neighborhoods_gdf, trees_gdf = _load_processed_data() + + neighborhoods = ak.GeoJSON.from_geopandas( + "neighborhoods", + neighborhoods_gdf, + layer_type="polygons", + ) + trees = ak.GeoJSON.from_geopandas( + "trees", + trees_gdf, + layer_type="points", + ) + + return ak.Spec( + metadata=ak.Metadata( + title="GeoPandas Neighborhood Tree Counts", + description=( + "Loads neighborhoods with GeoPandas, filters tree records with pandas, " + "computes per-neighborhood counts, and visualizes the processed result." + ), + created="2026-06-10", + ), + workspace=ak.Workspace(name="geopandas_workflow", coordinate_format="EPSG:4326"), + data=[neighborhoods, trees], + views=[ + ak.Map( + name="neighborhood_tree_map", + camera=ak.Camera(pitch=0, bearing=0, zoom=13), + layers=[ + ak.Layer(neighborhoods, id="neighborhoods_layer", type="polygons") + .encode( + color=ak.field( + "tree_count", + scale=ak.Scale(type="quantile", scheme="greens"), + ) + ) + .style(opacity=0.75, strokeColor="#243b53", strokeWidth=1), + ak.Layer(trees, id="trees_layer", type="points").style( + color="#1f9d55", + size=5, + opacity=0.85, + ), + ], + ), + ak.Histogram( + name="tree_count_histogram", + source=neighborhoods, + x="tree_count", + bins=8, + ), + ], + layout=ak.Layout(type="vertical"), + ) + + +def main() -> None: + """Generate and optionally save the specification.""" + parser = argparse.ArgumentParser( + description="Generate a GeoPandas workflow example spec." + ) + parser.add_argument( + "--output", + type=Path, + help="Write the generated JSON spec to this path.", + ) + parser.add_argument( + "--html", + type=Path, + help="Write the generated HTML to this path.", + ) + parser.add_argument( + "--validate", + action="store_true", + help="Validate the spec against the JSON schema.", + ) + args = parser.parse_args() + + spec = build_spec() + + if args.validate: + try: + schema_path = REPO_ROOT / "schema" / "autark-spec-v0.1.json" + spec.validate(schema_path) + print("Spec validation passed.") + except Exception as exc: + print(f"Validation failed: {exc}") + sys.exit(1) + + if args.output: + spec.save_json(args.output) + print(f"JSON spec saved to: {args.output}") + + if args.html: + spec.save_html(args.html) + print(f"HTML saved to: {args.html}") + + if not args.output and not args.html: + print(spec.to_json()) + + +if __name__ == "__main__": + main() diff --git a/python/examples/geotiff_raster_example.py b/python/examples/geotiff_raster_example.py new file mode 100644 index 00000000..d324e32e --- /dev/null +++ b/python/examples/geotiff_raster_example.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +""" +Example demonstrating GeoTIFF raster data loading with Autark. +""" + +import autark as ak + +def main(): + # Create a spec with GeoTIFF data source + spec = ak.Spec( + metadata=ak.Metadata( + title="GeoTIFF Raster Visualization", + description="Loading and visualizing GeoTIFF raster data" + ), + workspace=ak.Workspace(name="raster_demo") + ) + + # Add GeoTIFF data source + # This would typically point to a real GeoTIFF file + geotiff_data = ak.GeoTIFF( + name="elevation", + url="https://example.com/data/elevation.tif", + band=1 # Use the first band + ) + spec.add_data(geotiff_data) + + # Create a map view with raster layer + map_view = ak.Map( + id="elevation_map", + title="Elevation Map" + ) + + # Add raster layer + # Note: The exact raster layer API may need adjustment based on runtime support + raster_layer = ak.RasterLayer( + id="elevation_layer", + source="elevation", + encoding={ + "color": { + "field": "value", + "scale": { + "domain": [0, 1000], # Elevation range in meters + "range": ["green", "brown", "white"] # Valley to peak colors + } + } + } + ) + map_view.add_layer(raster_layer) + + # Set viewport + map_view.set_viewport( + center=[-74.0, 40.7], + zoom=10 + ) + + spec.add_view(map_view) + + # Save the spec + spec.save_json("geotiff_raster_example.json") + print("Saved geotiff_raster_example.json") + + # For Jupyter notebooks, display the spec + # return spec + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/python/examples/json_data_example.py b/python/examples/json_data_example.py new file mode 100644 index 00000000..42e227be --- /dev/null +++ b/python/examples/json_data_example.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +""" +Example demonstrating JSON data source loading with Autark. +""" + +import autark as ak + +def main(): + # Create a spec with JSON data source + spec = ak.Spec( + metadata=ak.Metadata( + title="JSON Data Example", + description="Loading and visualizing JSON data" + ), + workspace=ak.Workspace(name="json_demo") + ) + + # Add JSON data from a URL + json_url_data = ak.JSON( + name="remote_events", + url="https://example.com/events.json" + ) + spec.add_data(json_url_data) + + # Add inline JSON data + inline_data = [ + {"id": 1, "name": "Station A", "lat": 40.7128, "lon": -74.0060, "capacity": 50}, + {"id": 2, "name": "Station B", "lat": 40.7580, "lon": -73.9855, "capacity": 75}, + {"id": 3, "name": "Station C", "lat": 40.7489, "lon": -73.9680, "capacity": 60}, + ] + + json_inline_data = ak.JSON( + name="stations", + values=inline_data + ) + spec.add_data(json_inline_data) + + # Create a scatter plot view + scatter = ak.Scatterplot( + id="station_capacity", + title="Station Capacity", + data="stations" + ) + scatter.encode_x("id", title="Station ID") + scatter.encode_y("capacity", title="Capacity") + scatter.encode_color("name") + spec.add_view(scatter) + + # Create a table view + table = ak.Table( + id="station_table", + title="Station Data", + data="stations", + columns=["id", "name", "lat", "lon", "capacity"] + ) + spec.add_view(table) + + # Set layout + spec.set_grid_layout([ + ["station_capacity"], + ["station_table"] + ]) + + # Save the spec + spec.save_json("json_data_example.json") + print("Saved json_data_example.json") + + # For Jupyter notebooks, display the spec + # return spec + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/python/examples/osmnx_autark_workflow.py b/python/examples/osmnx_autark_workflow.py new file mode 100644 index 00000000..b49a9cc3 --- /dev/null +++ b/python/examples/osmnx_autark_workflow.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +""" +OSMnx to Autark Workflow Example + +Demonstrates: +- Downloading a small street network with OSMnx +- Computing edge speed and travel-time attributes +- Converting the OSMnx graph to GeoPandas GeoDataFrames +- Serializing GeoDataFrames as inline Autark GeoJSON sources +- Rendering the network with an interactive Autark map and table +""" +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import autark as ak + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _load_osmnx_network( + *, + latitude: float, + longitude: float, + distance: int, + network_type: str, +) -> tuple[object, object]: + """Fetch an OSMnx network and return node/edge GeoDataFrames.""" + try: + import osmnx as ox + except ModuleNotFoundError as exc: + raise RuntimeError( + "This example requires OSMnx and GeoPandas. " + "Use the local '.venv-osmnx' environment or install osmnx in your Python environment." + ) from exc + + # OSMnx otherwise creates a ./cache directory in the current working + # directory. Keep generated Overpass responses out of the repo. + ox.settings.use_cache = True + ox.settings.cache_folder = "/tmp/autark-osmnx-cache" + + graph = ox.graph_from_point( + (latitude, longitude), + dist=distance, + network_type=network_type, + simplify=True, + ) + graph = ox.add_edge_speeds(graph) + graph = ox.add_edge_travel_times(graph) + + nodes, edges = ox.graph_to_gdfs(graph) + return nodes.reset_index(), edges.reset_index() + + +def build_spec( + *, + latitude: float = 40.7128, + longitude: float = -74.0060, + distance: int = 200, + network_type: str = "walk", +) -> ak.Spec: + """Build an Autark spec from an OSMnx street network.""" + nodes_gdf, edges_gdf = _load_osmnx_network( + latitude=latitude, + longitude=longitude, + distance=distance, + network_type=network_type, + ) + + edges = ak.GeoJSON.from_geopandas( + "street_edges", + edges_gdf, + layer_type="polylines", + ) + nodes = ak.GeoJSON.from_geopandas( + "street_nodes", + nodes_gdf, + layer_type="points", + ) + + return ak.Spec( + metadata=ak.Metadata( + title="OSMnx Walk Network in Autark", + description=( + "OSMnx downloads and analyzes a street network; Autark renders " + "the resulting GeoDataFrames as an interactive map and table." + ), + created="2026-06-11", + ), + workspace=ak.Workspace(name="osmnx_autark_workflow", coordinate_format="EPSG:4326"), + data=[edges, nodes], + views=[ + ak.Map( + name="network_map", + camera=ak.Camera(zoom=13), + layers=[ + ak.Layer("street_edges", id="edges", type="polylines").style( + color="#3949ab", + width=6, + opacity=0.9, + ), + ak.Layer("street_nodes", id="nodes", type="points").style( + color="#f57c00", + size=3, + opacity=0.85, + ), + ], + ), + ak.Table( + "street_edges", + name="edge_table", + columns=["u", "v", "length", "speed_kph", "travel_time", "highway"], + sort={"column": "travel_time", "direction": "desc"}, + ), + ], + layout=ak.Layout(type="vertical"), + ) + + +def main() -> None: + """Generate and optionally save the OSMnx/Autark specification.""" + parser = argparse.ArgumentParser( + description="Generate an Autark visualization from a small OSMnx street network." + ) + parser.add_argument("--latitude", type=float, default=40.7128) + parser.add_argument("--longitude", type=float, default=-74.0060) + parser.add_argument("--distance", type=int, default=200, help="Network radius in meters.") + parser.add_argument("--network-type", default="walk", help="OSMnx network type.") + parser.add_argument("--output", type=Path, help="Write the generated JSON spec to this path.") + parser.add_argument("--html", type=Path, help="Write the generated embedded HTML to this path.") + parser.add_argument( + "--validate", + action="store_true", + help="Validate the spec against the JSON schema.", + ) + args = parser.parse_args() + + try: + spec = build_spec( + latitude=args.latitude, + longitude=args.longitude, + distance=args.distance, + network_type=args.network_type, + ) + except Exception as exc: + print(f"Failed to build OSMnx/Autark example: {exc}", file=sys.stderr) + sys.exit(1) + + if args.validate: + try: + schema_path = REPO_ROOT / "schema" / "autark-spec-v0.1.json" + spec.validate(schema_path) + print("Spec validation passed.") + except Exception as exc: + print(f"Validation failed: {exc}", file=sys.stderr) + sys.exit(1) + + if args.output: + spec.save_json(args.output) + print(f"JSON spec saved to: {args.output}") + + if args.html: + spec.save_html(args.html) + print(f"HTML saved to: {args.html}") + + if not args.output and not args.html: + print(spec.to_json()) + + +if __name__ == "__main__": + main() diff --git a/python/examples/simple_geojson_map.py b/python/examples/simple_geojson_map.py new file mode 100644 index 00000000..c2b4c311 --- /dev/null +++ b/python/examples/simple_geojson_map.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +""" +Simple GeoJSON Map Example + +Demonstrates: +- Loading GeoJSON data from a URL +- Creating a basic map view +- Styling polygon layers with color and opacity +- Setting camera position +""" +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import autark as ak + + +def build_spec() -> ak.Spec: + """Build a simple GeoJSON map specification.""" + # Define the GeoJSON data source + neighborhoods = ak.GeoJSON( + "neighborhoods", + url="/examples/data/neighborhoods.geojson", + coordinate_format="EPSG:4326", + layer_type="polygons", + ) + + # Create the specification + return ak.Spec( + metadata=ak.Metadata( + title="Simple GeoJSON Map", + description="A basic map showing neighborhood polygons with custom styling.", + created="2026-06-10", + ), + workspace=ak.Workspace( + name="simple_map", + coordinate_format="EPSG:4326" + ), + data=[neighborhoods], + views=[ + ak.Map( + name="main_map", + camera=ak.Camera( + pitch=0, + bearing=0, + zoom=13 + ), + layers=[ + ak.Layer(neighborhoods, id="neighborhoods_layer", type="polygons") + .style( + color="#3498db", # Blue + opacity=0.7, + strokeColor="#2c3e50", # Dark blue-grey + strokeWidth=2 + ) + ] + ) + ] + ) + + +def main() -> None: + """Generate and optionally save the specification.""" + parser = argparse.ArgumentParser( + description="Generate a simple GeoJSON map example spec." + ) + parser.add_argument( + "--output", + type=Path, + help="Write the generated JSON spec to this path." + ) + parser.add_argument( + "--html", + type=Path, + help="Write the generated HTML to this path." + ) + parser.add_argument( + "--validate", + action="store_true", + help="Validate the spec against the JSON schema." + ) + args = parser.parse_args() + + # Build the spec + spec = build_spec() + + # Validate if requested + if args.validate: + try: + # Assumes schema is at ../../schema/autark-spec-v0.1.json + schema_path = Path(__file__).parents[2] / "schema" / "autark-spec-v0.1.json" + spec.validate(schema_path) + print("✅ Spec validation passed!") + except Exception as e: + print(f"❌ Validation failed: {e}") + sys.exit(1) + + # Save to JSON if requested + if args.output: + spec.save_json(args.output) + print(f"✅ JSON spec saved to: {args.output}") + + # Save to HTML if requested + if args.html: + spec.save_html(args.html) + print(f"✅ HTML saved to: {args.html}") + + # Otherwise print to stdout + if not args.output and not args.html: + print(spec.to_json()) + + +if __name__ == "__main__": + main() diff --git a/python/examples/spatial_join.py b/python/examples/spatial_join.py new file mode 100644 index 00000000..49fa9e97 --- /dev/null +++ b/python/examples/spatial_join.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import autark as ak + + +def build_spec() -> ak.Spec: + neighborhoods = ak.GeoJSON( + "neighborhoods", + url="/examples/data/neighborhoods.geojson", + coordinate_format="EPSG:4326", + layer_type="polygons", + ) + trees = ak.CSV( + "trees", + url="/examples/data/trees.csv", + geometry=ak.latlng("latitude", "longitude", coordinate_format="EPSG:4326"), + ) + brush = ak.interval("tree_count_brush") + + return ak.Spec( + metadata=ak.Metadata( + title="Spatial Join - Trees per Neighborhood", + description=( + "Demonstrates spatial join between neighborhoods and tree points, " + "aggregating tree counts and visualizing on a thematic map." + ), + created="2026-06-07", + ), + workspace=ak.Workspace(name="spatial_join_demo", coordinate_format="EPSG:4326"), + data=[neighborhoods, trees], + transforms=[ + ak.SpatialJoin( + root=neighborhoods, + join=trees, + group_by=[ + ak.count(), + ak.collect("species", as_="tree_species"), + ], + ) + ], + views=[ + ak.Map( + name="neighborhood_map", + camera=ak.Camera(pitch=0, bearing=0, zoom=12), + layers=[ + ak.Layer("neighborhoods", id="neighborhoods_layer", type="polygons") + .encode( + color=ak.field( + "properties.sjoin.count.trees", + scale=ak.Scale(type="quantile", scheme="greens", domain_strategy="minMax"), + ) + ) + .style(opacity=0.8, strokeColor="#333333", strokeWidth=1), + ak.Layer(trees, id="trees_layer", type="points").style( + color="#228B22", + size=3, + opacity=0.6, + ), + ], + ), + ak.Histogram( + name="tree_count_histogram", + source=neighborhoods, + x="sjoin.count.trees", + bins=20, + selection=brush, + ), + ], + links=[ + ak.Link(brush, target="neighborhoods_layer", action="highlight"), + ], + layout=ak.Layout(type="vertical"), + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate the Autark spatial join example spec.") + parser.add_argument("--output", type=Path, help="Write the generated JSON spec to this path.") + args = parser.parse_args() + + spec = build_spec() + if args.output: + spec.save_json(args.output) + else: + print(spec.to_json()) + + +if __name__ == "__main__": + main() diff --git a/python/examples/test_html_output.py b/python/examples/test_html_output.py new file mode 100644 index 00000000..bb0e349b --- /dev/null +++ b/python/examples/test_html_output.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +""" +Quick test of HTML generation without needing Jupyter running. +This creates a standalone HTML file you can open in a browser. +""" +from __future__ import annotations + +import sys +import webbrowser +from pathlib import Path + +# Add parent directory to path +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import autark as ak + + +def main(): + # Create a simple GeoJSON map spec + neighborhoods = ak.GeoJSON( + "neighborhoods", + url="/examples/data/neighborhoods.geojson", + coordinate_format="EPSG:4326", + layer_type="polygons", + ) + + spec = ak.Spec( + metadata=ak.Metadata( + title="Simple GeoJSON Map - HTML Test", + description="Test of HTML generation and browser rendering", + ), + workspace=ak.Workspace(name="html_test", coordinate_format="EPSG:4326"), + data=[neighborhoods], + views=[ + ak.Map( + name="test_map", + camera=ak.Camera(pitch=0, bearing=0, zoom=12), + layers=[ + ak.Layer(neighborhoods, id="neighborhoods_layer", type="polygons").style( + color="#4a90e2", opacity=0.7, strokeColor="#2c5aa0", strokeWidth=2 + ) + ], + ) + ], + ) + + # Save HTML to tmp directory + output_path = Path("/tmp/autark_html_test.html") + + # Option 1: Use default runtime URL (requires local server) + # spec.save_html(output_path) + + # Option 2: Use explicit runtime URL for local testing + # You'll need to start a local server first: + # cd /Users/csilva/src/autark && python -m http.server 8000 + spec.save_html( + output_path, + runtime_url="http://localhost:8000/autk-runtime/dist/autk-runtime.js" + ) + + print(f"✅ HTML saved to: {output_path}") + print(f" File size: {output_path.stat().st_size} bytes") + print() + print("To view:") + print(f" 1. Start a local server from repo root:") + print(f" cd /Users/csilva/src/autark && python -m http.server 8000") + print(f" 2. Open in browser:") + print(f" open {output_path}") + print() + print("Or just run with --open flag to auto-open") + + # Check if --open flag passed + if len(sys.argv) > 1 and sys.argv[1] == "--open": + print(f"\nOpening in browser...") + webbrowser.open(str(output_path)) + + +if __name__ == "__main__": + main() diff --git a/python/examples/test_jupyter_integration.ipynb b/python/examples/test_jupyter_integration.ipynb new file mode 100644 index 00000000..f3856d9b --- /dev/null +++ b/python/examples/test_jupyter_integration.ipynb @@ -0,0 +1,858 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Autark Python API - Jupyter Integration Test\n", + "\n", + "This notebook tests the `_repr_html_()` integration for displaying Autark specs in Jupyter." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/Users/csilva/src/autark/.venv-osmnx/bin/python\n", + "/Users/csilva/src/autark/python/autark/__init__.py\n", + "0.11.0\n" + ] + } + ], + "source": [ + "import sys\n", + "import autark as ak\n", + "import anywidget\n", + "import osmnx as ox\n", + "\n", + "print(sys.executable)\n", + "print(ak.__file__)\n", + "print(anywidget.__version__)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Setup: Add parent directory to path\n", + "import sys\n", + "from pathlib import Path\n", + "\n", + "# Add python/ directory to path\n", + "python_dir = Path.cwd().parent\n", + "if str(python_dir) not in sys.path:\n", + " sys.path.insert(0, str(python_dir))\n", + "\n", + "import autark as ak" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test 1: Simple GeoJSON Map\n", + "\n", + "Load a GeoJSON file and display it on a map." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0cdaac57851f49729379e3a813bd6a8e", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + ".AutarkWidget object at 0x12272dc40>" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create a simple GeoJSON map spec\n", + "neighborhoods = ak.GeoJSON(\n", + " \"neighborhoods\",\n", + " url=\"http://127.0.0.1:8000/examples/data/neighborhoods.geojson\",\n", + " coordinate_format=\"EPSG:4326\",\n", + " layer_type=\"polygons\"\n", + ")\n", + "\n", + "spec1 = ak.Spec(\n", + " metadata=ak.Metadata(\n", + " title=\"Simple GeoJSON Map\",\n", + " description=\"Basic test of GeoJSON rendering\"\n", + " ),\n", + " workspace=ak.Workspace(name=\"test_geojson\", coordinate_format=\"EPSG:4326\"),\n", + " data=[neighborhoods],\n", + " views=[\n", + " ak.Map(\n", + " name=\"simple_map\",\n", + " camera=ak.Camera(pitch=0, bearing=0, zoom=12),\n", + " layers=[\n", + " ak.Layer(neighborhoods, id=\"neighborhoods_layer\", type=\"polygons\")\n", + " .style(color=\"#4a90e2\", opacity=0.7)\n", + " ]\n", + " )\n", + " ]\n", + ")\n", + "\n", + "# Display - this should trigger _repr_html_()\n", + "spec1.widget(height=\"500px\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "ename": "RuntimeError", + "evalue": "This example requires OSMnx and GeoPandas. Use the local '.venv-osmnx' environment or install osmnx in your Python environment.", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mModuleNotFoundError\u001b[39m Traceback (most recent call last)", + "\u001b[36mFile \u001b[39m\u001b[32m~/src/autark/python/examples/osmnx_autark_workflow.py:35\u001b[39m, in \u001b[36m_load_osmnx_network\u001b[39m\u001b[34m(latitude, longitude, distance, network_type)\u001b[39m\n\u001b[32m 34\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m35\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mosmnx\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mox\u001b[39;00m\n\u001b[32m 36\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mModuleNotFoundError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n", + "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'osmnx'", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[31mRuntimeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[14]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mosmnx_autark_workflow\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m build_spec\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m spec = \u001b[43mbuild_spec\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 4\u001b[39m spec.widget(height=\u001b[33m\"\u001b[39m\u001b[33m700px\u001b[39m\u001b[33m\"\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/src/autark/python/examples/osmnx_autark_workflow.py:68\u001b[39m, in \u001b[36mbuild_spec\u001b[39m\u001b[34m(latitude, longitude, distance, network_type)\u001b[39m\n\u001b[32m 60\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mbuild_spec\u001b[39m(\n\u001b[32m 61\u001b[39m *,\n\u001b[32m 62\u001b[39m latitude: \u001b[38;5;28mfloat\u001b[39m = \u001b[32m40.7128\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 65\u001b[39m network_type: \u001b[38;5;28mstr\u001b[39m = \u001b[33m\"\u001b[39m\u001b[33mwalk\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 66\u001b[39m ) -> ak.Spec:\n\u001b[32m 67\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Build an Autark spec from an OSMnx street network.\"\"\"\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m68\u001b[39m nodes_gdf, edges_gdf = \u001b[43m_load_osmnx_network\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 69\u001b[39m \u001b[43m \u001b[49m\u001b[43mlatitude\u001b[49m\u001b[43m=\u001b[49m\u001b[43mlatitude\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 70\u001b[39m \u001b[43m \u001b[49m\u001b[43mlongitude\u001b[49m\u001b[43m=\u001b[49m\u001b[43mlongitude\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 71\u001b[39m \u001b[43m \u001b[49m\u001b[43mdistance\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdistance\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 72\u001b[39m \u001b[43m \u001b[49m\u001b[43mnetwork_type\u001b[49m\u001b[43m=\u001b[49m\u001b[43mnetwork_type\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 73\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 75\u001b[39m edges = ak.GeoJSON.from_geopandas(\n\u001b[32m 76\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mstreet_edges\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 77\u001b[39m edges_gdf,\n\u001b[32m 78\u001b[39m layer_type=\u001b[33m\"\u001b[39m\u001b[33mpolylines\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 79\u001b[39m )\n\u001b[32m 80\u001b[39m nodes = ak.GeoJSON.from_geopandas(\n\u001b[32m 81\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mstreet_nodes\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 82\u001b[39m nodes_gdf,\n\u001b[32m 83\u001b[39m layer_type=\u001b[33m\"\u001b[39m\u001b[33mpoints\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 84\u001b[39m )\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/src/autark/python/examples/osmnx_autark_workflow.py:37\u001b[39m, in \u001b[36m_load_osmnx_network\u001b[39m\u001b[34m(latitude, longitude, distance, network_type)\u001b[39m\n\u001b[32m 35\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mosmnx\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mox\u001b[39;00m\n\u001b[32m 36\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mModuleNotFoundError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m---> \u001b[39m\u001b[32m37\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\n\u001b[32m 38\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mThis example requires OSMnx and GeoPandas. \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 39\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mUse the local \u001b[39m\u001b[33m'\u001b[39m\u001b[33m.venv-osmnx\u001b[39m\u001b[33m'\u001b[39m\u001b[33m environment or install osmnx in your Python environment.\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 40\u001b[39m ) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mexc\u001b[39;00m\n\u001b[32m 42\u001b[39m \u001b[38;5;66;03m# OSMnx otherwise creates a ./cache directory in the current working\u001b[39;00m\n\u001b[32m 43\u001b[39m \u001b[38;5;66;03m# directory. Keep generated Overpass responses out of the repo.\u001b[39;00m\n\u001b[32m 44\u001b[39m ox.settings.use_cache = \u001b[38;5;28;01mTrue\u001b[39;00m\n", + "\u001b[31mRuntimeError\u001b[39m: This example requires OSMnx and GeoPandas. Use the local '.venv-osmnx' environment or install osmnx in your Python environment." + ] + } + ], + "source": [ + "from osmnx_autark_workflow import build_spec\n", + "\n", + "spec = build_spec()\n", + "spec.widget(height=\"700px\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Runtime URL: http://localhost:8000/autk-runtime/dist/autk-runtime.js\n", + "Data URL: http://localhost:8000/examples/data/neighborhoods.geojson\n", + "\n", + "Now displaying spec...\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "
\n", + " Autark spec\n", + "
{\n",
+       "  "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",\n",
+       "  "version": "0.1",\n",
+       "  "workspace": {\n",
+       "    "coordinateFormat": "EPSG:4326"\n",
+       "  },\n",
+       "  "data": [\n",
+       "    {\n",
+       "      "name": "test",\n",
+       "      "url": "http://localhost:8000/examples/data/neighborhoods.geojson",\n",
+       "      "layerType": "polygons",\n",
+       "      "coordinateFormat": "EPSG:4326",\n",
+       "      "type": "geojson"\n",
+       "    }\n",
+       "  ],\n",
+       "  "views": [\n",
+       "    {\n",
+       "      "layers": [\n",
+       "        {\n",
+       "          "source": "test",\n",
+       "          "type": "polygons"\n",
+       "        }\n",
+       "      ],\n",
+       "      "type": "map"\n",
+       "    }\n",
+       "  ]\n",
+       "}
\n", + "
" + ], + "text/plain": [ + "AutarkSpec(views=[Map(layers=[Layer(source='test', id=None, type='polygons', encoding=None, style_values=None)], name=None, camera=None, style=None, type='map')], data=[GeoJSON(name='test', url='http://localhost:8000/examples/data/neighborhoods.geojson', values=None, layer_type='polygons', coordinate_format='EPSG:4326', type='geojson')], transforms=None, links=None, layout=None, metadata=None, workspace=Workspace(name=None, coordinate_format='EPSG:4326'))" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Diagnostic cell - run this\n", + "import autark as ak\n", + "\n", + "spec = ak.Spec(\n", + " workspace=ak.Workspace(coordinate_format='EPSG:4326'),\n", + " data=[ak.GeoJSON('test', url='http://localhost:8000/examples/data/neighborhoods.geojson', \n", + " coordinate_format='EPSG:4326', layer_type='polygons')],\n", + " views=[ak.Map(layers=[ak.Layer('test', type='polygons')])]\n", + ")\n", + "\n", + "html = spec.to_html()\n", + "\n", + "# Check what URL it's using\n", + "import re\n", + "runtime_match = re.search(r'from \"([^\"]+)\"', html)\n", + "data_match = re.search(r'\"url\": \"([^\"]+)\"', html)\n", + "\n", + "print(\"Runtime URL:\", runtime_match.group(1) if runtime_match else \"NOT FOUND\")\n", + "print(\"Data URL:\", data_match.group(1) if data_match else \"NOT FOUND\")\n", + "print(\"\\nNow displaying spec...\")\n", + "\n", + "spec" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test 2: CSV Points with Histogram\n", + "\n", + "Load CSV data with lat/lng and create a histogram." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "
\n", + " Autark spec\n", + "
{\n",
+       "  "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",\n",
+       "  "version": "0.1",\n",
+       "  "metadata": {\n",
+       "    "title": "CSV Points with Histogram",\n",
+       "    "description": "Test CSV loading and histogram rendering"\n",
+       "  },\n",
+       "  "workspace": {\n",
+       "    "name": "test_csv",\n",
+       "    "coordinateFormat": "EPSG:4326"\n",
+       "  },\n",
+       "  "data": [\n",
+       "    {\n",
+       "      "name": "trees",\n",
+       "      "url": "http://localhost:8000/examples/data/trees.csv",\n",
+       "      "geometry": {\n",
+       "        "latitude": "latitude",\n",
+       "        "longitude": "longitude",\n",
+       "        "coordinateFormat": "EPSG:4326",\n",
+       "        "type": "latlng"\n",
+       "      },\n",
+       "      "type": "csv"\n",
+       "    }\n",
+       "  ],\n",
+       "  "views": [\n",
+       "    {\n",
+       "      "layers": [\n",
+       "        {\n",
+       "          "source": "trees",\n",
+       "          "id": "trees_layer",\n",
+       "          "type": "points",\n",
+       "          "style": {\n",
+       "            "color": "#228B22",\n",
+       "            "size": 4,\n",
+       "            "opacity": 0.6\n",
+       "          }\n",
+       "        }\n",
+       "      ],\n",
+       "      "name": "points_map",\n",
+       "      "camera": {\n",
+       "        "pitch": 0,\n",
+       "        "bearing": 0,\n",
+       "        "zoom": 12\n",
+       "      },\n",
+       "      "type": "map"\n",
+       "    },\n",
+       "    {\n",
+       "      "type": "histogram",\n",
+       "      "source": "trees",\n",
+       "      "x": {\n",
+       "        "field": "latitude"\n",
+       "      },\n",
+       "      "name": "tree_histogram",\n",
+       "      "bins": 30\n",
+       "    }\n",
+       "  ],\n",
+       "  "layout": {\n",
+       "    "type": "vertical"\n",
+       "  }\n",
+       "}
\n", + "
" + ], + "text/plain": [ + "AutarkSpec(views=[Map(layers=[Layer(source=CSV(name='trees', url='/examples/data/trees.csv', values=None, delimiter=None, geometry=LatLngGeometry(latitude='latitude', longitude='longitude', coordinate_format='EPSG:4326', type='latlng'), type='csv'), id='trees_layer', type='points', encoding=None, style_values={'color': '#228B22', 'size': 4, 'opacity': 0.6})], name='points_map', camera=Camera(pitch=0, bearing=0, zoom=12), style=None, type='map'), Histogram(source=CSV(name='trees', url='/examples/data/trees.csv', values=None, delimiter=None, geometry=LatLngGeometry(latitude='latitude', longitude='longitude', coordinate_format='EPSG:4326', type='latlng'), type='csv'), x='latitude', name='tree_histogram', y=None, bins=30, selection=None, type='histogram')], data=[CSV(name='trees', url='/examples/data/trees.csv', values=None, delimiter=None, geometry=LatLngGeometry(latitude='latitude', longitude='longitude', coordinate_format='EPSG:4326', type='latlng'), type='csv')], transforms=None, links=None, layout=Layout(type='vertical', columns=None, gap=None), metadata=Metadata(title='CSV Points with Histogram', description='Test CSV loading and histogram rendering', authors=None, created=None), workspace=Workspace(name='test_csv', coordinate_format='EPSG:4326'))" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "trees = ak.CSV(\n", + " \"trees\",\n", + " url=\"/examples/data/trees.csv\",\n", + " geometry=ak.latlng(\"latitude\", \"longitude\", coordinate_format=\"EPSG:4326\")\n", + ")\n", + "\n", + "spec2 = ak.Spec(\n", + " metadata=ak.Metadata(\n", + " title=\"CSV Points with Histogram\",\n", + " description=\"Test CSV loading and histogram rendering\"\n", + " ),\n", + " workspace=ak.Workspace(name=\"test_csv\", coordinate_format=\"EPSG:4326\"),\n", + " data=[trees],\n", + " views=[\n", + " ak.Map(\n", + " name=\"points_map\",\n", + " camera=ak.Camera(pitch=0, bearing=0, zoom=12),\n", + " layers=[\n", + " ak.Layer(trees, id=\"trees_layer\", type=\"points\")\n", + " .style(color=\"#228B22\", size=4, opacity=0.6)\n", + " ]\n", + " ),\n", + " ak.Histogram(\n", + " name=\"tree_histogram\",\n", + " source=trees,\n", + " x=\"latitude\",\n", + " bins=30\n", + " )\n", + " ],\n", + " layout=ak.Layout(type=\"vertical\")\n", + ")\n", + "\n", + "spec2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test 3: Full Spatial Join Example\n", + "\n", + "Complete workflow with spatial join, thematic mapping, and linked selection." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "
\n", + " Autark spec\n", + "
{\n",
+       "  "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",\n",
+       "  "version": "0.1",\n",
+       "  "metadata": {\n",
+       "    "title": "Spatial Join - Trees per Neighborhood",\n",
+       "    "description": "Demonstrates spatial join between neighborhoods and tree points, aggregating tree counts and visualizing on a thematic map.",\n",
+       "    "created": "2026-06-07"\n",
+       "  },\n",
+       "  "workspace": {\n",
+       "    "name": "spatial_join_demo",\n",
+       "    "coordinateFormat": "EPSG:4326"\n",
+       "  },\n",
+       "  "data": [\n",
+       "    {\n",
+       "      "name": "neighborhoods",\n",
+       "      "url": "http://localhost:8000/examples/data/neighborhoods.geojson",\n",
+       "      "layerType": "polygons",\n",
+       "      "coordinateFormat": "EPSG:4326",\n",
+       "      "type": "geojson"\n",
+       "    },\n",
+       "    {\n",
+       "      "name": "trees",\n",
+       "      "url": "http://localhost:8000/examples/data/trees.csv",\n",
+       "      "geometry": {\n",
+       "        "latitude": "latitude",\n",
+       "        "longitude": "longitude",\n",
+       "        "coordinateFormat": "EPSG:4326",\n",
+       "        "type": "latlng"\n",
+       "      },\n",
+       "      "type": "csv"\n",
+       "    }\n",
+       "  ],\n",
+       "  "transforms": [\n",
+       "    {\n",
+       "      "type": "spatialJoin",\n",
+       "      "root": "neighborhoods",\n",
+       "      "join": "trees",\n",
+       "      "groupBy": [\n",
+       "        {\n",
+       "          "column": "*",\n",
+       "          "op": "count"\n",
+       "        },\n",
+       "        {\n",
+       "          "column": "species",\n",
+       "          "op": "collect",\n",
+       "          "as": "tree_species"\n",
+       "        }\n",
+       "      ]\n",
+       "    }\n",
+       "  ],\n",
+       "  "views": [\n",
+       "    {\n",
+       "      "layers": [\n",
+       "        {\n",
+       "          "source": "neighborhoods",\n",
+       "          "id": "neighborhoods_layer",\n",
+       "          "type": "polygons",\n",
+       "          "encoding": {\n",
+       "            "color": {\n",
+       "              "field": "properties.sjoin.count.trees",\n",
+       "              "scale": {\n",
+       "                "type": "quantile",\n",
+       "                "scheme": "greens",\n",
+       "                "domainStrategy": "minMax"\n",
+       "              }\n",
+       "            }\n",
+       "          },\n",
+       "          "style": {\n",
+       "            "opacity": 0.8,\n",
+       "            "strokeColor": "#333333",\n",
+       "            "strokeWidth": 1\n",
+       "          }\n",
+       "        },\n",
+       "        {\n",
+       "          "source": "trees",\n",
+       "          "id": "trees_layer",\n",
+       "          "type": "points",\n",
+       "          "style": {\n",
+       "            "color": "#228B22",\n",
+       "            "size": 3,\n",
+       "            "opacity": 0.6\n",
+       "          }\n",
+       "        }\n",
+       "      ],\n",
+       "      "name": "neighborhood_map",\n",
+       "      "camera": {\n",
+       "        "pitch": 0,\n",
+       "        "bearing": 0,\n",
+       "        "zoom": 12\n",
+       "      },\n",
+       "      "type": "map"\n",
+       "    },\n",
+       "    {\n",
+       "      "type": "histogram",\n",
+       "      "source": "neighborhoods",\n",
+       "      "x": {\n",
+       "        "field": "sjoin.count.trees"\n",
+       "      },\n",
+       "      "name": "tree_count_histogram",\n",
+       "      "bins": 20,\n",
+       "      "selection": {\n",
+       "        "name": "tree_count_brush",\n",
+       "        "type": "interval"\n",
+       "      }\n",
+       "    }\n",
+       "  ],\n",
+       "  "links": [\n",
+       "    {\n",
+       "      "selection": "tree_count_brush",\n",
+       "      "target": "neighborhoods_layer",\n",
+       "      "action": "highlight"\n",
+       "    }\n",
+       "  ],\n",
+       "  "layout": {\n",
+       "    "type": "vertical"\n",
+       "  }\n",
+       "}
\n", + "
" + ], + "text/plain": [ + "AutarkSpec(views=[Map(layers=[Layer(source='neighborhoods', id='neighborhoods_layer', type='polygons', encoding=Encoding(color=Field(field='properties.sjoin.count.trees', scale=Scale(type='quantile', scheme='greens', domain=None, domain_strategy='minMax')), opacity=None, size=None, height=None), style_values={'opacity': 0.8, 'strokeColor': '#333333', 'strokeWidth': 1}), Layer(source=CSV(name='trees', url='/examples/data/trees.csv', values=None, delimiter=None, geometry=LatLngGeometry(latitude='latitude', longitude='longitude', coordinate_format='EPSG:4326', type='latlng'), type='csv'), id='trees_layer', type='points', encoding=None, style_values={'color': '#228B22', 'size': 3, 'opacity': 0.6})], name='neighborhood_map', camera=Camera(pitch=0, bearing=0, zoom=12), style=None, type='map'), Histogram(source=GeoJSON(name='neighborhoods', url='/examples/data/neighborhoods.geojson', values=None, layer_type='polygons', coordinate_format='EPSG:4326', type='geojson'), x='sjoin.count.trees', name='tree_count_histogram', y=None, bins=20, selection=Selection(name='tree_count_brush', type='interval', fields=None), type='histogram')], data=[GeoJSON(name='neighborhoods', url='/examples/data/neighborhoods.geojson', values=None, layer_type='polygons', coordinate_format='EPSG:4326', type='geojson'), CSV(name='trees', url='/examples/data/trees.csv', values=None, delimiter=None, geometry=LatLngGeometry(latitude='latitude', longitude='longitude', coordinate_format='EPSG:4326', type='latlng'), type='csv')], transforms=[SpatialJoin(root=GeoJSON(name='neighborhoods', url='/examples/data/neighborhoods.geojson', values=None, layer_type='polygons', coordinate_format='EPSG:4326', type='geojson'), join=CSV(name='trees', url='/examples/data/trees.csv', values=None, delimiter=None, geometry=LatLngGeometry(latitude='latitude', longitude='longitude', coordinate_format='EPSG:4326', type='latlng'), type='csv'), near=None, group_by=[Aggregation(column='*', op='count', as_=None, normalize=None), Aggregation(column='species', op='collect', as_='tree_species', normalize=None)], type='spatialJoin')], links=[Link(selection=Selection(name='tree_count_brush', type='interval', fields=None), target='neighborhoods_layer', action='highlight')], layout=Layout(type='vertical', columns=None, gap=None), metadata=Metadata(title='Spatial Join - Trees per Neighborhood', description='Demonstrates spatial join between neighborhoods and tree points, aggregating tree counts and visualizing on a thematic map.', authors=None, created='2026-06-07'), workspace=Workspace(name='spatial_join_demo', coordinate_format='EPSG:4326'))" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Import the working spatial join example\n", + "from spatial_join import build_spec\n", + "\n", + "spec3 = build_spec()\n", + "spec3" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b04ae885d20e45f19f56d8042b241078", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + ".AutarkWidget object at 0x10f2a55e0>" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import display\n", + "from osmnx_autark_workflow import build_spec\n", + "\n", + "spec = build_spec(distance=500)\n", + "\n", + "w = spec.widget(height=\"700px\")\n", + "spec.save_html(\"/tmp/osmnx_autark.html\")\n", + "display(w)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Module reloaded\n" + ] + } + ], + "source": [ + "# Force reload the autark module\n", + "import sys\n", + "if 'autark' in sys.modules:\n", + " del sys.modules['autark']\n", + " del sys.modules['autark.spec']\n", + " del sys.modules['autark.display']\n", + " del sys.modules['autark.data']\n", + " del sys.modules['autark.views']\n", + " del sys.modules['autark.encodings']\n", + " del sys.modules['autark.transforms']\n", + " del sys.modules['autark.selections']\n", + " del sys.modules['autark.links']\n", + " del sys.modules['autark._serialise']\n", + "\n", + "# Now import fresh\n", + "from pathlib import Path\n", + "sys.path.insert(0, str(Path.cwd().parent))\n", + "import autark as ak\n", + "\n", + "print(\"✅ Module reloaded\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test 4: Check JSON Output\n", + "\n", + "Verify the spec serializes correctly to JSON." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"$schema\": \"https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json\",\n", + " \"version\": \"0.1\",\n", + " \"metadata\": {\n", + " \"title\": \"Simple GeoJSON Map\",\n", + " \"description\": \"Basic test of GeoJSON rendering\"\n", + " },\n", + " \"workspace\": {\n", + " \"name\": \"test_geojson\",\n", + " \"coordinateFormat\": \"EPSG:4326\"\n", + " },\n", + " \"data\": [\n", + " {\n", + " \"name\": \"neighborhoods\",\n", + " \"url\": \"/examples/data/neighborhoods.geojson\",\n", + " \"layerType\": \"polygons\",\n", + " \"coordinateFormat\": \"EPSG:4326\",\n", + " \"type\": \"geojson\"\n", + " }\n", + " ],\n", + " \"views\": [\n", + " {\n", + " \"layers\": [\n", + " {\n", + " \"source\": \"neighborhoods\",\n", + " \"id\": \"neighborhoods_layer\",\n", + " \"type\": \"polygons\",\n", + " \"style\": {\n", + " \"color\": \"#4a90e2\",\n", + " \"opacity\": 0.7\n", + " }\n", + " }\n", + " ],\n", + " \"name\": \"simple_map\",\n", + " \"camera\": {\n", + " \"pitch\": 0,\n", + " \"bearing\": 0,\n", + " \"zoom\": 12\n", + " },\n", + " \"type\": \"map\"\n", + " }\n", + " ]\n", + "}\n" + ] + } + ], + "source": [ + "# Print the JSON to verify structure\n", + "import json\n", + "print(json.dumps(spec1.to_dict(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test 5: Export to HTML\n", + "\n", + "Test the `save_html()` method." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "HTML saved to: /tmp/autark_test.html\n", + "File exists: True\n", + "File size: 2725 bytes\n" + ] + } + ], + "source": [ + "# Save to HTML file\n", + "output_path = Path(\"/tmp/autark_test.html\")\n", + "spec1.save_html(output_path)\n", + "print(f\"HTML saved to: {output_path}\")\n", + "print(f\"File exists: {output_path.exists()}\")\n", + "print(f\"File size: {output_path.stat().st_size} bytes\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test 6: Zero-Server Widget (anywidget)\n", + "\n", + "`spec.widget()` renders with the runtime bundled inside the Python package -\n", + "no `cors_server.py` needed. DuckDB assets load from the jsDelivr CDN on first\n", + "use (network required). Data must use absolute URLs or inline `values`.\n", + "\n", + "Requires: `pip install anywidget` (or `pip install -e \".[widget]\"`).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "ename": "RuntimeError", + "evalue": "The Autark widget requires anywidget. Install it with: pip install autark[widget]", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mModuleNotFoundError\u001b[39m Traceback (most recent call last)", + "\u001b[36mFile \u001b[39m\u001b[32m~/src/autark/python/autark/widget.py:28\u001b[39m, in \u001b[36m_build_widget_class\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m 27\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m28\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01manywidget\u001b[39;00m\n\u001b[32m 29\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mtraitlets\u001b[39;00m\n", + "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'anywidget'", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[31mRuntimeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 36\u001b[39m\n\u001b[32m 2\u001b[39m inline = ak.GeoJSON(\n\u001b[32m 3\u001b[39m \u001b[33m\"\u001b[39m\u001b[33minline_polys\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 4\u001b[39m values={\n\u001b[32m (...)\u001b[39m\u001b[32m 22\u001b[39m layer_type=\u001b[33m\"\u001b[39m\u001b[33mpolygons\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 23\u001b[39m )\n\u001b[32m 25\u001b[39m widget_spec = ak.Spec(\n\u001b[32m 26\u001b[39m workspace=ak.Workspace(coordinate_format=\u001b[33m\"\u001b[39m\u001b[33mEPSG:4326\u001b[39m\u001b[33m\"\u001b[39m),\n\u001b[32m 27\u001b[39m data=[inline],\n\u001b[32m (...)\u001b[39m\u001b[32m 33\u001b[39m ],\n\u001b[32m 34\u001b[39m )\n\u001b[32m---> \u001b[39m\u001b[32m36\u001b[39m \u001b[43mwidget_spec\u001b[49m\u001b[43m.\u001b[49m\u001b[43mwidget\u001b[49m\u001b[43m(\u001b[49m\u001b[43mheight\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43m400px\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/src/autark/python/autark/spec.py:105\u001b[39m, in \u001b[36mAutarkSpec.widget\u001b[39m\u001b[34m(self, height)\u001b[39m\n\u001b[32m 97\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Render the spec as a Jupyter widget using the bundled runtime.\u001b[39;00m\n\u001b[32m 98\u001b[39m \n\u001b[32m 99\u001b[39m \u001b[33;03mRequires the ``widget`` extra (``pip install autark[widget]``). Works\u001b[39;00m\n\u001b[32m 100\u001b[39m \u001b[33;03mwithout a local server; DuckDB assets load from the jsDelivr CDN.\u001b[39;00m\n\u001b[32m 101\u001b[39m \u001b[33;03mData sources must use absolute URLs or inline values.\u001b[39;00m\n\u001b[32m 102\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 103\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01mwidget\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m create_widget\n\u001b[32m--> \u001b[39m\u001b[32m105\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mcreate_widget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mheight\u001b[49m\u001b[43m=\u001b[49m\u001b[43mheight\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/src/autark/python/autark/widget.py:76\u001b[39m, in \u001b[36mcreate_widget\u001b[39m\u001b[34m(spec, height)\u001b[39m\n\u001b[32m 74\u001b[39m \u001b[38;5;28;01mglobal\u001b[39;00m _widget_class\n\u001b[32m 75\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m _widget_class \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m76\u001b[39m _widget_class = \u001b[43m_build_widget_class\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 78\u001b[39m spec_dict = spec.to_dict() \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(spec, \u001b[33m\"\u001b[39m\u001b[33mto_dict\u001b[39m\u001b[33m\"\u001b[39m) \u001b[38;5;28;01melse\u001b[39;00m spec\n\u001b[32m 79\u001b[39m _warn_on_relative_urls(spec_dict)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/src/autark/python/autark/widget.py:31\u001b[39m, in \u001b[36m_build_widget_class\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m 29\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mtraitlets\u001b[39;00m\n\u001b[32m 30\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mModuleNotFoundError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m---> \u001b[39m\u001b[32m31\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\n\u001b[32m 32\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mThe Autark widget requires anywidget. Install it with: pip install autark[widget]\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 33\u001b[39m ) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mexc\u001b[39;00m\n\u001b[32m 35\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m _WIDGET_ESM.exists():\n\u001b[32m 36\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\n\u001b[32m 37\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mBundled widget runtime not found: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m_WIDGET_ESM\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m. \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 38\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mBuild it with: cd autk-runtime && npm run build:widget\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 39\u001b[39m )\n", + "\u001b[31mRuntimeError\u001b[39m: The Autark widget requires anywidget. Install it with: pip install autark[widget]" + ] + } + ], + "source": [ + "# Widget test: inline data, no local server required\n", + "inline = ak.GeoJSON(\n", + " \"inline_polys\",\n", + " values={\n", + " \"type\": \"FeatureCollection\",\n", + " \"features\": [\n", + " {\n", + " \"type\": \"Feature\",\n", + " \"properties\": {\"name\": \"a\"},\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [[\n", + " [-74.01, 40.70], [-74.00, 40.70],\n", + " [-74.00, 40.71], [-74.01, 40.71],\n", + " [-74.01, 40.70],\n", + " ]],\n", + " },\n", + " }\n", + " ],\n", + " },\n", + " coordinate_format=\"EPSG:4326\",\n", + " layer_type=\"polygons\",\n", + ")\n", + "\n", + "widget_spec = ak.Spec(\n", + " workspace=ak.Workspace(coordinate_format=\"EPSG:4326\"),\n", + " data=[inline],\n", + " views=[\n", + " ak.Map(\n", + " camera=ak.Camera(zoom=13),\n", + " layers=[ak.Layer(inline, type=\"polygons\").style(color=\"#e67e22\", opacity=0.8)],\n", + " )\n", + " ],\n", + ")\n", + "\n", + "widget_spec.widget(height=\"400px\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Expected Results\n", + "\n", + "For each spec display above, you should see:\n", + "\n", + "1. **Interactive visualization** - A rendered map/plot using the TypeScript runtime\n", + "2. **Collapsible spec details** - A `
` element showing the JSON spec\n", + "3. **No console errors** - Check browser console for any JavaScript errors\n", + "\n", + "### Known Issues to Watch For:\n", + "\n", + "1. **Runtime URL** - Default is `/autk-runtime/dist/autk-runtime.js`. May need to adjust based on how you're serving the runtime.\n", + "2. **Data URLs** - Relative paths like `/examples/data/...` need proper server setup.\n", + "3. **CORS issues** - If loading from local files without a server.\n", + "\n", + "### Debugging Tips:\n", + "\n", + "If the visualization doesn't appear:\n", + "- Check the browser console for errors\n", + "- Inspect the HTML to verify the script tag is present\n", + "- Verify the runtime module URL is accessible\n", + "- Check that data file URLs are accessible" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (Autark OSMnx)", + "language": "python", + "name": "autark-osmnx" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 00000000..06ed790b --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,69 @@ +[build-system] +requires = ["hatchling>=1.24", "hatch-jupyter-builder>=0.5.0"] +build-backend = "hatchling.build" + +[project] +name = "autark" +version = "0.1.0a0" +description = "Declarative Python API for urban visual analytics (maps, plots, spatial joins) rendered with WebGPU and DuckDB-WASM" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [ + { name = "Urban Toolkit" } +] +keywords = ["visualization", "maps", "gis", "urban", "duckdb", "webgpu", "jupyter", "declarative"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Framework :: Jupyter", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Scientific/Engineering :: GIS", +] +dependencies = [] + +[project.urls] +Homepage = "https://github.com/urban-toolkit/autark" +Repository = "https://github.com/urban-toolkit/autark" +Issues = "https://github.com/urban-toolkit/autark/issues" + +[project.optional-dependencies] +validation = ["jsonschema>=4.0"] +widget = ["anywidget>=0.9"] +test = ["jsonschema>=4.0"] +dev = ["jsonschema>=4.0", "anywidget>=0.9", "mypy>=1.8", "nbformat>=5.0", "nbclient>=0.7"] + +[tool.hatch.build] +only-packages = true +artifacts = ["autark/static/autark-widget.js"] + +[tool.hatch.build.targets.wheel] +packages = ["autark"] + +[tool.hatch.build.targets.sdist] +include = ["autark", "README.md", "pyproject.toml"] + +[tool.hatch.build.hooks.jupyter-builder] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = ["autark/static/autark-widget.js"] +skip-if-exists = ["autark/static/autark-widget.js"] +dependencies = ["hatch-jupyter-builder>=0.5.0"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] +path = "../autk-runtime" +npm = "npm" +build_cmd = "build:widget" + +[tool.mypy] +python_version = "3.10" +strict = true +files = ["autark"] +# Notebook/display integrations return third-party objects. +[[tool.mypy.overrides]] +module = ["anywidget", "traitlets", "jsonschema"] +ignore_missing_imports = true diff --git a/python/tests/test_dataframe_sources.py b/python/tests/test_dataframe_sources.py new file mode 100644 index 00000000..53481c3a --- /dev/null +++ b/python/tests/test_dataframe_sources.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import json +import unittest +from typing import Any + +import autark as ak +from autark.data import _jsonable + + +class _NumpyLikeScalar: + """Mimics a numpy scalar: not JSON serializable, exposes .item().""" + + def __init__(self, value: Any) -> None: + self._value = value + + def item(self) -> Any: + return self._value + + +class _FakeCrs: + def __init__(self, epsg: int) -> None: + self._epsg = epsg + + def to_epsg(self) -> int: + return self._epsg + + +class _FakeGeoDataFrame: + """Duck-typed GeoDataFrame: __geo_interface__ + optional crs/to_crs.""" + + def __init__(self, features: list[dict[str, Any]], crs: _FakeCrs | None = None) -> None: + self._features = features + self.crs = crs + self.reprojected_to: int | None = None + + @property + def __geo_interface__(self) -> dict[str, Any]: + return {"type": "FeatureCollection", "features": self._features} + + def to_crs(self, epsg: int) -> "_FakeGeoDataFrame": + out = _FakeGeoDataFrame(self._features, crs=_FakeCrs(epsg)) + out.reprojected_to = epsg + return out + + +class _FakeDataFrame: + """Duck-typed DataFrame: to_dict('records').""" + + def __init__(self, records: list[dict[str, Any]]) -> None: + self._records = records + + def to_dict(self, orient: str) -> list[dict[str, Any]]: + assert orient == "records" + return self._records + + +POLYGON_FEATURE = { + "type": "Feature", + "properties": {"name": "a", "score": _NumpyLikeScalar(7)}, + "geometry": { + "type": "Polygon", + "coordinates": (((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)),), + }, +} + + +class JsonableTests(unittest.TestCase): + def test_unwraps_numpy_like_scalars_and_tuples(self) -> None: + out = _jsonable({"a": _NumpyLikeScalar(3), "b": (1.0, 2.0)}) + self.assertEqual(out, {"a": 3, "b": [1.0, 2.0]}) + json.dumps(out) # must be JSON serializable + + def test_non_finite_floats_become_null(self) -> None: + self.assertIsNone(_jsonable(float("nan"))) + self.assertIsNone(_jsonable(float("inf"))) + + +class FromGeopandasTests(unittest.TestCase): + def test_builds_inline_source_and_infers_layer_type(self) -> None: + gdf = _FakeGeoDataFrame([POLYGON_FEATURE]) + source = ak.GeoJSON.from_geopandas("neigh", gdf) + assert source.values is not None + self.assertEqual(source.layer_type, "polygons") + feature = source.values["features"][0] + self.assertEqual(feature["properties"], {"name": "a", "score": 7}) + json.dumps(source.to_dict()) # spec fragment must serialize + + def test_reprojects_when_crs_differs(self) -> None: + gdf = _FakeGeoDataFrame([POLYGON_FEATURE], crs=_FakeCrs(3857)) + source = ak.GeoJSON.from_geopandas("neigh", gdf) + self.assertEqual(source.coordinate_format, "EPSG:4326") + + def test_keeps_crs_when_already_wgs84(self) -> None: + gdf = _FakeGeoDataFrame([POLYGON_FEATURE], crs=_FakeCrs(4326)) + source = ak.GeoJSON.from_geopandas("neigh", gdf) + self.assertEqual(source.coordinate_format, "EPSG:4326") + + def test_rejects_objects_without_geo_interface(self) -> None: + with self.assertRaisesRegex(TypeError, "__geo_interface__"): + ak.GeoJSON.from_geopandas("bad", object()) + + def test_mixed_geometries_require_explicit_layer_type(self) -> None: + point_feature = { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Point", "coordinates": (0.0, 0.0)}, + } + gdf = _FakeGeoDataFrame([POLYGON_FEATURE, point_feature]) + with self.assertRaisesRegex(ValueError, "layer_type"): + ak.GeoJSON.from_geopandas("mixed", gdf) + source = ak.GeoJSON.from_geopandas("mixed", gdf, layer_type="polygons") + self.assertEqual(source.layer_type, "polygons") + + +class FromDataframeTests(unittest.TestCase): + def test_builds_point_features_from_records(self) -> None: + df = _FakeDataFrame( + [ + {"lat": 40.7, "lon": -74.0, "species": "oak", "height": _NumpyLikeScalar(12)}, + {"lat": 40.8, "lon": -74.1, "species": "elm", "height": float("nan")}, + ] + ) + source = ak.GeoJSON.from_dataframe("trees", df, latitude="lat", longitude="lon") + assert source.values is not None + features = source.values["features"] + self.assertEqual(len(features), 2) + self.assertEqual(features[0]["geometry"], {"type": "Point", "coordinates": [-74.0, 40.7]}) + self.assertEqual(features[0]["properties"], {"species": "oak", "height": 12}) + self.assertIsNone(features[1]["properties"]["height"]) # NaN -> null + self.assertEqual(source.layer_type, "points") + json.dumps(source.to_dict()) + + def test_respects_explicit_property_selection(self) -> None: + df = _FakeDataFrame([{"lat": 1.0, "lon": 2.0, "keep": "yes", "drop": "no"}]) + source = ak.GeoJSON.from_dataframe( + "pts", df, latitude="lat", longitude="lon", properties=["keep"] + ) + assert source.values is not None + self.assertEqual(source.values["features"][0]["properties"], {"keep": "yes"}) + + def test_missing_coordinate_column_raises(self) -> None: + df = _FakeDataFrame([{"lat": 1.0}]) + with self.assertRaises(KeyError): + ak.GeoJSON.from_dataframe("pts", df, latitude="lat", longitude="lon") + + +class RealPandasTests(unittest.TestCase): + def test_round_trip_with_real_pandas(self) -> None: + try: + import pandas as pd + except ModuleNotFoundError: + self.skipTest("pandas not installed") + df = pd.DataFrame({"lat": [40.7], "lon": [-74.0], "species": ["oak"]}) + source = ak.GeoJSON.from_dataframe("trees", df, latitude="lat", longitude="lon") + assert source.values is not None + self.assertEqual( + source.values["features"][0]["geometry"]["coordinates"], [-74.0, 40.7] + ) + json.dumps(source.to_dict()) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/tests/test_display.py b/python/tests/test_display.py new file mode 100644 index 00000000..37279d90 --- /dev/null +++ b/python/tests/test_display.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import unittest + +import autark as ak +from autark.display import _resolve_data_urls +from autark.widget import _WIDGET_ESM + + +def _inline_spec() -> ak.AutarkSpec: + inline = ak.GeoJSON( + "inline_polys", + values={"type": "FeatureCollection", "features": []}, + coordinate_format="EPSG:4326", + layer_type="polygons", + ) + return ak.Spec( + metadata=ak.Metadata(title="Display test"), + workspace=ak.Workspace(coordinate_format="EPSG:4326"), + data=[inline], + views=[ak.Map(layers=[ak.Layer(inline, type="polygons")])], + ) + + +class ResolveDataUrlsTests(unittest.TestCase): + def test_rewrites_root_relative_urls_against_runtime_origin(self) -> None: + spec = {"data": [{"name": "n", "url": "/examples/data/x.geojson"}]} + out = _resolve_data_urls(spec, "http://localhost:8000/runtime.js") + self.assertEqual(out["data"][0]["url"], "http://localhost:8000/examples/data/x.geojson") + # Original is not mutated. + self.assertEqual(spec["data"][0]["url"], "/examples/data/x.geojson") + + def test_leaves_absolute_urls_and_inline_values_alone(self) -> None: + spec = {"data": [{"name": "a", "url": "https://example.com/x.csv"}, {"name": "b", "values": []}]} + out = _resolve_data_urls(spec, "http://localhost:8000/runtime.js") + self.assertEqual(out["data"][0]["url"], "https://example.com/x.csv") + self.assertNotIn("url", out["data"][1]) + + +class EmbeddedHtmlTests(unittest.TestCase): + def setUp(self) -> None: + if not _WIDGET_ESM.exists(): + self.skipTest("widget bundle not built (cd autk-runtime && npm run build:widget)") + + def test_embeds_runtime_and_spec(self) -> None: + html = ak.to_embedded_html(_inline_spec()) + self.assertIn("", html) + self.assertIn("Display test", html) + self.assertIn("inline_polys", html) + # Runtime is inlined, not loaded from a dev server. + self.assertNotIn("autk-runtime/dist/autk-runtime.js", html) + # The bundle is larger than 1MB; the document must contain it. + self.assertGreater(len(html), 1_000_000) + + def test_script_content_is_script_safe(self) -> None: + html = ak.to_embedded_html(_inline_spec()) + body = html.split("", body.rsplit("", 1)[0]) + + def test_save_html_defaults_to_embedded(self) -> None: + import tempfile + from pathlib import Path + + with tempfile.TemporaryDirectory() as tmp: + out = Path(tmp) / "viz.html" + _inline_spec().save_html(out) + content = out.read_text(encoding="utf-8") + self.assertIn("", content) + self.assertNotIn("autk-runtime/dist/autk-runtime.js", content) + + def test_save_html_with_runtime_url_uses_dev_server_mode(self) -> None: + import tempfile + from pathlib import Path + + with tempfile.TemporaryDirectory() as tmp: + out = Path(tmp) / "viz.html" + _inline_spec().save_html(out, runtime_url="http://localhost:8000/autk-runtime/dist/autk-runtime.js") + content = out.read_text(encoding="utf-8") + self.assertIn("autk-runtime/dist/autk-runtime.js", content) + self.assertLess(len(content), 100_000) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/tests/test_notebooks.py b/python/tests/test_notebooks.py new file mode 100644 index 00000000..87df005a --- /dev/null +++ b/python/tests/test_notebooks.py @@ -0,0 +1,43 @@ +"""Executes the example notebooks end to end (kernel side only). + +Verifies every cell runs without raising: spec construction, serialization, +HTML generation, and widget instantiation. Browser-side rendering is covered +separately by the Playwright suite. +""" + +from __future__ import annotations + +import unittest +from pathlib import Path + +EXAMPLES_DIR = Path(__file__).resolve().parents[1] / "examples" + + +class NotebookExecutionTests(unittest.TestCase): + def _execute(self, notebook_name: str) -> None: + try: + import nbformat + from nbclient import NotebookClient + except ModuleNotFoundError: + self.skipTest("nbformat/nbclient not installed") + try: + import anywidget # noqa: F401 + except ModuleNotFoundError: + self.skipTest("anywidget not installed (widget cells would fail)") + + path = EXAMPLES_DIR / notebook_name + nb = nbformat.read(path, as_version=4) + client = NotebookClient( + nb, + timeout=120, + kernel_name="python3", + resources={"metadata": {"path": str(EXAMPLES_DIR)}}, + ) + client.execute() + + def test_jupyter_integration_notebook_executes(self) -> None: + self._execute("test_jupyter_integration.ipynb") + + +if __name__ == "__main__": + unittest.main() diff --git a/python/tests/test_spec_builders.py b/python/tests/test_spec_builders.py new file mode 100644 index 00000000..05cf4abf --- /dev/null +++ b/python/tests/test_spec_builders.py @@ -0,0 +1,315 @@ +from __future__ import annotations + +import json +import unittest +from pathlib import Path + +import autark as ak +from examples.spatial_join import build_spec + +ROOT = Path(__file__).resolve().parents[2] + + +class SpecBuilderTests(unittest.TestCase): + def test_builds_spatial_join_example(self) -> None: + expected = json.loads((ROOT / "examples/specs/03-spatial-join.json").read_text()) + self.assertEqual(build_spec().to_dict(), expected) + + def test_validates_against_current_schema_when_jsonschema_is_installed(self) -> None: + try: + import jsonschema # noqa: F401 + except ModuleNotFoundError: + self.skipTest("jsonschema is not installed") + build_spec().validate(ROOT / "schema/autark-spec-v0.1.json") + + def test_data_sources_require_url_or_values(self) -> None: + with self.assertRaisesRegex(ValueError, "exactly one"): + ak.GeoJSON("bad") + with self.assertRaisesRegex(ValueError, "exactly one"): + ak.CSV("bad", url="a.csv", values=[["x"]]) + with self.assertRaisesRegex(ValueError, "exactly one"): + ak.JSON("bad") + with self.assertRaisesRegex(ValueError, "exactly one"): + ak.JSON("bad", url="a.json", values=[{"id": 1}]) + + def test_json_and_geotiff_builders(self) -> None: + records = ak.JSON( + "events", + values=[{"id": 1, "lat": 40.7, "lon": -74.0}], + geometry=ak.latlng("lat", "lon", coordinate_format="EPSG:4326"), + ) + raster = ak.GeoTIFF( + "lst", + url="data/lst.tif", + coordinate_format="EPSG:4326", + max_pixels=1_000_000, + ) + + self.assertEqual( + records.to_dict(), + { + "name": "events", + "values": [{"id": 1, "lat": 40.7, "lon": -74.0}], + "geometry": { + "latitude": "lat", + "longitude": "lon", + "coordinateFormat": "EPSG:4326", + "type": "latlng", + }, + "type": "json", + }, + ) + self.assertEqual( + ak.JSON("events", url="data/events.json").to_dict(), + { + "name": "events", + "url": "data/events.json", + "type": "json", + }, + ) + self.assertEqual( + raster.to_dict(), + { + "name": "lst", + "url": "data/lst.tif", + "coordinateFormat": "EPSG:4326", + "maxPixels": 1_000_000, + "type": "geotiff", + }, + ) + + def test_osm_layer_source_uses_locked_table_naming(self) -> None: + osm = ak.OSM("manhattan_osm", area="Manhattan, New York", layers=["buildings", "roads"]) + self.assertEqual(osm.layer_source("buildings"), "manhattan_osm_buildings") + self.assertEqual(ak.Layer(osm.layer_source("roads")).to_dict(), {"source": "manhattan_osm_roads"}) + + def test_osm_serializes_explicit_geocode_area_and_relation_areas(self) -> None: + osm = ak.OSM( + "nyc_osm", + area="Battery Park City", + geocode_area="New York", + areas=["Battery Park City", "Financial District"], + layers=["buildings", "roads"], + ) + self.assertEqual( + osm.to_dict(), + { + "name": "nyc_osm", + "area": "Battery Park City", + "layers": ["buildings", "roads"], + "geocodeArea": "New York", + "areas": ["Battery Park City", "Financial District"], + "type": "osm", + }, + ) + + def test_scatterplot_and_table_builders(self) -> None: + points = ak.GeoJSON( + "points", + values={ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"id": 1, "name": "A", "value": 10}, + "geometry": {"type": "Point", "coordinates": [0, 0]}, + } + ], + }, + layer_type="points", + ) + selection = ak.interval("scatter_brush") + spec = ak.Spec( + data=[points], + views=[ + ak.Scatterplot( + points, + x="id", + y="value", + color="name", + selection=selection, + ), + ak.Table( + points, + columns=["id", "name", "value"], + sort={"column": "value", "direction": "desc"}, + ), + ], + ) + + views = spec.to_dict()["views"] + self.assertEqual( + views[0], + { + "type": "scatterplot", + "source": "points", + "x": {"field": "id"}, + "y": {"field": "value"}, + "color": {"field": "name"}, + "selection": {"name": "scatter_brush", "type": "interval"}, + }, + ) + self.assertEqual( + views[1], + { + "type": "table", + "source": "points", + "columns": [{"field": "id"}, {"field": "name"}, {"field": "value"}], + "sort": {"column": "value", "direction": "desc"}, + }, + ) + + def test_table_requires_columns(self) -> None: + with self.assertRaisesRegex(ValueError, "at least one column"): + ak.Table("points", columns=[]) + + def test_heatmap_builder(self) -> None: + points = ak.GeoJSON("points", url="points.geojson", layer_type="points") + heatmap = ak.Heatmap( + points, + output="points_heatmap", + near=ak.Near(250, use_centroid=True), + grid=ak.HeatmapGrid(rows=32, columns=24), + group_by=[ak.count("*", as_="point_count")], + ) + + self.assertEqual( + heatmap.to_dict(), + { + "type": "heatmap", + "source": "points", + "output": "points_heatmap", + "near": {"distance": 250, "useCentroid": True}, + "grid": {"rows": 32, "columns": 24}, + "groupBy": [ + { + "column": "*", + "op": "count", + "as": "point_count", + } + ], + }, + ) + with self.assertRaisesRegex(ValueError, "does not support collect"): + ak.Heatmap( + points, + output="bad_heatmap", + near=ak.Near(250), + grid=ak.HeatmapGrid(rows=32, columns=24), + group_by=[ak.collect("name")], + ) + + def test_compute_builders(self) -> None: + points = ak.GeoJSON("points", url="points.geojson", layer_type="points") + + gpgpu = ak.Compute.gpgpu( + points, + output="computed_points", + layer_type="points", + variable_mapping={"value": "value"}, + uniforms={"scale": 2}, + wgsl_body="return value * scale;", + result_field="score", + ) + render = ak.Compute.render( + output="visibility", + layers=[ + { + "id": "buildings", + "source": "buildings", + "type": "buildings", + "objectIdProperty": "id", + } + ], + viewpoints={ + "source": "points", + "strategy": {"type": "centroid"}, + "sampling": {"directions": 8}, + }, + aggregation={"type": "classes", "includeBackground": False}, + tile_size=64, + ) + + self.assertEqual( + gpgpu.to_dict(), + { + "type": "gpgpuCompute", + "source": "points", + "output": "computed_points", + "variableMapping": {"value": "value"}, + "wgslBody": "return value * scale;", + "layerType": "points", + "uniforms": {"scale": 2}, + "resultField": "score", + }, + ) + self.assertEqual( + render.to_dict(), + { + "type": "renderCompute", + "output": "visibility", + "layers": [ + { + "id": "buildings", + "source": "buildings", + "type": "buildings", + "objectIdProperty": "id", + } + ], + "viewpoints": { + "source": "points", + "strategy": {"type": "centroid"}, + "sampling": {"directions": 8}, + }, + "aggregation": {"type": "classes", "includeBackground": False}, + "tileSize": 64, + }, + ) + with self.assertRaisesRegex(ValueError, "exactly one"): + ak.Compute.gpgpu( + points, + output="bad_compute", + variable_mapping={"value": "value"}, + wgsl_body="return value;", + ) + with self.assertRaisesRegex(ValueError, "multiple of 8"): + ak.Compute.render( + output="bad_render", + layers=[{"id": "points", "source": "points", "type": "points"}], + viewpoints={"source": "points"}, + aggregation={"type": "objects"}, + tile_size=10, + ) + + def test_transform_builders_validate_against_current_schema(self) -> None: + try: + import jsonschema # noqa: F401 + except ModuleNotFoundError: + self.skipTest("jsonschema is not installed") + + points = ak.GeoJSON("points", url="points.geojson", layer_type="points") + spec = ak.Spec( + data=[points], + transforms=[ + ak.Heatmap( + points, + output="points_heatmap", + near=ak.Near(250), + grid={"rows": 16, "columns": 16}, + group_by=[ak.count("*", as_="point_count")], + ), + ak.Compute.gpgpu( + points, + output="computed_points", + variable_mapping={"value": "value"}, + wgsl_body="return value;", + output_columns=["score"], + ), + ], + views=[ak.Table("computed_points", columns=["score"])], + ) + spec.validate(ROOT / "schema/autark-spec-v0.1.json") + + +if __name__ == "__main__": + unittest.main() diff --git a/schema/autark-spec-v0.1.json b/schema/autark-spec-v0.1.json new file mode 100644 index 00000000..ba5756b3 --- /dev/null +++ b/schema/autark-spec-v0.1.json @@ -0,0 +1,1197 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "title": "AutarkSpec", + "description": "Declarative specification for Autark urban visual analytics applications (v0.1 MVP)", + "type": "object", + "required": ["$schema", "version", "views"], + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "const": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "description": "JSON Schema reference" + }, + "version": { + "type": "string", + "const": "0.1", + "description": "AutarkSpec version" + }, + "metadata": { + "$ref": "#/definitions/Metadata" + }, + "workspace": { + "$ref": "#/definitions/Workspace" + }, + "data": { + "type": "array", + "description": "Data source definitions", + "items": { + "$ref": "#/definitions/DataSource" + } + }, + "transforms": { + "type": "array", + "description": "Data transformation operations", + "items": { + "$ref": "#/definitions/Transform" + } + }, + "views": { + "type": "array", + "description": "Visual view definitions (maps, plots)", + "items": { + "$ref": "#/definitions/View" + }, + "minItems": 1 + }, + "links": { + "type": "array", + "description": "Cross-view interaction links", + "items": { + "$ref": "#/definitions/Link" + } + }, + "layout": { + "$ref": "#/definitions/Layout" + } + }, + "definitions": { + "Identifier": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", + "description": "Valid SQL-safe identifier (letters, digits, underscores; must start with letter or underscore)" + }, + "Metadata": { + "type": "object", + "description": "Optional metadata about the specification", + "additionalProperties": false, + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "authors": { + "type": "array", + "items": { + "type": "string" + } + }, + "created": { + "type": "string", + "description": "Creation date (ISO 8601 format recommended)" + } + } + }, + "Workspace": { + "type": "object", + "description": "Workspace configuration for database and coordinate system", + "additionalProperties": false, + "properties": { + "name": { + "$ref": "#/definitions/Identifier", + "description": "Workspace name (used as DuckDB schema name)", + "default": "autk" + }, + "coordinateFormat": { + "type": "string", + "description": "Default coordinate reference system", + "default": "EPSG:4326" + } + } + }, + "DataSource": { + "oneOf": [ + {"$ref": "#/definitions/OsmDataSource"}, + {"$ref": "#/definitions/GeoJsonDataSource"}, + {"$ref": "#/definitions/CsvDataSource"}, + {"$ref": "#/definitions/JsonDataSource"}, + {"$ref": "#/definitions/GeoTiffDataSource"} + ] + }, + "OsmDataSource": { + "type": "object", + "description": "OpenStreetMap data source via Overpass API or PBF file", + "required": ["type", "name", "area", "layers"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "osm" + }, + "name": { + "$ref": "#/definitions/Identifier", + "description": "Data source name (generates tables as ${name}_${layer})" + }, + "area": { + "type": "string", + "description": "Place name for Overpass query. Used as the relation area by default and as geocodeArea when geocodeArea is omitted." + }, + "geocodeArea": { + "type": "string", + "description": "Parent geocoding area for Overpass area lookup (e.g., 'New York'). Defaults to area." + }, + "areas": { + "type": "array", + "description": "One or more OSM relation area names inside geocodeArea. Defaults to [area].", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + }, + "layers": { + "type": "array", + "description": "OSM layer types to extract", + "items": { + "type": "string", + "enum": ["buildings", "roads", "parks", "water", "surface"] + }, + "minItems": 1 + }, + "source": { + "type": "string", + "enum": ["overpass", "pbf"], + "default": "overpass", + "description": "Data source type" + }, + "pbfFileUrl": { + "type": "string", + "description": "URL to PBF file (required if source is 'pbf')" + }, + "coordinateFormat": { + "type": "string", + "description": "Override workspace coordinate format" + } + }, + "if": { + "required": ["source"], + "properties": {"source": {"const": "pbf"}} + }, + "then": { + "required": ["pbfFileUrl"] + } + }, + "GeoJsonDataSource": { + "type": "object", + "description": "GeoJSON data source from URL or inline", + "required": ["type", "name"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "geojson" + }, + "name": { + "$ref": "#/definitions/Identifier", + "description": "Data source name (table name in database)" + }, + "url": { + "type": "string", + "description": "URL to GeoJSON file" + }, + "values": { + "type": "object", + "description": "Inline GeoJSON FeatureCollection (alternative to url)" + }, + "layerType": { + "type": "string", + "enum": ["polygons", "polylines", "points", "buildings"], + "description": "Geometry type hint for rendering" + }, + "coordinateFormat": { + "type": "string", + "description": "CRS of the input GeoJSON data before it is transformed into the workspace CRS", + "default": "EPSG:4326" + } + }, + "oneOf": [ + {"required": ["url"]}, + {"required": ["values"]} + ] + }, + "CsvDataSource": { + "type": "object", + "description": "CSV data source with optional geometry", + "required": ["type", "name"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "csv" + }, + "name": { + "$ref": "#/definitions/Identifier", + "description": "Data source name (table name in database)" + }, + "url": { + "type": "string", + "description": "URL to CSV file" + }, + "values": { + "type": "array", + "description": "Inline CSV data as array of arrays (alternative to url)" + }, + "delimiter": { + "type": "string", + "default": ",", + "description": "CSV delimiter character" + }, + "geometry": { + "$ref": "#/definitions/CsvGeometry" + } + }, + "oneOf": [ + {"required": ["url"]}, + {"required": ["values"]} + ] + }, + "CsvGeometry": { + "oneOf": [ + { + "type": "object", + "description": "Latitude/longitude columns", + "required": ["type", "latitude", "longitude"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "latlng" + }, + "latitude": { + "type": "string", + "description": "Column name for latitude" + }, + "longitude": { + "type": "string", + "description": "Column name for longitude" + }, + "coordinateFormat": { + "type": "string", + "default": "EPSG:4326" + } + } + }, + { + "type": "object", + "description": "WKT geometry column", + "required": ["type", "column"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "wkt" + }, + "column": { + "type": "string", + "description": "Column name containing WKT geometry" + }, + "coordinateFormat": { + "type": "string" + } + } + } + ] + }, + "JsonDataSource": { + "type": "object", + "description": "JSON data source for arbitrary tabular or nested data", + "required": ["type", "name"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "json" + }, + "name": { + "$ref": "#/definitions/Identifier", + "description": "Data source name (table name in database)" + }, + "url": { + "type": "string", + "description": "URL to JSON file" + }, + "values": { + "type": ["object", "array"], + "description": "Inline JSON data (alternative to url)" + }, + "flatten": { + "type": "boolean", + "default": false, + "description": "Whether to flatten nested JSON structures into columns" + }, + "geometry": { + "$ref": "#/definitions/CsvGeometry", + "description": "Optional geometry columns for spatial JSON records" + } + }, + "oneOf": [ + {"required": ["url"]}, + {"required": ["values"]} + ] + }, + "GeoTiffDataSource": { + "type": "object", + "description": "GeoTIFF raster data source", + "required": ["type", "name", "url"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "geotiff" + }, + "name": { + "$ref": "#/definitions/Identifier", + "description": "Data source name (table name in database)" + }, + "url": { + "type": "string", + "description": "URL to GeoTIFF file" + }, + "band": { + "type": "integer", + "default": 1, + "description": "Band index to use (1-based)" + }, + "coordinateFormat": { + "type": "string", + "description": "Override coordinate reference system (defaults to GeoTIFF CRS)" + }, + "maxPixels": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Maximum number of raster pixels to load" + } + } + }, + "Transform": { + "oneOf": [ + {"$ref": "#/definitions/SpatialJoinTransform"}, + {"$ref": "#/definitions/HeatmapTransform"}, + {"$ref": "#/definitions/GpgpuComputeTransform"}, + {"$ref": "#/definitions/RenderComputeTransform"} + ] + }, + "SpatialJoinTransform": { + "type": "object", + "description": "Spatial join that mutates root table in place (MVP). Runtime must validate that root and join reference existing data sources.", + "required": ["type", "root", "join"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "spatialJoin" + }, + "root": { + "type": "string", + "description": "Root data source name (will be mutated in place). Runtime must validate existence." + }, + "join": { + "type": "string", + "description": "Join data source name. Runtime must validate existence." + }, + "near": { + "type": "object", + "description": "Nearest neighbor join configuration", + "required": ["distance"], + "additionalProperties": false, + "properties": { + "distance": { + "type": "number", + "description": "Maximum distance for near join", + "exclusiveMinimum": 0 + }, + "useCentroid": { + "type": "boolean", + "default": true, + "description": "Use centroid for distance calculation (matches runtime default)" + } + } + }, + "groupBy": { + "type": "array", + "description": "Aggregation operations", + "items": { + "$ref": "#/definitions/Aggregation" + } + } + } + }, + "HeatmapTransform": { + "type": "object", + "description": "Builds a grid heatmap table from a source layer using spatial aggregation.", + "required": ["type", "source", "output", "near", "grid"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "heatmap" + }, + "source": { + "type": "string", + "description": "Source table/layer to aggregate into heatmap cells." + }, + "output": { + "$ref": "#/definitions/Identifier", + "description": "Output table name for the heatmap result." + }, + "near": { + "type": "object", + "required": ["distance"], + "additionalProperties": false, + "properties": { + "distance": { + "type": "number", + "exclusiveMinimum": 0 + }, + "useCentroid": { + "type": "boolean", + "default": true + } + } + }, + "grid": { + "type": "object", + "required": ["rows", "columns"], + "additionalProperties": false, + "properties": { + "rows": { + "type": "integer", + "minimum": 1 + }, + "columns": { + "type": "integer", + "minimum": 1 + } + } + }, + "groupBy": { + "type": "array", + "items": { + "$ref": "#/definitions/HeatmapAggregation" + } + } + } + }, + "HeatmapAggregation": { + "type": "object", + "description": "Heatmap aggregation operation. collect is not supported for raster bands.", + "required": ["column"], + "additionalProperties": false, + "properties": { + "column": { + "type": "string" + }, + "op": { + "type": "string", + "enum": ["count", "sum", "avg", "min", "max", "weighted"] + }, + "as": { + "type": "string" + }, + "normalize": { + "type": "boolean", + "default": false + } + } + }, + "GpgpuComputeTransform": { + "type": "object", + "description": "Runs a WGSL GPGPU compute body over a GeoJSON source layer and materializes the result as a new GeoJSON table.", + "required": ["type", "source", "output", "variableMapping", "wgslBody"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "gpgpuCompute" + }, + "source": { + "type": "string" + }, + "output": { + "$ref": "#/definitions/Identifier" + }, + "layerType": { + "type": "string", + "enum": ["polygons", "polylines", "points", "buildings"] + }, + "coordinateFormat": { + "type": "string" + }, + "variableMapping": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "string" + }, + "propertyNames": { + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + } + }, + "attributeArrays": { + "type": "object", + "additionalProperties": { + "type": "integer", + "minimum": 1 + }, + "propertyNames": { + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + } + }, + "attributeMatrices": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["rows", "cols"], + "additionalProperties": false, + "properties": { + "rows": { + "oneOf": [ + {"type": "integer", "minimum": 1}, + {"type": "string", "const": "auto"} + ] + }, + "cols": { + "type": "integer", + "minimum": 1 + } + } + }, + "propertyNames": { + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + } + }, + "uniforms": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "propertyNames": { + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + } + }, + "uniformArrays": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": {"type": "number"}, + "minItems": 1 + }, + "propertyNames": { + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + } + }, + "uniformMatrices": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["data", "cols"], + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "type": "array", + "items": {"type": "number"} + }, + "minItems": 1 + }, + "cols": { + "type": "integer", + "minimum": 1 + } + } + }, + "propertyNames": { + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + } + }, + "wgslBody": { + "type": "string", + "minLength": 1, + "description": "WGSL function body inserted into compute_value; runtime rejects full-module declarations." + }, + "resultField": { + "$ref": "#/definitions/Identifier" + }, + "outputColumns": { + "type": "array", + "items": { + "$ref": "#/definitions/Identifier" + }, + "minItems": 1 + } + }, + "oneOf": [ + {"required": ["resultField"]}, + {"required": ["outputColumns"]} + ] + }, + "RenderComputeTransform": { + "type": "object", + "description": "Runs render-based compute sampling and materializes viewpoint metrics as a new GeoJSON table.", + "required": ["type", "output", "layers", "viewpoints", "aggregation"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "renderCompute" + }, + "output": { + "$ref": "#/definitions/Identifier" + }, + "layerType": { + "type": "string", + "enum": ["polygons", "polylines", "points", "buildings"] + }, + "coordinateFormat": { + "type": "string" + }, + "layers": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/RenderComputeLayer" + } + }, + "viewpoints": { + "$ref": "#/definitions/RenderComputeViewpoints" + }, + "aggregation": { + "$ref": "#/definitions/RenderComputeAggregation" + }, + "camera": { + "$ref": "#/definitions/RenderComputeCamera" + }, + "tileSize": { + "type": "integer", + "minimum": 8, + "multipleOf": 8 + } + } + }, + "RenderComputeLayer": { + "type": "object", + "required": ["id", "source", "type"], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/definitions/Identifier" + }, + "source": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["buildings", "roads", "polygons", "polylines", "points"] + }, + "objectIdProperty": { + "type": "string" + } + } + }, + "RenderComputeViewpoints": { + "type": "object", + "required": ["source"], + "additionalProperties": false, + "properties": { + "source": { + "type": "string" + }, + "strategy": { + "oneOf": [ + { + "type": "object", + "required": ["type"], + "additionalProperties": false, + "properties": { + "type": {"type": "string", "const": "centroid"} + } + }, + { + "type": "object", + "required": ["type", "floors"], + "additionalProperties": false, + "properties": { + "type": {"type": "string", "const": "building-windows"}, + "floors": {"type": "integer", "minimum": 1} + } + } + ] + }, + "sampling": { + "type": "object", + "additionalProperties": false, + "properties": { + "directions": {"type": "integer", "minimum": 1}, + "azimuthOffsetDeg": {"type": "number"}, + "pitchDeg": {"type": "number"} + } + } + } + }, + "RenderComputeAggregation": { + "oneOf": [ + { + "type": "object", + "required": ["type"], + "additionalProperties": false, + "properties": { + "type": {"type": "string", "const": "classes"}, + "includeBackground": {"type": "boolean"}, + "backgroundLayerType": {"type": "string"} + } + }, + { + "type": "object", + "required": ["type"], + "additionalProperties": false, + "properties": { + "type": {"type": "string", "const": "objects"} + } + } + ] + }, + "RenderComputeCamera": { + "type": "object", + "additionalProperties": false, + "properties": { + "fov": {"type": "number", "exclusiveMinimum": 0}, + "clip": { + "type": "object", + "additionalProperties": false, + "properties": { + "near": {"type": "number", "exclusiveMinimum": 0}, + "far": {"type": "number", "exclusiveMinimum": 0} + } + } + } + }, + "Aggregation": { + "type": "object", + "description": "Spatial join aggregation operation", + "required": ["column", "op"], + "additionalProperties": false, + "properties": { + "column": { + "type": "string", + "description": "Column to aggregate (use '*' for count)" + }, + "op": { + "type": "string", + "enum": ["count", "sum", "avg", "min", "max", "weighted", "collect"], + "description": "Aggregation operation" + }, + "as": { + "type": "string", + "description": "Output property name (defaults to column name)" + }, + "normalize": { + "type": "boolean", + "default": false, + "description": "Normalize result values" + } + } + }, + "View": { + "oneOf": [ + {"$ref": "#/definitions/MapView"}, + {"$ref": "#/definitions/HistogramView"}, + {"$ref": "#/definitions/ScatterplotView"}, + {"$ref": "#/definitions/TableView"} + ] + }, + "MapView": { + "type": "object", + "description": "3D map view with layers", + "required": ["type", "layers"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "map" + }, + "name": { + "$ref": "#/definitions/Identifier", + "description": "View identifier" + }, + "camera": { + "$ref": "#/definitions/Camera" + }, + "style": { + "type": "object", + "description": "Map-level style configuration (runtime-defined properties)" + }, + "layers": { + "type": "array", + "description": "Map layers to render", + "items": { + "$ref": "#/definitions/MapLayer" + }, + "minItems": 1 + } + } + }, + "Camera": { + "type": "object", + "description": "Camera configuration for 3D map", + "additionalProperties": false, + "properties": { + "pitch": { + "type": "number", + "description": "Camera pitch angle in degrees (0-90)", + "minimum": 0, + "maximum": 90 + }, + "bearing": { + "type": "number", + "description": "Camera bearing/rotation in degrees (0-360)" + }, + "zoom": { + "type": "number", + "description": "Zoom level", + "minimum": 0 + } + } + }, + "MapLayer": { + "type": "object", + "description": "Individual map layer. Runtime must validate that source references an existing data source or table.", + "required": ["source"], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "description": "Data source name (e.g., 'manhattan_osm_buildings'). Runtime must validate existence." + }, + "id": { + "$ref": "#/definitions/Identifier", + "description": "Layer identifier (required for link targets)" + }, + "type": { + "type": "string", + "enum": ["buildings", "roads", "polygons", "polylines", "points"], + "description": "Layer rendering type" + }, + "encoding": { + "$ref": "#/definitions/Encoding" + }, + "style": { + "$ref": "#/definitions/LayerStyle" + } + } + }, + "Encoding": { + "type": "object", + "description": "Visual encoding mappings", + "additionalProperties": false, + "properties": { + "color": { + "$ref": "#/definitions/EncodingChannel" + }, + "opacity": { + "$ref": "#/definitions/EncodingChannel" + }, + "size": { + "$ref": "#/definitions/EncodingChannel" + }, + "height": { + "$ref": "#/definitions/EncodingChannel" + } + } + }, + "EncodingChannel": { + "oneOf": [ + {"$ref": "#/definitions/FieldEncoding"}, + {"$ref": "#/definitions/ValueEncoding"} + ] + }, + "FieldEncoding": { + "type": "object", + "description": "Data-driven encoding from field", + "required": ["field"], + "additionalProperties": false, + "properties": { + "field": { + "type": "string", + "description": "Property path (maps: 'properties.height', plots: 'height')" + }, + "scale": { + "$ref": "#/definitions/Scale" + } + } + }, + "ValueEncoding": { + "type": "object", + "description": "Constant value encoding", + "required": ["value"], + "additionalProperties": false, + "properties": { + "value": { + "oneOf": [ + {"type": "string"}, + {"type": "number"}, + {"type": "boolean"} + ], + "description": "Constant value" + } + } + }, + "Scale": { + "type": "object", + "description": "Scale configuration for encoding", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": ["linear", "quantile", "categorical"], + "default": "linear", + "description": "Scale type" + }, + "scheme": { + "type": "string", + "description": "Color scheme name (e.g., 'viridis', 'plasma', 'greens')" + }, + "domain": { + "type": "array", + "description": "Explicit domain values" + }, + "domainStrategy": { + "type": "string", + "enum": ["minMax", "percentile", "user"], + "default": "minMax", + "description": "How to compute domain" + } + } + }, + "LayerStyle": { + "type": "object", + "description": "Layer style properties", + "additionalProperties": false, + "properties": { + "opacity": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "color": { + "type": "string", + "description": "Constant color (hex or CSS color)" + }, + "strokeColor": { + "type": "string" + }, + "strokeWidth": { + "type": "number", + "minimum": 0 + }, + "width": { + "type": "number", + "description": "Line width for polylines/roads", + "minimum": 0 + }, + "size": { + "type": "number", + "description": "Point size", + "minimum": 0 + } + } + }, + "HistogramView": { + "type": "object", + "description": "Histogram plot. Runtime must validate that source references an existing data source.", + "required": ["type", "source", "x"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "histogram" + }, + "name": { + "$ref": "#/definitions/Identifier", + "description": "View identifier" + }, + "source": { + "type": "string", + "description": "Data source name. Runtime must validate existence." + }, + "x": { + "$ref": "#/definitions/PlotAxis" + }, + "y": { + "$ref": "#/definitions/PlotAxis" + }, + "bins": { + "type": "integer", + "default": 30, + "minimum": 1, + "description": "Number of histogram bins" + }, + "selection": { + "$ref": "#/definitions/Selection" + } + } + }, + "ScatterplotView": { + "type": "object", + "description": "Scatterplot with x/y field encodings and optional selection.", + "required": ["type", "source", "x", "y"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "scatterplot" + }, + "name": { + "$ref": "#/definitions/Identifier", + "description": "View identifier" + }, + "source": { + "type": "string", + "description": "Data source name. Runtime must validate existence." + }, + "x": { + "$ref": "#/definitions/PlotAxis" + }, + "y": { + "$ref": "#/definitions/PlotAxis" + }, + "color": { + "$ref": "#/definitions/PlotAxis" + }, + "selection": { + "$ref": "#/definitions/Selection" + } + } + }, + "TableView": { + "type": "object", + "description": "Interactive table view with sortable columns.", + "required": ["type", "source", "columns"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "table" + }, + "name": { + "$ref": "#/definitions/Identifier", + "description": "View identifier" + }, + "source": { + "type": "string", + "description": "Data source name. Runtime must validate existence." + }, + "columns": { + "type": "array", + "items": { + "$ref": "#/definitions/PlotAxis" + }, + "minItems": 1 + }, + "selection": { + "$ref": "#/definitions/Selection" + }, + "sort": { + "type": "object", + "additionalProperties": false, + "properties": { + "column": { + "type": "string" + }, + "direction": { + "type": "string", + "enum": ["asc", "desc"], + "default": "asc" + } + } + } + } + }, + "PlotAxis": { + "type": "object", + "description": "Plot axis configuration", + "required": ["field"], + "additionalProperties": false, + "properties": { + "field": { + "type": "string", + "description": "Property name (plots use short names, not 'properties.' prefix)" + }, + "scale": { + "$ref": "#/definitions/Scale" + } + } + }, + "Selection": { + "type": "object", + "description": "Interactive selection definition", + "required": ["name", "type"], + "additionalProperties": false, + "properties": { + "name": { + "$ref": "#/definitions/Identifier", + "description": "Selection identifier (used in links)" + }, + "type": { + "type": "string", + "enum": ["point", "interval", "multi"], + "description": "Selection interaction type" + }, + "fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields to include in selection" + } + } + }, + "Link": { + "type": "object", + "description": "Cross-view selection link. Runtime must validate that selection and target reference existing elements.", + "required": ["selection", "target", "action"], + "additionalProperties": false, + "properties": { + "selection": { + "type": "string", + "description": "Selection name to link from. Runtime must validate existence." + }, + "target": { + "type": "string", + "description": "Layer ID to link to (not data source name). Runtime must validate existence." + }, + "action": { + "type": "string", + "enum": ["highlight"], + "description": "Link action (MVP: 'highlight' only, 'filter' deferred)" + } + } + }, + "Layout": { + "type": "object", + "description": "View layout configuration (MVP: simple only)", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": ["vertical", "horizontal", "grid"], + "default": "vertical", + "description": "Layout type" + }, + "columns": { + "type": "integer", + "minimum": 1, + "description": "Number of columns for grid layout" + }, + "gap": { + "type": "number", + "minimum": 0, + "description": "Gap between views in pixels" + } + } + } + } +} diff --git a/start_jupyter_test.sh b/start_jupyter_test.sh new file mode 100755 index 00000000..54c85de1 --- /dev/null +++ b/start_jupyter_test.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# Quick start script for Jupyter testing + +echo "🚀 Starting Autark Jupyter Test Environment" +echo "" + +# Check if we're in the right directory +if [ ! -f "PYTHON_API_IMPLEMENTATION.md" ]; then + echo "❌ Error: Please run this script from the repo root (/Users/csilva/src/autark)" + exit 1 +fi + +# Check if runtime is built +if [ ! -f "autk-runtime/dist/autk-runtime.js" ]; then + echo "⚠️ Runtime not built. Building now..." + cd autk-runtime + npm run build + cd .. +fi + +# Function to cleanup on exit +cleanup() { + echo "" + echo "🛑 Stopping server..." + kill $SERVER_PID 2>/dev/null + exit 0 +} +trap cleanup INT TERM + +# Start CORS-enabled HTTP server +echo "📡 Starting CORS-enabled HTTP server on port 8000..." +python cors_server.py 8000 > /tmp/autark-server.log 2>&1 & +SERVER_PID=$! + +# Wait for server to start +sleep 2 + +# Check if server started +if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "❌ Failed to start server. Check /tmp/autark-server.log" + exit 1 +fi + +echo "✅ Server running (PID: $SERVER_PID)" +echo " - Runtime: http://localhost:8000/autk-runtime/dist/autk-runtime.js" +echo " - Data: http://localhost:8000/examples/data/" +echo "" + +# Choose Jupyter environment +echo "Choose Jupyter environment:" +echo " 1) Jupyter Notebook (classic)" +echo " 2) JupyterLab (modern)" +echo " 3) Open in VS Code" +echo " 4) Just run server (no Jupyter)" +echo "" +read -p "Enter choice (1-4): " choice + +case $choice in + 1) + echo "🎯 Starting Jupyter Notebook..." + cd python/examples + jupyter notebook test_jupyter_integration.ipynb + ;; + 2) + echo "🎯 Starting JupyterLab..." + cd python/examples + jupyter lab test_jupyter_integration.ipynb + ;; + 3) + echo "🎯 Opening in VS Code..." + code python/examples/test_jupyter_integration.ipynb + echo "📝 Server is still running. Press Ctrl+C when done." + wait $SERVER_PID + ;; + 4) + echo "📝 Server is running. You can now:" + echo " - Open Jupyter manually" + echo " - Test at http://localhost:8000/test-jupyter.html" + echo "" + echo "Press Ctrl+C to stop the server." + wait $SERVER_PID + ;; + *) + echo "❌ Invalid choice" + cleanup + ;; +esac + +cleanup diff --git a/test-cross-origin.html b/test-cross-origin.html new file mode 100644 index 00000000..97c6de22 --- /dev/null +++ b/test-cross-origin.html @@ -0,0 +1,58 @@ + + + + + Autark Cross-Origin (Jupyter-like) Test + + +

Cross-Origin Runtime Test (simulates Jupyter on another port)

+
Loading...
+
+ + + + diff --git a/test-jupyter.html b/test-jupyter.html new file mode 100644 index 00000000..1d3e6441 --- /dev/null +++ b/test-jupyter.html @@ -0,0 +1,186 @@ + + + + + Autark Python API Test + + + +

Autark Python API - Jupyter Integration Test

+ +
+

Test 1: Simple GeoJSON Map

+
Loading...
+
+
+ +
+

Test 2: CSV Points with Histogram

+
Not started...
+
+
+ + + + diff --git a/tests/fixtures/runtime/fixture-01-geojson-map.json b/tests/fixtures/runtime/fixture-01-geojson-map.json new file mode 100644 index 00000000..1f62caa1 --- /dev/null +++ b/tests/fixtures/runtime/fixture-01-geojson-map.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "Fixture Test - GeoJSON Map", + "description": "Local fixture test with GeoJSON polygons", + "created": "2026-06-07" + }, + "data": [ + { + "type": "geojson", + "name": "manhattan", + "url": "/tests/fixtures/runtime/mnt-small.geojson", + "coordinateFormat": "EPSG:3857", + "layerType": "polygons" + } + ], + "views": [ + { + "type": "map", + "name": "manhattan_map", + "camera": { + "pitch": 0, + "bearing": 0, + "zoom": 13 + }, + "layers": [ + { + "source": "manhattan", + "id": "manhattan_layer", + "type": "polygons", + "style": { + "color": "#2f6f73", + "strokeColor": "#123456" + } + } + ] + } + ], + "layout": { + "type": "vertical" + } +} diff --git a/tests/fixtures/runtime/fixture-02-csv-histogram.json b/tests/fixtures/runtime/fixture-02-csv-histogram.json new file mode 100644 index 00000000..9ebb94bb --- /dev/null +++ b/tests/fixtures/runtime/fixture-02-csv-histogram.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "Fixture Test - CSV with Histogram", + "description": "Local fixture test with CSV points and histogram", + "created": "2026-06-07" + }, + "data": [ + { + "type": "csv", + "name": "points", + "url": "/tests/fixtures/runtime/simple-points.csv", + "geometry": { + "type": "latlng", + "latitude": "latitude", + "longitude": "longitude", + "coordinateFormat": "EPSG:4326" + } + } + ], + "views": [ + { + "type": "map", + "name": "points_map", + "camera": { + "pitch": 0, + "bearing": 0, + "zoom": 13 + }, + "layers": [ + { + "source": "points", + "id": "points_layer", + "type": "points", + "encoding": { + "color": { + "field": "properties.value", + "scale": { + "type": "linear", + "scheme": "viridis", + "domainStrategy": "minMax" + } + } + } + } + ] + }, + { + "type": "histogram", + "name": "value_histogram", + "source": "points", + "x": { + "field": "value" + }, + "bins": 5 + } + ], + "layout": { + "type": "vertical" + } +} diff --git a/tests/fixtures/runtime/fixture-03-osm-har.json b/tests/fixtures/runtime/fixture-03-osm-har.json new file mode 100644 index 00000000..267b3727 --- /dev/null +++ b/tests/fixtures/runtime/fixture-03-osm-har.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "Fixture Test - OSM HAR", + "description": "OSM runtime fixture using a recorded Overpass HAR", + "created": "2026-06-10" + }, + "workspace": { + "name": "runtime_osm_har", + "coordinateFormat": "EPSG:4326" + }, + "data": [ + { + "type": "osm", + "name": "nyc_osm", + "area": "Battery Park City", + "geocodeArea": "New York", + "areas": ["Battery Park City", "Financial District"], + "layers": ["buildings", "roads"], + "source": "overpass" + } + ], + "views": [ + { + "type": "map", + "name": "osm_map", + "camera": { + "pitch": 45, + "bearing": 0, + "zoom": 13 + }, + "layers": [ + { + "source": "nyc_osm_buildings", + "id": "buildings_layer", + "type": "buildings", + "style": { + "opacity": 0.85 + } + }, + { + "source": "nyc_osm_roads", + "id": "roads_layer", + "type": "roads", + "style": { + "color": "#444444" + } + } + ] + } + ], + "layout": { + "type": "vertical" + } +} diff --git a/tests/fixtures/runtime/fixture-04-osm-live.json b/tests/fixtures/runtime/fixture-04-osm-live.json new file mode 100644 index 00000000..351e388c --- /dev/null +++ b/tests/fixtures/runtime/fixture-04-osm-live.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "Fixture Test - Live OSM Overpass", + "description": "Small live Overpass fixture for manual network validation", + "created": "2026-06-10" + }, + "workspace": { + "name": "runtime_osm_live", + "coordinateFormat": "EPSG:4326" + }, + "data": [ + { + "type": "osm", + "name": "battery_park_osm", + "area": "Battery Park City", + "geocodeArea": "New York", + "areas": ["Battery Park City"], + "layers": ["roads"], + "source": "overpass" + } + ], + "views": [ + { + "type": "map", + "name": "osm_live_map", + "camera": { + "pitch": 0, + "bearing": 0, + "zoom": 13 + }, + "layers": [ + { + "source": "battery_park_osm_roads", + "id": "roads_layer", + "type": "roads", + "style": { + "color": "#444444" + } + } + ] + } + ], + "layout": { + "type": "vertical" + } +} diff --git a/tests/fixtures/runtime/fixture-05-line-style.json b/tests/fixtures/runtime/fixture-05-line-style.json new file mode 100644 index 00000000..5ca80c73 --- /dev/null +++ b/tests/fixtures/runtime/fixture-05-line-style.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "Fixture Test - Line Style", + "description": "Local fixture test with polyline width and color styles", + "created": "2026-06-10" + }, + "data": [ + { + "type": "geojson", + "name": "lines", + "url": "/tests/fixtures/runtime/simple-lines.geojson", + "coordinateFormat": "EPSG:3857", + "layerType": "polylines" + } + ], + "views": [ + { + "type": "map", + "name": "line_map", + "camera": { + "pitch": 0, + "bearing": 0, + "zoom": 13 + }, + "layers": [ + { + "source": "lines", + "id": "line_layer", + "type": "polylines", + "style": { + "color": "#654321", + "width": 12 + } + } + ] + } + ], + "layout": { + "type": "vertical" + } +} diff --git a/tests/fixtures/runtime/fixture-06-scatter-table.json b/tests/fixtures/runtime/fixture-06-scatter-table.json new file mode 100644 index 00000000..1b0b6623 --- /dev/null +++ b/tests/fixtures/runtime/fixture-06-scatter-table.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json", + "version": "0.1", + "metadata": { + "title": "Fixture Test - Scatterplot and Table", + "description": "Local fixture test with scatterplot and table views", + "created": "2026-06-10" + }, + "data": [ + { + "type": "geojson", + "name": "points", + "url": "/tests/fixtures/runtime/simple-points.geojson", + "layerType": "points", + "coordinateFormat": "EPSG:4326" + } + ], + "views": [ + { + "type": "scatterplot", + "name": "value_scatterplot", + "source": "points", + "x": { + "field": "id" + }, + "y": { + "field": "value" + }, + "selection": { + "name": "scatter_brush", + "type": "interval" + } + }, + { + "type": "table", + "name": "points_table", + "source": "points", + "columns": [ + { "field": "id" }, + { "field": "name" }, + { "field": "value" } + ], + "sort": { + "column": "value", + "direction": "desc" + } + } + ], + "layout": { + "type": "vertical" + } +} diff --git a/tests/fixtures/runtime/grid-points.geojson b/tests/fixtures/runtime/grid-points.geojson new file mode 100644 index 00000000..31e471d0 --- /dev/null +++ b/tests/fixtures/runtime/grid-points.geojson @@ -0,0 +1,53 @@ +{ + "type": "FeatureCollection", + "features": [ + {"type": "Feature", "properties": {"value": 0}, "geometry": {"type": "Point", "coordinates": [-74.01, 40.70]}}, + {"type": "Feature", "properties": {"value": 10}, "geometry": {"type": "Point", "coordinates": [-74.00, 40.70]}}, + {"type": "Feature", "properties": {"value": 20}, "geometry": {"type": "Point", "coordinates": [-73.99, 40.70]}}, + {"type": "Feature", "properties": {"value": 30}, "geometry": {"type": "Point", "coordinates": [-73.98, 40.70]}}, + {"type": "Feature", "properties": {"value": 40}, "geometry": {"type": "Point", "coordinates": [-73.97, 40.70]}}, + {"type": "Feature", "properties": {"value": 50}, "geometry": {"type": "Point", "coordinates": [-73.96, 40.70]}}, + + {"type": "Feature", "properties": {"value": 0}, "geometry": {"type": "Point", "coordinates": [-74.01, 40.71]}}, + {"type": "Feature", "properties": {"value": 10}, "geometry": {"type": "Point", "coordinates": [-74.00, 40.71]}}, + {"type": "Feature", "properties": {"value": 20}, "geometry": {"type": "Point", "coordinates": [-73.99, 40.71]}}, + {"type": "Feature", "properties": {"value": 30}, "geometry": {"type": "Point", "coordinates": [-73.98, 40.71]}}, + {"type": "Feature", "properties": {"value": 40}, "geometry": {"type": "Point", "coordinates": [-73.97, 40.71]}}, + {"type": "Feature", "properties": {"value": 50}, "geometry": {"type": "Point", "coordinates": [-73.96, 40.71]}}, + + {"type": "Feature", "properties": {"value": 0}, "geometry": {"type": "Point", "coordinates": [-74.01, 40.72]}}, + {"type": "Feature", "properties": {"value": 10}, "geometry": {"type": "Point", "coordinates": [-74.00, 40.72]}}, + {"type": "Feature", "properties": {"value": 20}, "geometry": {"type": "Point", "coordinates": [-73.99, 40.72]}}, + {"type": "Feature", "properties": {"value": 30}, "geometry": {"type": "Point", "coordinates": [-73.98, 40.72]}}, + {"type": "Feature", "properties": {"value": 40}, "geometry": {"type": "Point", "coordinates": [-73.97, 40.72]}}, + {"type": "Feature", "properties": {"value": 50}, "geometry": {"type": "Point", "coordinates": [-73.96, 40.72]}}, + + {"type": "Feature", "properties": {"value": 0}, "geometry": {"type": "Point", "coordinates": [-74.01, 40.73]}}, + {"type": "Feature", "properties": {"value": 10}, "geometry": {"type": "Point", "coordinates": [-74.00, 40.73]}}, + {"type": "Feature", "properties": {"value": 20}, "geometry": {"type": "Point", "coordinates": [-73.99, 40.73]}}, + {"type": "Feature", "properties": {"value": 30}, "geometry": {"type": "Point", "coordinates": [-73.98, 40.73]}}, + {"type": "Feature", "properties": {"value": 40}, "geometry": {"type": "Point", "coordinates": [-73.97, 40.73]}}, + {"type": "Feature", "properties": {"value": 50}, "geometry": {"type": "Point", "coordinates": [-73.96, 40.73]}}, + + {"type": "Feature", "properties": {"value": 0}, "geometry": {"type": "Point", "coordinates": [-74.01, 40.74]}}, + {"type": "Feature", "properties": {"value": 10}, "geometry": {"type": "Point", "coordinates": [-74.00, 40.74]}}, + {"type": "Feature", "properties": {"value": 20}, "geometry": {"type": "Point", "coordinates": [-73.99, 40.74]}}, + {"type": "Feature", "properties": {"value": 30}, "geometry": {"type": "Point", "coordinates": [-73.98, 40.74]}}, + {"type": "Feature", "properties": {"value": 40}, "geometry": {"type": "Point", "coordinates": [-73.97, 40.74]}}, + {"type": "Feature", "properties": {"value": 50}, "geometry": {"type": "Point", "coordinates": [-73.96, 40.74]}}, + + {"type": "Feature", "properties": {"value": 0}, "geometry": {"type": "Point", "coordinates": [-74.01, 40.75]}}, + {"type": "Feature", "properties": {"value": 10}, "geometry": {"type": "Point", "coordinates": [-74.00, 40.75]}}, + {"type": "Feature", "properties": {"value": 20}, "geometry": {"type": "Point", "coordinates": [-73.99, 40.75]}}, + {"type": "Feature", "properties": {"value": 30}, "geometry": {"type": "Point", "coordinates": [-73.98, 40.75]}}, + {"type": "Feature", "properties": {"value": 40}, "geometry": {"type": "Point", "coordinates": [-73.97, 40.75]}}, + {"type": "Feature", "properties": {"value": 50}, "geometry": {"type": "Point", "coordinates": [-73.96, 40.75]}}, + + {"type": "Feature", "properties": {"value": 0}, "geometry": {"type": "Point", "coordinates": [-74.01, 40.76]}}, + {"type": "Feature", "properties": {"value": 10}, "geometry": {"type": "Point", "coordinates": [-74.00, 40.76]}}, + {"type": "Feature", "properties": {"value": 20}, "geometry": {"type": "Point", "coordinates": [-73.99, 40.76]}}, + {"type": "Feature", "properties": {"value": 30}, "geometry": {"type": "Point", "coordinates": [-73.98, 40.76]}}, + {"type": "Feature", "properties": {"value": 40}, "geometry": {"type": "Point", "coordinates": [-73.97, 40.76]}}, + {"type": "Feature", "properties": {"value": 50}, "geometry": {"type": "Point", "coordinates": [-73.96, 40.76]}} + ] +} diff --git a/tests/fixtures/runtime/mnt-small.geojson b/tests/fixtures/runtime/mnt-small.geojson new file mode 100644 index 00000000..31f408d8 --- /dev/null +++ b/tests/fixtures/runtime/mnt-small.geojson @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8237729.476530929,4939486.723352737],[-8237720.358874049,4939501.544867631],[-8237672.23274935,4939586.072452652],[-8237909.265717435,4939695.158246201],[-8237935.70874332,4939644.024264006],[-8237977.828631048,4939558.58263889],[-8237749.03886236,4939454.9231396075],[-8237729.476530929,4939486.723352737]]],[[[-8237589.301427131,4939821.404307374],[-8237583.77207122,4939840.316885006],[-8237587.012725554,4939843.589038526],[-8237576.3489652015,4939866.797874732],[-8237592.658057331,4939875.579759889],[-8237596.421683646,4939868.6798342755],[-8237590.149048499,4939865.543428275],[-8237597.0489784395,4939851.743642718],[-8237690.350101905,4939891.263798308],[-8237696.791120582,4939872.795361239],[-8237589.301427131,4939821.404307374]]],[[[-8237516.657981151,4939906.637363263],[-8237461.714625767,4940039.109280011],[-8237696.607617705,4940140.686476795],[-8237759.0962517,4940016.429762579],[-8237516.657981151,4939906.637363263]]],[[[-8237419.632356523,4940146.523672253],[-8237363.906676442,4940269.559542211],[-8237583.835715242,4940369.592531709],[-8237644.367463572,4940244.28159099],[-8237422.02036553,4940141.265625634],[-8237419.632356523,4940146.523672253]]],[[[-8237229.314594233,4940530.839151823],[-8237221.129538602,4940545.304824099],[-8237128.521718537,4940708.973414901],[-8237114.6811444685,4940733.430168655],[-8237101.113143409,4940757.405117231],[-8237184.363546495,4940799.233813706],[-8237218.44439219,4940816.357226915],[-8237435.130754037,4940544.531651108],[-8237397.597035381,4940515.366436343],[-8237392.975325424,4940521.22017157],[-8237367.096037779,4940502.117574327],[-8237288.533210941,4940467.300480571],[-8237274.707158257,4940450.654381632],[-8237265.765010679,4940466.455978008],[-8237252.510031823,4940489.844330125],[-8237229.314594233,4940530.839151823]]],[[[-8237088.0973573215,4940782.82082595],[-8237063.499859484,4940803.897261015],[-8237083.721111736,4940817.0263839755],[-8237118.389846316,4940842.236856135],[-8237135.330013549,4940801.461332419],[-8237090.833774301,4940780.476137386],[-8237088.0973573215,4940782.82082595]]],[[[-8237060.044434256,4940900.575747768],[-8237067.506524534,4940905.467711655],[-8237072.590809772,4940908.73182727],[-8237088.978363566,4940879.4103815155],[-8237077.889487127,4940874.014977182],[-8237075.750438863,4940877.818261731],[-8237072.629967545,4940876.459444738],[-8237060.044434256,4940900.575747768]]],[[[-8237800.31953413,4941758.066985747],[-8237809.489778417,4941767.039374147],[-8237828.105530949,4941786.041686358],[-8237933.790581336,4941895.244578574],[-8237944.41123671,4941906.32865029],[-8238001.393760265,4941967.330242689],[-8238006.774151418,4941973.051297426],[-8238027.629691407,4941995.004442811],[-8238035.381069921,4942003.479945837],[-8238057.647997198,4942027.524646664],[-8238080.567462152,4942048.287309539],[-8238084.397267781,4942051.756762751],[-8238088.382909413,4942056.005735866],[-8238113.096579365,4942081.47172398],[-8238133.982356117,4942106.105073077],[-8238139.195998683,4942117.554296995],[-8238143.308956632,4942136.959525567],[-8238142.059364997,4942157.725008234],[-8238133.404955119,4942179.816910615],[-8238125.654336498,4942197.647117635],[-8238120.006027721,4942210.552596309],[-8238166.246427756,4942161.717758972],[-8238183.073190417,4942143.946763919],[-8238197.487471257,4942132.582017497],[-8238213.067631632,4942122.621833836],[-8238229.597914704,4942114.234317702],[-8238246.836830107,4942107.542040559],[-8238264.519922626,4942102.6205216795],[-8238281.342230898,4942090.727297248],[-8238317.936197715,4942078.975737747],[-8238326.859523881,4942076.183447021],[-8238423.5587773165,4942046.136580999],[-8238477.950824499,4942029.314389585],[-8238484.5787407765,4942028.8343218975],[-8238491.214218979,4942029.1951092305],[-8238497.751011105,4942030.390974725],[-8238504.084442359,4942032.402789402],[-8238510.113105307,4942035.198320572],[-8238515.740467899,4942038.732806447],[-8238522.271147978,4942039.255850402],[-8238528.821424482,4942039.1245619245],[-8238535.325900826,4942038.340251802],[-8238541.719637705,4942036.910750566],[-8238544.922591036,4942035.457603321],[-8238547.884239325,4942033.560452285],[-8238550.971136312,4942030.819873139],[-8238553.553650574,4942027.599575532],[-8238555.558347566,4942023.9911306165],[-8238556.928223493,4942020.097146599],[-8238557.62432609,4942016.028350847],[-8238557.626862273,4942011.900441345],[-8238556.935760781,4942007.830796672],[-8238555.570674165,4942003.935138364],[-8238549.279853727,4942001.256211265],[-8238542.765855358,4941999.178021077],[-8238536.085929993,4941997.71879114],[-8238529.29872751,4941996.891369566],[-8238571.570373433,4941983.90261964],[-8238599.761320107,4941988.831806863],[-8238626.248825451,4942005.175421621],[-8238769.058375801,4942090.133274991],[-8238942.433338367,4942191.932309318],[-8238954.468311393,4942199.276500339],[-8239039.035817401,4942248.447705436],[-8239098.896088975,4942283.608404662],[-8239170.08412175,4942323.2714528125],[-8239138.33851615,4942433.303604083],[-8239117.896761841,4942526.077371007],[-8239113.75262933,4942547.095146103],[-8239100.867511137,4942607.214653602],[-8239092.251283868,4942655.9316142015],[-8239080.708701133,4942721.194340526],[-8239059.490263693,4942825.862092408],[-8239054.747178189,4942852.850265178],[-8239014.379947979,4943083.189876133],[-8239010.61990665,4943107.5495111],[-8239027.254669314,4943110.289100086],[-8239039.9001388,4943112.072654976],[-8239056.263112301,4943115.278624403],[-8239092.88750529,4943121.084244981],[-8239097.529130375,4943092.870333089],[-8239097.047921944,4943090.467234024],[-8239113.208424985,4943001.735187999],[-8239279.015803812,4943031.060359425],[-8239306.005801588,4943035.0662979325],[-8239308.811841636,4943033.942036723],[-8239310.255625071,4943032.177646667],[-8239310.977096516,4943029.7716711555],[-8239311.6877513025,4943024.74658148],[-8239312.178615476,4943021.269539593],[-8239373.43447195,4943031.905770166],[-8239386.764208668,4943034.219563345],[-8239386.565621493,4943036.6688356465],[-8239417.177721561,4943041.755493941],[-8239417.52528334,4943039.750176101],[-8239493.557757829,4943052.891502876],[-8239496.842385705,4943052.987009126],[-8239500.707539052,4943050.47374617],[-8239502.488105307,4943046.864720037],[-8239519.496355883,4942924.373163319],[-8239532.828999296,4942828.351292426],[-8239548.244521247,4942717.328372634],[-8239564.162335575,4942603.34271793],[-8239571.007870812,4942553.926825367],[-8239577.29990381,4942508.507607481],[-8239577.364691719,4942508.064689222],[-8239580.247836084,4942488.367701634],[-8239594.552869613,4942384.604182368],[-8239603.582571579,4942319.762336783],[-8239613.438741759,4942248.802509519],[-8239615.452467895,4942234.304460663],[-8239615.239977365,4942233.858163045],[-8239614.962961485,4942233.448778197],[-8239614.627685494,4942233.085565094],[-8239614.2417323,4942232.776738515],[-8239613.8138309745,4942232.529283133],[-8239613.353659318,4942232.348795613],[-8239612.871624976,4942232.239358034],[-8239612.338148093,4942232.203792398],[-8239611.805963139,4942232.255189676],[-8239611.289152272,4942232.392189844],[-8239610.8013908565,4942232.611167732],[-8239610.355585549,4942232.906328981],[-8239609.963532823,4942233.269863324],[-8239609.635606802,4942233.692151263],[-8239609.380484765,4942234.1620186325],[-8239607.674828762,4942247.599985653],[-8239599.262483642,4942311.155236012],[-8239524.807762996,4942296.6480919765],[-8239483.670748532,4942288.561503206],[-8239459.060169779,4942283.723436232],[-8239467.0338270925,4942238.666465449],[-8239476.556895976,4942228.351898352],[-8239483.667948952,4942217.536443768],[-8239485.267300267,4942213.664174952],[-8239489.275499963,4942203.7845105585],[-8239492.270615205,4942188.363064055],[-8239492.245654052,4942172.799257304],[-8239489.362398494,4942157.936807521],[-8239484.0593937095,4942144.458230348],[-8239492.921107281,4942100.75733363],[-8239623.847815526,4942128.232174783],[-8239615.065885142,4942192.458461764],[-8239614.924083532,4942201.322026111],[-8239616.217920083,4942201.992265366],[-8239618.565502538,4942201.848195097],[-8239620.481921677,4942199.836229929],[-8239631.969167107,4942123.846245622],[-8239637.413619041,4942083.483594274],[-8239643.679764749,4942042.093807616],[-8239651.200943734,4941981.847986947],[-8239658.379011123,4941935.784659792],[-8239662.189001492,4941915.160750993],[-8239675.468101982,4941844.992090303],[-8239684.070163168,4941800.736465393],[-8239698.2584899515,4941725.1191283455],[-8239716.359337921,4941630.256008444],[-8239729.213875462,4941562.888336544],[-8239741.248716349,4941499.540158007],[-8239741.078228776,4941495.806656993],[-8239738.9054850545,4941492.911224649],[-8239738.682770831,4941492.464639243],[-8239735.195336602,4941489.751843378],[-8239718.0810899595,4941484.806243655],[-8239719.047310354,4941481.4614256015],[-8239667.252811443,4941466.49321013],[-8239670.225180824,4941456.207120141],[-8239670.255242944,4941454.875306247],[-8239691.319894995,4941384.025346009],[-8239705.106439282,4941339.096144849],[-8239705.64023582,4941334.355079954],[-8239707.1758769695,4941332.884748788],[-8239708.912012846,4941332.616969066],[-8239709.44727567,4941334.687607415],[-8239709.713793919,4941337.291828435],[-8239710.582355142,4941338.961916032],[-8239711.918414276,4941340.36324266],[-8239713.188712143,4941342.634065015],[-8239713.389343215,4941344.703403986],[-8239714.257659341,4941345.905986659],[-8239715.9943047725,4941347.9090779275],[-8239718.198415175,4941348.909812443],[-8239721.603564947,4941349.043430308],[-8239723.406665196,4941347.90749882],[-8239726.54571562,4941347.306179867],[-8239728.682420532,4941347.43876744],[-8239732.421844946,4941345.9021102395],[-8239734.959803711,4941343.697595742],[-8239736.360814484,4941341.360501429],[-8239740.568445769,4941341.960343132],[-8239740.501553798,4941343.829547373],[-8239739.433675392,4941345.5672706645],[-8239737.3647819655,4941348.639673389],[-8239734.493678107,4941351.044232297],[-8239732.223127592,4941352.780915209],[-8239729.551722915,4941353.582499565],[-8239730.888277452,4941357.389117481],[-8239729.61996063,4941357.990111706],[-8239726.347864597,4941358.258150688],[-8239722.540883647,4941358.592200103],[-8239718.200405646,4941358.259888545],[-8239717.800589432,4941361.73239308],[-8239716.665043707,4941361.399300028],[-8239713.66006092,4941360.26446405],[-8239709.652334724,4941357.994365416],[-8239706.981864638,4941362.068024447],[-8239710.7221198045,4941364.405304631],[-8239716.2657959135,4941366.741024239],[-8239719.037320527,4941367.007822762],[-8239722.843105555,4941367.140154297],[-8239726.917529112,4941367.273554222],[-8239730.7900897795,4941366.4705849765],[-8239734.729801075,4941365.000930254],[-8239738.402124535,4941363.196796094],[-8239741.740854191,4941360.99217728],[-8239744.545016628,4941358.854967346],[-8239747.282966017,4941355.71529277],[-8239749.218670726,4941352.777257893],[-8239750.4210474165,4941351.306989142],[-8239751.354510474,4941348.902697714],[-8239752.223205565,4941345.830551698],[-8239761.104893855,4941348.232714728],[-8239764.755194024,4941348.317521284],[-8239767.580420998,4941346.9574699765],[-8239768.805384805,4941346.367141165],[-8239771.054567276,4941342.765186532],[-8239782.14902458,4941283.853657207],[-8239795.521100831,4941213.724226824],[-8239795.41098369,4941210.340203733],[-8239749.120641236,4941020.649319637],[-8239744.863300317,4941014.122456582],[-8239742.125988963,4941012.727536041],[-8239658.651873799,4940988.44489695],[-8239653.315738797,4941000.472330743],[-8239652.0723407185,4940999.547704667],[-8239650.84847287,4940999.332995562],[-8239648.762029104,4941001.923963548],[-8239646.962694487,4941000.843817302],[-8239645.882689989,4941002.499648207],[-8239644.875012885,4941002.067604522],[-8239643.723743592,4941001.20343761],[-8239642.643590681,4941002.138662567],[-8239639.83780161,4941002.283599016],[-8239639.549349014,4941003.362410178],[-8239636.094313519,4941003.86692372],[-8239635.158942201,4941002.786745428],[-8239628.250193536,4941004.153987796],[-8239627.026461986,4941004.729574751],[-8239624.507954279,4941004.298865479],[-8239623.571405969,4941005.018068806],[-8239621.98817977,4941005.593728209],[-8239620.117678549,4941004.729606172],[-8239619.972776646,4941005.809833729],[-8239618.174657578,4941006.241787433],[-8239616.375501201,4941005.666176791],[-8239614.719673509,4941006.889870722],[-8239611.193391474,4941006.025763059],[-8239609.969672332,4941007.033353806],[-8239605.363406313,4941006.744897825],[-8239601.328607565,4941007.558896697],[-8239597.860420112,4941005.673926301],[-8239598.613929104,4941005.147007006],[-8239598.237178687,4941004.392267194],[-8239596.880429954,4941004.845350053],[-8239595.7488815915,4941004.543719259],[-8239593.939074809,4941005.598818678],[-8239588.284126854,4941005.070534079],[-8239592.24252346,4940992.397566751],[-8239719.41405064,4940970.186179438],[-8239695.834475127,4940931.214690113],[-8239628.749357752,4940942.870691559],[-8239641.031197033,4940903.924068735],[-8239639.690123645,4940903.542528817],[-8239636.305969126,4940915.437684217],[-8239624.72030487,4940912.141601086],[-8239628.016513958,4940902.005034107],[-8239625.763313971,4940898.385716048],[-8239622.84751537,4940900.285848893],[-8239618.300727903,4940915.49408454],[-8239630.023506375,4940918.999361503],[-8239623.1982661,4940941.825851163],[-8239598.241756287,4940934.363506845],[-8239549.359423708,4940948.794206209],[-8239517.607625399,4941000.334917705],[-8239509.184557032,4940999.94074736],[-8239501.334209788,4941000.087024985],[-8239494.373557845,4941000.119103769],[-8239493.389744948,4941016.282030118],[-8239493.295887037,4941024.404155811],[-8239481.361617679,4941024.446088184],[-8239465.197289216,4941030.116917266],[-8239456.420148957,4941024.48962577],[-8239446.397616827,4941025.159748862],[-8239408.145386527,4941029.3427260155],[-8239385.310923258,4941029.327401464],[-8239356.3701531105,4941024.50917135],[-8239343.01810448,4941021.651086288],[-8239331.002014435,4941018.439751712],[-8239308.921891005,4941011.75766107],[-8239300.469293313,4941009.496590462],[-8239287.6869025165,4941006.072937233],[-8239275.052516491,4941002.1380835315],[-8239258.357333093,4940996.062085458],[-8239242.014457308,4940989.09380805],[-8239232.988354876,4940987.782875498],[-8239230.92104043,4940983.563338348],[-8239229.580175487,4940979.059968032],[-8239229.002752587,4940974.396894991],[-8239229.204627729,4940969.702548125],[-8239244.403025703,4940848.777970234],[-8239248.640146485,4940783.227254025],[-8239246.744231659,4940772.366437443],[-8239243.908382943,4940761.712352623],[-8239240.154212192,4940751.346181607],[-8239235.510365571,4940741.346867954],[-8239230.012190497,4940731.790644234],[-8239223.70158445,4940722.750325623],[-8239182.358087038,4940674.261614145],[-8239180.691649818,4940649.540202345],[-8239179.585526596,4940636.54882987],[-8239179.45480811,4940635.227627606],[-8239179.536889753,4940633.902514442],[-8239179.829662006,4940632.607544869],[-8239180.325600752,4940631.375998644],[-8239181.011960634,4940630.239525578],[-8239181.871102677,4940629.227332175],[-8239182.88094749,4940628.365430994],[-8239184.0155427875,4940627.67597221],[-8239185.245730314,4940627.176674368],[-8239186.539895181,4940626.880368996],[-8239187.864778312,4940626.794670898],[-8239190.4568532,4940625.97984043],[-8239192.898363497,4940624.787436638],[-8239195.134666808,4940623.244146375],[-8239197.115713401,4940621.384509542],[-8239198.79716645,4940619.250146093],[-8239199.796263755,4940617.578539331],[-8239200.676265141,4940616.173619894],[-8239201.352936625,4940614.66024091],[-8239201.813202264,4940613.067647141],[-8239202.048167918,4940611.426614053],[-8239198.467878288,4940546.328341913],[-8239180.092297871,4940513.446290851],[-8239172.70374705,4940497.954803924],[-8239169.972003132,4940488.105753931],[-8239168.258906371,4940478.928382118],[-8239238.552143708,4940488.055831861],[-8239195.3955730805,4940467.329827611],[-8239151.618369593,4940459.853804436],[-8239146.289097216,4940417.509080897],[-8239153.717629261,4940416.5305688055],[-8239151.012945125,4940396.005591759],[-8239154.6403675405,4940395.794345576],[-8239154.115184814,4940389.21254235],[-8239147.687535178,4940389.724589162],[-8239147.760923538,4940387.021888642],[-8239151.778139111,4940386.582538452],[-8239152.1416425165,4940377.96433553],[-8239148.635484236,4940378.257330937],[-8239148.416669239,4940377.015192299],[-8239151.848835121,4940375.992680654],[-8239151.956917732,4940367.737966998],[-8239148.524521465,4940367.884613951],[-8239148.816683012,4940365.840376173],[-8239151.956578626,4940365.547548037],[-8239152.978717256,4940356.344405633],[-8239157.579726005,4940358.2416806305],[-8239161.011761171,4940346.044423828],[-8239163.248894552,4940342.369101979],[-8239164.589638686,4940340.784456752],[-8239157.410105831,4940334.710782795],[-8239154.782465364,4940334.022195187],[-8239148.788693367,4940338.201130025],[-8239145.521068649,4940348.190975649],[-8239143.2522385465,4940360.905791143],[-8239142.981684977,4940374.71095576],[-8239143.529754286,4940392.329181761],[-8239119.917387856,4940395.056687071],[-8239114.282989472,4940371.173475295],[-8239112.556168468,4940362.635901699],[-8239111.829708723,4940357.459636362],[-8239110.14598732,4940337.479948801],[-8239109.235258847,4940324.947369788],[-8239108.053116515,4940312.324086026],[-8239106.59847237,4940299.70071237],[-8239105.144714805,4940293.6156520005],[-8239102.055444424,4940289.3486153595],[-8239094.336847199,4940289.439279423],[-8239091.793876242,4940292.164826465],[-8239090.795967256,4940295.797758264],[-8239092.25028929,4940305.242712111],[-8239098.203411173,4940337.481739163],[-8239099.931474483,4940355.099724102],[-8239101.887758496,4940375.079482232],[-8239101.890205351,4940391.426728558],[-8239101.436531516,4940396.604488042],[-8239089.70066104,4940397.858850644],[-8239079.321742019,4940398.968107966],[-8239078.868065459,4940395.062820821],[-8239076.323473332,4940386.981111917],[-8239074.597301204,4940381.986649291],[-8239071.05419866,4940373.9947069185],[-8239066.784212137,4940366.275456255],[-8239059.9714808,4940354.379936311],[-8239053.930806442,4940343.846232208],[-8239050.297482567,4940339.214301893],[-8239046.482818847,4940338.216398536],[-8239038.49085064,4940342.123024337],[-8239035.494686499,4940344.392793168],[-8239035.586211186,4940349.38875136],[-8239037.040552559,4940359.286938222],[-8239039.903596025,4940374.997580333],[-8239043.903097478,4940397.247822625],[-8239044.2678283155,4940405.875147328],[-8239021.745340281,4940413.143816484],[-8239018.4752278095,4940406.6059746],[-8239013.614500065,4940393.801175449],[-8239006.528247114,4940375.729457531],[-8239004.52924409,4940369.373104859],[-8238997.3529341165,4940356.477907724],[-8238995.717870668,4940353.298427772],[-8238992.356335509,4940343.309596184],[-8238987.995563194,4940335.045091135],[-8238982.636463158,4940329.414974304],[-8238973.918128479,4940327.872435485],[-8238970.467446515,4940329.871036054],[-8238962.203355165,4940338.046450762],[-8238967.563991442,4940355.573156802],[-8238975.559562844,4940377.731856166],[-8238982.445958547,4940399.749854523],[-8238985.869449963,4940410.102278763],[-8238987.625187967,4940419.577751256],[-8238987.801930708,4940422.034172766],[-8238977.217264381,4940425.474657676],[-8238961.876067613,4940430.461094651],[-8238961.262114482,4940429.145241722],[-8238959.682018264,4940422.740025531],[-8238958.804410916,4940419.845126603],[-8238953.362662581,4940405.895683363],[-8238950.466616131,4940400.017711779],[-8238947.657547535,4940392.3849849515],[-8238945.11409344,4940392.73573034],[-8238942.305527516,4940389.929261504],[-8238937.129042457,4940389.403141825],[-8238935.1983199045,4940391.50943108],[-8238934.760071546,4940394.491535947],[-8238937.130929762,4940403.616515162],[-8238940.904904316,4940415.635203748],[-8238942.836926026,4940423.794706427],[-8238944.767039724,4940427.918536588],[-8238948.014862836,4940435.024177423],[-8238922.816946064,4940441.59762351],[-8238919.094749219,4940431.924775671],[-8238915.00094695,4940421.044283528],[-8238910.813342745,4940408.767483433],[-8238909.325838558,4940405.79084502],[-8238906.999623831,4940405.140519554],[-8238904.024286517,4940405.512359525],[-8238901.327015839,4940407.186765447],[-8238900.024601516,4940409.97805064],[-8238900.58396962,4940417.139345931],[-8238906.725878015,4940446.994816436],[-8238883.568379891,4940453.787827923],[-8238881.707859478,4940449.322870097],[-8238878.4508531485,4940440.952645221],[-8238875.101329387,4940431.838277278],[-8238871.7058328735,4940422.258911267],[-8238867.2387825465,4940407.378194342],[-8238859.144492026,4940383.103983464],[-8238850.31037716,4940357.267091611],[-8238846.273133933,4940351.699134249],[-8238844.523908025,4940352.689204028],[-8238842.94169014,4940356.389982449],[-8238856.109052423,4940388.403073449],[-8238853.989798779,4940389.872539114],[-8238856.254336332,4940394.31270032],[-8238857.836356036,4940393.534456469],[-8238860.947471044,4940401.035071882],[-8238858.54796412,4940403.680909197],[-8238858.4648998445,4940405.2841602685],[-8238859.728338903,4940406.60772763],[-8238862.1880663615,4940406.424841682],[-8238865.543695516,4940415.1501652915],[-8238863.490375837,4940416.8017664645],[-8238865.869531261,4940420.870321162],[-8238867.235969571,4940420.0972955525],[-8238867.784505299,4940422.363056514],[-8238866.023536415,4940423.39217357],[-8238867.576806821,4940426.433775673],[-8238869.059142699,4940426.581103071],[-8238870.112398661,4940429.327373472],[-8238868.545627298,4940430.783301714],[-8238871.006389361,4940433.124615092],[-8238873.449147552,4940439.778336488],[-8238873.04952958,4940441.081631295],[-8238875.596657894,4940446.993485779],[-8238877.227435262,4940446.89212688],[-8238877.281894324,4940449.108727535],[-8238878.951732076,4940454.229560322],[-8238874.30104578,4940456.207231396],[-8238875.558294329,4940462.37677088],[-8238881.158293734,4940478.141237111],[-8238890.1832310865,4940475.169898492],[-8238899.097837002,4940505.213412614],[-8238878.828191364,4940508.9846443795],[-8238886.968908365,4940534.673701397],[-8238873.071268795,4940535.0816718545],[-8238709.297116774,4940591.133068776],[-8238601.932970304,4940403.14428324],[-8238571.755809528,4940420.28475168],[-8238592.5546893915,4940456.902718147],[-8238487.385259196,4940516.636903598],[-8238505.655994069,4940548.804779992],[-8238610.75854619,4940489.10706127],[-8238676.557590332,4940604.690384831],[-8238660.003930948,4940610.947512746],[-8238656.480854277,4940612.559657619],[-8238573.114833632,4940679.942458881],[-8238527.970880329,4940716.21665377],[-8238499.2546887295,4940739.144817113],[-8238493.417614631,4940750.68515351],[-8238488.901329651,4940760.188008284],[-8238480.237184279,4940778.010026214],[-8238479.335835412,4940779.010366408],[-8238446.097371535,4940805.362732045],[-8238420.960694452,4940825.72576759],[-8238397.623886327,4940844.293625827],[-8238394.328153686,4940840.39724353],[-8238372.263845223,4940814.310478759],[-8238371.328688187,4940814.9638499785],[-8238365.255518065,4940805.153687002],[-8238361.474708123,4940800.8619097965],[-8238350.725585283,4940788.660421226],[-8238336.808175546,4940772.862417782],[-8238320.227788954,4940754.041573986],[-8238308.488308915,4940740.71571726],[-8238274.081580909,4940700.352244511],[-8238261.001261292,4940712.101299124],[-8238258.523444558,4940709.3405401865],[-8238264.494017283,4940704.544257406],[-8238264.048686885,4940703.274553096],[-8238264.811296232,4940702.511846442],[-8238264.62023451,4940700.607137518],[-8238265.635604862,4940699.781217418],[-8238265.128416958,4940698.004296371],[-8238266.7155865375,4940695.780767473],[-8238265.889520102,4940693.62196311],[-8238267.22379687,4940692.478443196],[-8238254.760767051,4940677.9426650405],[-8238253.938937154,4940678.017798663],[-8238232.771303303,4940696.81156163],[-8238246.481668012,4940712.253648359],[-8238247.451190766,4940711.3934499845],[-8238251.381917472,4940715.821591172],[-8238255.167310966,4940712.45995025],[-8238257.562175226,4940715.157371564],[-8238252.777070408,4940719.404333172],[-8238261.080638376,4940729.195004435],[-8238297.709161301,4940772.383506348],[-8238299.038849637,4940773.9566753125],[-8238300.379125339,4940775.542463613],[-8238343.665755533,4940826.7564794095],[-8238345.284053699,4940828.671082169],[-8238345.947778887,4940829.466367279],[-8238376.573062078,4940861.521159882],[-8238358.391648257,4940876.922104846],[-8238348.324234471,4940885.498616074],[-8238325.939213226,4940904.6828481285],[-8238312.51608806,4940889.166910759],[-8238298.135094706,4940901.593507053],[-8238272.755087626,4940922.714348005],[-8238237.908284442,4940952.823251463],[-8238227.899108677,4940962.244693084],[-8238222.472362779,4940966.785678073],[-8238219.849861389,4940968.980259643],[-8238190.052705185,4940993.916042738],[-8238181.174973907,4941001.050642533],[-8238152.855364074,4941024.4069112595],[-8238125.501823441,4941048.290930046],[-8238093.904516748,4941075.298853657],[-8237961.460307159,4940918.469645232],[-8237934.973644753,4940940.078608205],[-8238046.505191925,4941071.815677239],[-8238037.792270418,4941078.089462428],[-8238041.2776851775,4941081.923097558],[-8238049.990534359,4941075.649306797],[-8238069.036959216,4941098.1006768225],[-8238048.018338338,4941115.426093777],[-8238046.616736249,4941116.614260929],[-8238041.134512339,4941121.196021157],[-8238012.241463954,4941145.692862031],[-8237908.005755439,4941023.852957679],[-8237909.299297129,4941023.744926834],[-8237919.270628849,4941014.797860392],[-8237919.325933062,4941013.775713025],[-8237912.904186156,4941006.620655202],[-8237911.777903196,4941006.982851787],[-8237902.13068573,4941016.09187566],[-8237902.076746317,4941016.900629837],[-8237885.6211768035,4940996.258508963],[-8237884.0878736405,4940996.196720027],[-8237868.521442662,4941008.828816402],[-8237861.63461647,4941014.4174099425],[-8237861.696465741,4941015.398679367],[-8237907.6881805975,4941068.5564242285],[-8237911.237066641,4941072.65821582],[-8237905.350234583,4941076.996157675],[-8237941.225675663,4941119.458123005],[-8237928.370897959,4941130.331248034],[-8237882.56658889,4941105.981600145],[-8237878.626255027,4941109.182699232],[-8237871.950868366,4941114.605865602],[-8237871.795554581,4941114.741581707],[-8237871.099990519,4941113.914335415],[-8237804.21889632,4941035.2098397715],[-8237736.409836271,4941090.973373895],[-8237708.264601638,4941113.728421086],[-8237798.813307566,4941226.149414507],[-8237798.908147442,4941226.26280205],[-8237798.759349017,4941226.372027495],[-8237751.719815043,4941260.97889271],[-8237773.232738507,4941291.823970036],[-8237773.278922737,4941292.918657985],[-8237772.46603872,4941293.568130223],[-8237773.143161164,4941294.920883992],[-8237771.492751292,4941297.111533755],[-8237778.909164554,4941307.5193413],[-8237779.479556722,4941308.318920381],[-8237778.53762335,4941309.039463864],[-8237768.255084716,4941316.907750992],[-8237774.4385324735,4941325.798051838],[-8237740.568280292,4941349.997235277],[-8237736.910622457,4941352.610546805],[-8237728.229763169,4941358.027397794],[-8237703.820418875,4941373.259404733],[-8237686.877481035,4941384.3775093565],[-8237668.34571094,4941397.084080992],[-8237657.757445615,4941405.555622208],[-8237641.92353588,4941418.222809235],[-8237639.226969029,4941420.380625573],[-8237637.499973486,4941424.008461777],[-8237636.592374556,4941428.0301184235],[-8237636.202425498,4941431.531553551],[-8237636.592371738,4941432.829000121],[-8237629.291003577,4941437.596337294],[-8237598.846258374,4941457.47443849],[-8237594.617685678,4941451.217141578],[-8237592.359990306,4941447.875225259],[-8237590.93324619,4941447.746093018],[-8237585.615308371,4941451.896855362],[-8237588.007441917,4941455.82631952],[-8237589.247069392,4941457.862893135],[-8237582.242996334,4941462.792270778],[-8237577.897154026,4941466.099917494],[-8237573.551284236,4941469.407734942],[-8237567.325739562,4941474.985140713],[-8237559.881577392,4941480.00925004],[-8237552.652086779,4941484.8885732135],[-8237545.954821278,4941490.011506812],[-8237555.8939534575,4941500.347383092],[-8237563.984681811,4941508.761101587],[-8237568.235522539,4941513.616666655],[-8237572.94633053,4941518.907033324],[-8237586.115384329,4941533.410753788],[-8237615.490048737,4941563.36265813],[-8237627.783280741,4941575.898290748],[-8237637.681538216,4941586.143168399],[-8237649.53620289,4941598.413956662],[-8237657.399276715,4941606.553670626],[-8237665.263736053,4941614.693458563],[-8237695.138865481,4941646.406053194],[-8237778.587098531,4941734.986076845],[-8237800.31953413,4941758.066985747]]]]},"properties":{"shape_area":19245011.7093,"ntaname":"Financial District-Battery Park City","cdtaname":"MN01 Financial District-Tribeca (CD 1 Equivalent)","shape_leng":45306.3321199,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0101","borocode":1,"countyfips":"061","ntaabbrev":"FiDi","cdta2020":"MN01"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8237565.777214913,4942889.7408028655],[-8237637.443797543,4942958.044358142],[-8237701.535587562,4943018.490199773],[-8237783.998736478,4943096.207410126],[-8237798.2279014345,4943109.549946408],[-8237852.051499515,4943160.019400287],[-8237931.751332763,4943234.031243485],[-8238005.170203167,4943303.334585252],[-8238033.257560163,4943329.562015653],[-8238079.724477665,4943373.175300909],[-8238158.302453943,4943446.214759518],[-8238224.203588022,4943503.346116274],[-8238233.40961125,4943515.064408371],[-8238300.539484476,4943577.798303314],[-8238343.455213876,4943621.189137034],[-8238491.579573225,4943759.927711398],[-8238523.085093321,4943789.493494369],[-8238541.596661361,4943808.090116276],[-8238566.511147144,4943830.176468443],[-8238596.427943042,4943858.203551994],[-8238604.542649245,4943865.80668044],[-8238679.549587092,4943936.075081645],[-8238725.6268651225,4943978.335717166],[-8238761.456405217,4944015.513214433],[-8238845.947456763,4944095.310914751],[-8238858.236110565,4944095.947271896],[-8238860.080931399,4944079.920417106],[-8238860.457803936,4944076.645857539],[-8238873.088677546,4944001.460412006],[-8238880.470709569,4943953.477023264],[-8238883.4490525685,4943934.117228564],[-8238892.960728626,4943872.290948531],[-8238906.271662024,4943873.980994626],[-8238915.738964644,4943875.047017612],[-8238932.648021379,4943876.969010816],[-8238974.180730257,4943881.689301665],[-8238988.687678677,4943790.559364096],[-8239003.846065482,4943695.335861199],[-8239019.420636567,4943597.496999405],[-8239032.889533682,4943508.562945203],[-8239038.126866069,4943468.095017279],[-8239101.023611427,4943475.618349301],[-8239101.98140466,4943469.081721143],[-8239118.192126944,4943471.69383529],[-8239117.931042291,4943474.047013391],[-8239151.93243957,4943478.149342412],[-8239151.953520992,4943479.425217985],[-8239157.998507619,4943480.2400025455],[-8239160.186275678,4943470.053298656],[-8239118.631042144,4943463.505843217],[-8239117.725053781,4943466.155388511],[-8239102.45562207,4943463.787146972],[-8239107.357347913,4943431.631558575],[-8239381.402751642,4943473.988141825],[-8239389.446837516,4943424.676042635],[-8239078.039872373,4943375.166584057],[-8239089.4505746,4943296.896029008],[-8239147.268196332,4943305.981388493],[-8239256.139899014,4943323.32445742],[-8239457.464570968,4943354.532740647],[-8239465.56308027,4943303.2631703485],[-8239391.48293618,4943292.557149611],[-8239391.56921346,4943287.763832502],[-8239375.968763257,4943285.3263878105],[-8239375.968484365,4943283.75769995],[-8239230.510172354,4943260.861875574],[-8239230.248935952,4943262.082030337],[-8239200.791533766,4943257.9907378815],[-8239200.181786945,4943260.692494274],[-8239135.532205994,4943251.0114501985],[-8239140.128721692,4943219.148641827],[-8239079.33395383,4943209.366938558],[-8239084.740523074,4943175.029679176],[-8239084.489692316,4943173.462639322],[-8239090.777637032,4943133.888158647],[-8239092.88750529,4943121.084244981],[-8239056.263112301,4943115.278624403],[-8239039.9001388,4943112.072654976],[-8239027.254669314,4943110.289100086],[-8239010.61990665,4943107.5495111],[-8239014.379947979,4943083.189876133],[-8239054.747178189,4942852.850265178],[-8239059.490263693,4942825.862092408],[-8239080.708701133,4942721.194340526],[-8239092.251283868,4942655.9316142015],[-8239100.867511137,4942607.214653602],[-8239113.75262933,4942547.095146103],[-8239117.896761841,4942526.077371007],[-8239138.33851615,4942433.303604083],[-8239170.08412175,4942323.2714528125],[-8239098.896088975,4942283.608404662],[-8239039.035817401,4942248.447705436],[-8238954.468311393,4942199.276500339],[-8238942.433338367,4942191.932309318],[-8238769.058375801,4942090.133274991],[-8238626.248825451,4942005.175421621],[-8238599.761320107,4941988.831806863],[-8238571.570373433,4941983.90261964],[-8238529.29872751,4941996.891369566],[-8238536.085929993,4941997.71879114],[-8238542.765855358,4941999.178021077],[-8238549.279853727,4942001.256211265],[-8238555.570674165,4942003.935138364],[-8238556.935760781,4942007.830796672],[-8238557.626862273,4942011.900441345],[-8238557.62432609,4942016.028350847],[-8238556.928223493,4942020.097146599],[-8238555.558347566,4942023.9911306165],[-8238553.553650574,4942027.599575532],[-8238550.971136312,4942030.819873139],[-8238547.884239325,4942033.560452285],[-8238544.922591036,4942035.457603321],[-8238541.719637705,4942036.910750566],[-8238535.325900826,4942038.340251802],[-8238528.821424482,4942039.1245619245],[-8238522.271147978,4942039.255850402],[-8238515.740467899,4942038.732806447],[-8238510.113105307,4942035.198320572],[-8238504.084442359,4942032.402789402],[-8238497.751011105,4942030.390974725],[-8238491.214218979,4942029.1951092305],[-8238484.5787407765,4942028.8343218975],[-8238477.950824499,4942029.314389585],[-8238423.5587773165,4942046.136580999],[-8238326.859523881,4942076.183447021],[-8238317.936197715,4942078.975737747],[-8238281.342230898,4942090.727297248],[-8238264.519922626,4942102.6205216795],[-8238246.836830107,4942107.542040559],[-8238229.597914704,4942114.234317702],[-8238213.067631632,4942122.621833836],[-8238197.487471257,4942132.582017497],[-8238183.073190417,4942143.946763919],[-8238166.246427756,4942161.717758972],[-8238120.006027721,4942210.552596309],[-8238125.654336498,4942197.647117635],[-8238133.404955119,4942179.816910615],[-8238142.059364997,4942157.725008234],[-8238143.308956632,4942136.959525567],[-8238139.195998683,4942117.554296995],[-8238133.982356117,4942106.105073077],[-8238113.096579365,4942081.47172398],[-8238088.382909413,4942056.005735866],[-8238084.397267781,4942051.756762751],[-8238080.567462152,4942048.287309539],[-8238057.647997198,4942027.524646664],[-8238035.381069921,4942003.479945837],[-8238027.629691407,4941995.004442811],[-8238006.774151418,4941973.051297426],[-8238001.393760265,4941967.330242689],[-8237944.41123671,4941906.32865029],[-8237933.790581336,4941895.244578574],[-8237828.105530949,4941786.041686358],[-8237809.489778417,4941767.039374147],[-8237800.31953413,4941758.066985747],[-8237785.09544753,4941784.290379603],[-8237779.7721830765,4941793.254909574],[-8237768.82954691,4941810.902006918],[-8237754.000474462,4941836.951142871],[-8237743.211507713,4941855.062020415],[-8237726.130294009,4941883.734941383],[-8237716.197951464,4941900.407881909],[-8237699.076092533,4941929.149314699],[-8237711.364513891,4941943.444037972],[-8237721.811470289,4941959.557897757],[-8237730.039901782,4941977.109129187],[-8237735.78518377,4941995.622246055],[-8237738.92338641,4942014.567422285],[-8237739.47424305,4942033.411295414],[-8237741.578060778,4942223.928689593],[-8237741.787482408,4942228.418957971],[-8237742.105775624,4942230.973743172],[-8237746.039747545,4942262.550139808],[-8237782.249457638,4942296.714119721],[-8237816.684595924,4942323.967097307],[-8237750.120922893,4942356.522798241],[-8237725.421243908,4942444.997900859],[-8237692.92785773,4942423.739879753],[-8237701.022800357,4942541.512752517],[-8237730.211139916,4942559.201816089],[-8237706.704949575,4942600.156502246],[-8237657.704066467,4942692.15529433],[-8237634.635085794,4942742.510419996],[-8237580.912845467,4942857.488537143],[-8237565.777214913,4942889.7408028655]]]},"properties":{"shape_area":13577181.4049,"ntaname":"Tribeca-Civic Center","cdtaname":"MN01 Financial District-Tribeca (CD 1 Equivalent)","shape_leng":21222.4437187,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0102","borocode":1,"countyfips":"061","ntaabbrev":"TriBeCa","cdta2020":"MN01"}},{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8238858.863865515,4938054.262987884],[-8238703.520440219,4938178.586968017],[-8238599.746861389,4938261.6364725055],[-8238589.975061184,4938269.384691598],[-8238580.203318934,4938277.132735551],[-8238551.147197455,4938300.411315497],[-8238572.333527704,4938333.385012227],[-8238599.447390704,4938311.795270028],[-8238610.308343424,4938303.131337483],[-8238620.3607621435,4938296.299804976],[-8238768.455095196,4938178.579685174],[-8238997.741053573,4937996.3130074525],[-8238970.649559089,4937965.979365056],[-8238858.863865515,4938054.262987884]]],[[[-8238199.331630654,4938509.716849829],[-8238468.322853804,4938597.5055336235],[-8238514.277334727,4938477.408184631],[-8238269.03212048,4938389.541025435],[-8238199.331630654,4938509.716849829]]],[[[-8238067.602408826,4938703.290010755],[-8238350.824097302,4938811.513484986],[-8238397.5529187815,4938688.261461171],[-8238153.57621044,4938598.811836986],[-8238067.602408826,4938703.290010755]]],[[[-8237973.566957503,4938922.207384819],[-8238236.998136463,4939019.46512322],[-8238283.408395941,4938894.394845081],[-8238021.830845168,4938803.264330013],[-8237973.566957503,4938922.207384819]]],[[[-8242526.752543546,4938887.269245432],[-8242607.854257239,4938943.692051086],[-8242747.4610085795,4939014.275339864],[-8242749.25828182,4939015.299128171],[-8242751.992193208,4939016.765980912],[-8242757.115165717,4939019.236591442],[-8242762.367839595,4939021.417920481],[-8242767.557430464,4939022.682713068],[-8242772.830706073,4939023.53366787],[-8242774.465124988,4939023.71126892],[-8242775.283723159,4939023.785172345],[-8242776.923402851,4939023.903136165],[-8242779.387170523,4939024.005083339],[-8242782.125421625,4939020.3980601635],[-8242873.990358096,4939013.28238164],[-8242875.094063086,4939013.211448776],[-8242877.834729914,4939012.85200233],[-8242878.919467123,4939012.636012357],[-8242880.529320206,4939012.235497203],[-8242885.186657262,4939010.496726242],[-8242889.482204024,4939007.994251417],[-8242893.29183694,4939004.800384676],[-8242895.840463159,4939001.38598362],[-8242897.84596647,4938997.626785151],[-8242899.262625886,4938993.608492182],[-8242900.05814503,4938989.422714324],[-8242900.214388412,4938985.16487927],[-8242899.727794841,4938980.932057266],[-8242898.609458481,4938976.820748021],[-8242898.226608163,4938975.804975609],[-8242897.80505581,4938974.804732843],[-8242897.3453901615,4938973.82136691],[-8242896.585717385,4938972.3813301455],[-8242896.033589612,4938971.446671811],[-8242894.729998689,4938969.120066749],[-8242887.22769316,4938955.731373636],[-8242886.636203986,4938928.618324935],[-8242896.6312845815,4938921.949924502],[-8242862.318314306,4938868.465515712],[-8242922.342403616,4938831.142646325],[-8242939.536738034,4938857.264480905],[-8242955.556398882,4938847.43844522],[-8242927.380134813,4938802.987759132],[-8242852.3879456,4938852.98674199],[-8242793.671410473,4938761.462829963],[-8242627.649709705,4938654.447504166],[-8242583.25839646,4938642.823900802],[-8242537.5470768735,4938652.509355041],[-8242500.388710137,4938679.440154783],[-8242486.539027527,4938741.516293505],[-8242481.101166605,4938806.734519405],[-8242394.9716167655,4938859.421924363],[-8242388.903019086,4938849.318874861],[-8242379.377185618,4938854.376508435],[-8242396.137808152,4938882.520584073],[-8242405.0488078445,4938877.514914986],[-8242399.59479732,4938867.1628288515],[-8242485.38002992,4938814.1007863],[-8242526.752543546,4938887.269245432]]],[[[-8238133.172267546,4939229.653462522],[-8238176.0013669245,4939117.453768177],[-8237720.137492702,4938949.304105656],[-8237789.886417199,4939073.795810778],[-8237861.549629896,4939129.385939033],[-8238133.172267546,4939229.653462522]]],[[[-8239506.648660397,4939348.7849523695],[-8239574.712216297,4939343.300870377],[-8239641.660663821,4939337.906072508],[-8239667.400815844,4939335.831805198],[-8239734.802988589,4939330.39981994],[-8239740.774321463,4939330.39853675],[-8239742.450597183,4939330.40780485],[-8239758.7676021755,4939330.717166384],[-8239775.072200138,4939331.424605043],[-8239785.344125581,4939332.227379333],[-8239795.645960437,4939332.056780024],[-8239805.885672453,4939330.9143312285],[-8239815.971784388,4939328.810239459],[-8239818.074747492,4939328.267510119],[-8239823.938771034,4939326.14836992],[-8239829.450380782,4939323.232951382],[-8239834.50208002,4939319.578116794],[-8239838.9953423245,4939315.255149848],[-8239842.842533189,4939310.348365278],[-8239845.804018698,4939305.286349451],[-8239846.118027355,4939304.645426921],[-8239852.705202429,4939284.8532418525],[-8239891.55955342,4939157.277867185],[-8239920.347918794,4939125.697914938],[-8239948.434592488,4939091.511012798],[-8239948.824823633,4939091.036123487],[-8239977.797391688,4939053.5097525725],[-8240003.730223491,4939021.273129337],[-8240033.056228836,4938983.637505571],[-8240035.286746369,4938980.775125094],[-8240080.151200999,4938924.678318801],[-8240119.304340714,4938877.6964367125],[-8240140.299079097,4938851.021506604],[-8240154.285207567,4938834.340711062],[-8240167.563435037,4938818.504276152],[-8240217.577958387,4938756.787023533],[-8240246.119548442,4938720.781042477],[-8240262.383721472,4938701.261008819],[-8240277.736939412,4938683.205694957],[-8240292.996266658,4938664.517005198],[-8240310.5632001655,4938642.731943044],[-8240310.860939176,4938642.362801606],[-8240327.342171942,4938621.189708079],[-8240335.706512986,4938610.444218656],[-8240349.952225056,4938593.698313872],[-8240351.749179373,4938591.585506035],[-8240364.843036967,4938576.193665074],[-8240376.759777654,4938562.16914635],[-8240397.368692265,4938536.192591383],[-8240420.849283961,4938504.191668389],[-8240423.583499949,4938500.812999957],[-8240442.478974364,4938477.764907439],[-8240446.227631599,4938473.192455069],[-8240468.38577677,4938445.506076429],[-8240489.839958469,4938420.726334805],[-8240512.566047737,4938391.076575457],[-8240543.955763985,4938352.449407304],[-8240556.093124062,4938337.60723224],[-8240561.144008476,4938331.098942429],[-8240571.435479801,4938318.017953044],[-8240578.505651779,4938308.688361583],[-8240584.022233192,4938300.502274185],[-8240591.840090664,4938282.955789563],[-8240600.1015567705,4938261.025113131],[-8240604.408969259,4938240.173882394],[-8240605.842074523,4938224.356423655],[-8240606.555347143,4938205.663369692],[-8240604.39182546,4938183.735879896],[-8240600.791067207,4938163.965800054],[-8240594.314464722,4938144.5561658],[-8240585.680204989,4938121.552332545],[-8240573.810895425,4938099.6280234195],[-8240562.494209308,4938084.794546193],[-8240550.62744322,4938069.18431204],[-8240541.805364466,4938057.579242271],[-8240523.825751372,4938037.4540983355],[-8240500.094589613,4938016.971265952],[-8240477.444424367,4938004.755983404],[-8240449.403230869,4937996.136760272],[-8240424.533478806,4937990.014210962],[-8240407.70150402,4937984.286263996],[-8240386.131798611,4937979.259625728],[-8240362.046471427,4937975.671689036],[-8240337.96166021,4937974.240503518],[-8240308.125905377,4937974.967642278],[-8240289.79379122,4937977.848383618],[-8240262.958038724,4937982.83015648],[-8240248.841618255,4937987.600161466],[-8240246.946382453,4937988.384941153],[-8240231.251960693,4937994.88413264],[-8240217.705931199,4938000.54934349],[-8240204.56438263,4938006.82102427],[-8240175.837913364,4938023.989253787],[-8240132.339296579,4937950.210820013],[-8240224.33291893,4937836.236309438],[-8240208.87277335,4937824.018478226],[-8240106.097540221,4937948.42016103],[-8240157.426283044,4938034.992878517],[-8240135.577606006,4938048.050461708],[-8240128.698421088,4938052.057858077],[-8240125.65992248,4938047.290223612],[-8240128.9863144625,4938045.306020377],[-8240127.770268877,4938043.514990295],[-8240124.443844827,4938045.17929832],[-8240122.459627227,4938041.78899133],[-8240121.500041891,4938042.173044847],[-8240113.4354126975,4938028.931651642],[-8240111.836308422,4938030.08367977],[-8240119.709100656,4938043.645018989],[-8240114.463494693,4938046.333458586],[-8240106.590691409,4938032.708147291],[-8240104.543738948,4938033.860287833],[-8240112.352551597,4938047.485618602],[-8240111.648813813,4938047.933570763],[-8240118.152006891,4938058.2015998345],[-8240073.435665642,4938084.250558672],[-8239986.959127327,4938134.625577454],[-8239936.910893039,4938163.779667269],[-8239899.677429547,4938100.715552594],[-8239900.8798423195,4938097.105454541],[-8239981.005281933,4938048.7157841185],[-8239974.745910521,4938038.610036697],[-8239801.7413751315,4938141.407881007],[-8239808.548559206,4938151.2865522],[-8239886.830153153,4938104.207930923],[-8239891.736858901,4938104.327135026],[-8239929.229911909,4938168.411963068],[-8239886.137423816,4938194.400739224],[-8239548.218703414,4938398.188220675],[-8239509.53224392,4938333.176580413],[-8239510.3866142705,4938325.9032732],[-8239498.405935166,4938319.487964831],[-8239492.416616199,4938321.628325763],[-8239328.131150039,4938338.771542974],[-8239328.491404537,4938344.161466314],[-8239328.62106899,4938346.102869363],[-8239328.988962793,4938351.606469544],[-8239495.414284799,4938335.746295514],[-8239503.116020881,4938340.023281975],[-8239540.301113701,4938402.962853041],[-8239532.687144304,4938407.554366843],[-8239481.23607106,4938438.581227931],[-8239434.327874136,4938467.103771679],[-8239428.696567446,4938470.5278093545],[-8239298.861790524,4938458.326258657],[-8239142.063769971,4938569.100865612],[-8239136.06925399,4938574.970859878],[-8239126.229753865,4938580.962147971],[-8239118.956948374,4938585.241739179],[-8239108.3144679,4938589.9711763],[-8239062.144486807,4938672.712462941],[-8239056.433486337,4938682.947001806],[-8239012.314656051,4938754.553696083],[-8239003.902066706,4938927.290036155],[-8238993.98400548,4938926.260031892],[-8238992.367770552,4938937.1801631255],[-8238990.143225363,4938937.180470857],[-8238989.132513964,4938939.809531993],[-8238984.481581805,4938940.821248097],[-8238983.673872205,4938949.112456157],[-8238975.989388274,4938948.506884359],[-8238977.405824297,4938954.977768177],[-8238976.597766815,4938961.8534356365],[-8238974.981454376,4938972.571451912],[-8238976.801486442,4938972.773354945],[-8238978.014518374,4938969.5376558425],[-8238984.080836356,4938967.919044658],[-8238987.316884639,4938970.951979426],[-8238987.519454969,4938973.9851859985],[-8239001.549066307,4938975.604792905],[-8238999.9188457215,4939009.077293183],[-8238999.85999275,4939010.2873265585],[-8238959.059025456,4939007.729494912],[-8238958.755565647,4939015.5476359],[-8238998.949382808,4939017.479275909],[-8238998.585289897,4939025.989645005],[-8239036.910592332,4939027.4698718265],[-8239044.669496925,4939054.964065955],[-8238980.096964057,4939073.080079954],[-8238981.957065126,4939079.642176],[-8239035.983831646,4939064.104027307],[-8239037.406392416,4939068.806651843],[-8239039.921786087,4939067.931304001],[-8239038.827448892,4939063.337954598],[-8239043.2020648895,4939062.134204497],[-8239043.201797782,4939060.274933728],[-8239045.898137744,4939059.317653906],[-8239047.740292106,4939065.845209488],[-8239050.590144743,4939064.4883266045],[-8239062.128494326,4939075.750654338],[-8239068.983937191,4939079.210749694],[-8239074.496568379,4939086.080685959],[-8239075.579226906,4939087.430410962],[-8239077.803969457,4939090.202276167],[-8239077.993295732,4939090.438041982],[-8239073.873003519,4939094.254297081],[-8239081.457517616,4939104.947184347],[-8239079.707973595,4939107.0862739375],[-8239110.240444597,4939146.163596867],[-8239117.890427164,4939140.15101816],[-8239118.9451615065,4939141.4652485205],[-8239119.10584324,4939141.66674261],[-8239120.064859909,4939142.861056497],[-8239124.672458813,4939148.294300549],[-8239125.702703451,4939150.152402441],[-8239126.533996385,4939151.970941656],[-8239126.689502307,4939152.451745136],[-8239126.781728397,4939152.969235887],[-8239126.808076603,4939153.517093129],[-8239126.770015541,4939154.086006352],[-8239126.501491009,4939155.2371416],[-8239126.279034253,4939155.7864088705],[-8239126.005266649,4939156.294718724],[-8239125.769524267,4939156.642469333],[-8239125.503593937,4939156.970550436],[-8239124.876661245,4939157.567239259],[-8239124.118222073,4939158.092748862],[-8239123.217497098,4939158.553794022],[-8239121.215717998,4939159.24421083],[-8239117.062132405,4939160.245818655],[-8239116.406322377,4939160.421098303],[-8239114.338687958,4939160.973118941],[-8239070.354103226,4939172.708870512],[-8239040.379091164,4939180.56515959],[-8239038.24561543,4939181.123838896],[-8239029.515588627,4939183.412726469],[-8239025.938775645,4939174.873867857],[-8239023.636118836,4939171.9491173215],[-8239021.5683393935,4939170.317632623],[-8239018.174002151,4939168.077857978],[-8239014.254270124,4939165.652563174],[-8239000.543523685,4939158.883865756],[-8238994.723593777,4939157.866594489],[-8238994.063838552,4939158.052377243],[-8238993.439558933,4939158.227644199],[-8238993.326296492,4939158.260662417],[-8238974.694183179,4939163.500877793],[-8238968.266087343,4939165.309911334],[-8238967.365424246,4939165.564229356],[-8238963.089624949,4939169.568482724],[-8238960.231072868,4939174.674786491],[-8238960.156094083,4939174.809208038],[-8238959.725571877,4939175.57837095],[-8238951.1565591,4939190.88830153],[-8238950.360848253,4939198.582386788],[-8238950.359550942,4939198.591550294],[-8238950.430734834,4939198.840420507],[-8238954.079197176,4939211.513402392],[-8238955.756480047,4939217.338364274],[-8238957.208550065,4939222.419144157],[-8238961.331445738,4939228.05536931],[-8238961.36565545,4939228.102749502],[-8238983.016340495,4939240.332117885],[-8238989.684384483,4939241.450697119],[-8238991.206766926,4939241.025051791],[-8238991.305673526,4939240.997425518],[-8239011.249537864,4939235.423617212],[-8239014.309237681,4939234.213716122],[-8239017.1284244675,4939232.517306303],[-8239019.630395458,4939230.380543022],[-8239021.747078623,4939227.861562408],[-8239022.790486372,4939226.225005025],[-8239023.669900711,4939224.494788593],[-8239036.36136095,4939201.899796247],[-8239034.1002930505,4939196.715025681],[-8239134.423099455,4939169.328213163],[-8239135.591905477,4939169.091258424],[-8239136.783707071,4939169.048084456],[-8239137.9665946895,4939169.199847205],[-8239139.108897449,4939169.542483356],[-8239140.180031086,4939170.066819104],[-8239141.151316817,4939170.758815817],[-8239141.996749216,4939171.599945838],[-8239142.693692435,4939172.567688598],[-8239225.418878791,4939272.66650982],[-8239329.490440591,4939300.835890542],[-8239316.534758794,4939318.793295932],[-8239310.349894077,4939314.145113852],[-8239305.782588359,4939319.191491424],[-8239317.443041693,4939327.779975478],[-8239326.447115806,4939334.222361586],[-8239330.521247992,4939327.420517263],[-8239326.179136193,4939324.516456738],[-8239340.999551372,4939303.950949945],[-8239346.526581257,4939305.4469065955],[-8239340.468949965,4939328.824986696],[-8239333.188529549,4939327.612830461],[-8239331.572163084,4939336.106744934],[-8239355.436351971,4939342.169523828],[-8239357.659288326,4939333.473162511],[-8239351.591995364,4939331.451996902],[-8239357.038655113,4939308.292144681],[-8239506.648660397,4939348.7849523695]]],[[[-8237813.943114008,4939267.263792293],[-8237783.71104509,4939341.866418045],[-8238026.758883471,4939452.370488162],[-8238059.279109191,4939388.687779288],[-8238088.482736389,4939329.016349705],[-8237836.417569382,4939211.80362184],[-8237813.943114008,4939267.263792293]]],[[[-8242089.577691489,4940452.651398186],[-8242127.168000856,4940416.52064582],[-8242129.886611553,4940413.907570996],[-8242184.243615848,4940361.6608451],[-8242217.395230224,4940394.482487979],[-8242425.166369946,4940176.02171581],[-8242396.101475579,4940146.94994069],[-8242502.139063059,4940039.225648369],[-8242499.951898899,4940036.906159376],[-8242504.054043933,4940032.835911879],[-8242279.946382101,4939803.582082481],[-8242085.35655007,4939986.271320075],[-8242289.650074451,4940197.7423224775],[-8242233.425899383,4940254.621708298],[-8242123.546049085,4940147.567671664],[-8242087.608833209,4940109.48046513],[-8242021.669330788,4940046.746997552],[-8241983.827589993,4940083.952726787],[-8241898.012050603,4940083.76270214],[-8241883.353182095,4940181.893257254],[-8241840.315525067,4940226.43616049],[-8242026.3897766555,4940406.878706128],[-8242034.1652388135,4940400.364780123],[-8242089.577691489,4940452.651398186]]],[[[-8239199.796263755,4940617.578539331],[-8239198.79716645,4940619.250146093],[-8239197.115713401,4940621.384509542],[-8239195.134666808,4940623.244146375],[-8239192.898363497,4940624.787436638],[-8239190.4568532,4940625.97984043],[-8239187.864778312,4940626.794670898],[-8239186.539895181,4940626.880368996],[-8239185.245730314,4940627.176674368],[-8239184.0155427875,4940627.67597221],[-8239182.88094749,4940628.365430994],[-8239181.871102677,4940629.227332175],[-8239181.011960634,4940630.239525578],[-8239180.325600752,4940631.375998644],[-8239179.829662006,4940632.607544869],[-8239179.536889753,4940633.902514442],[-8239179.45480811,4940635.227627606],[-8239179.585526596,4940636.54882987],[-8239180.691649818,4940649.540202345],[-8239182.358087038,4940674.261614145],[-8239223.70158445,4940722.750325623],[-8239230.012190497,4940731.790644234],[-8239235.510365571,4940741.346867954],[-8239240.154212192,4940751.346181607],[-8239243.908382943,4940761.712352623],[-8239246.744231659,4940772.366437443],[-8239248.640146485,4940783.227254025],[-8239244.403025703,4940848.777970234],[-8239229.204627729,4940969.702548125],[-8239229.002752587,4940974.396894991],[-8239229.580175487,4940979.059968032],[-8239230.92104043,4940983.563338348],[-8239232.988354876,4940987.782875498],[-8239242.014457308,4940989.09380805],[-8239258.357333093,4940996.062085458],[-8239275.052516491,4941002.1380835315],[-8239287.6869025165,4941006.072937233],[-8239300.469293313,4941009.496590462],[-8239308.921891005,4941011.75766107],[-8239331.002014435,4941018.439751712],[-8239343.01810448,4941021.651086288],[-8239356.3701531105,4941024.50917135],[-8239385.310923258,4941029.327401464],[-8239408.145386527,4941029.3427260155],[-8239446.397616827,4941025.159748862],[-8239456.420148957,4941024.48962577],[-8239465.197289216,4941030.116917266],[-8239481.361617679,4941024.446088184],[-8239493.295887037,4941024.404155811],[-8239493.389744948,4941016.282030118],[-8239494.373557845,4941000.119103769],[-8239501.334209788,4941000.087024985],[-8239509.184557032,4940999.94074736],[-8239517.607625399,4941000.334917705],[-8239549.359423708,4940948.794206209],[-8239598.241756287,4940934.363506845],[-8239604.500571108,4940910.903572796],[-8239605.326481036,4940911.08648466],[-8239606.428992682,4940910.720008442],[-8239607.62199937,4940908.882297414],[-8239607.529502283,4940907.229124525],[-8239606.335750446,4940905.852910473],[-8239607.98990326,4940897.616031026],[-8239608.864828618,4940892.726322215],[-8239609.811384862,4940883.237998332],[-8239610.1017965125,4940873.896282851],[-8239609.62863525,4940865.308015633],[-8239610.793022306,4940865.211573814],[-8239609.48106885,4940849.896022042],[-8239620.608987643,4940848.670750214],[-8239621.177817549,4940853.71832503],[-8239633.922905103,4940852.643047508],[-8239633.226474366,4940846.291196629],[-8239632.532278924,4940839.961134326],[-8239620.165618249,4940840.937258213],[-8239620.608565214,4940846.588535352],[-8239609.201976402,4940847.685162831],[-8239608.271086486,4940841.6390604675],[-8239606.882300402,4940833.53848348],[-8239604.438167511,4940823.030171194],[-8239603.413262546,4940823.101588732],[-8239601.1498589,4940814.0527391005],[-8239598.812811347,4940806.1710070595],[-8239594.90520384,4940795.735427314],[-8239589.720904637,4940783.840725962],[-8239583.734569109,4940772.529680283],[-8239579.098987554,4940763.991142247],[-8239570.191681836,4940749.324444998],[-8239567.41810955,4940744.726626808],[-8239562.945491639,4940738.697749716],[-8239562.1030158475,4940737.56202327],[-8239562.782669009,4940737.137761572],[-8239550.9003472,4940718.091949848],[-8239550.007371995,4940718.968166988],[-8239535.191749791,4940696.031928208],[-8239523.236316686,4940676.627920338],[-8239514.556244737,4940664.442883998],[-8239482.71787275,4940620.263647263],[-8239480.777080921,4940617.449238258],[-8239467.073773401,4940599.955543384],[-8239450.647124765,4940578.749248524],[-8239438.893540183,4940563.141463936],[-8239436.353701951,4940560.915961909],[-8239413.449909231,4940536.071486287],[-8239400.44498493,4940519.390471612],[-8239387.561394267,4940506.096776829],[-8239327.953844904,4940445.668776169],[-8239314.463908045,4940467.552310306],[-8239293.002211136,4940453.252888873],[-8239291.041134463,4940454.582196034],[-8239275.78264794,4940444.457111069],[-8239279.010177751,4940439.265596228],[-8239281.700563185,4940435.594301653],[-8239292.078325484,4940419.008032601],[-8239299.41949056,4940407.23265667],[-8239305.653594096,4940399.001988361],[-8239305.084518948,4940398.875620895],[-8239303.627922221,4940399.952055096],[-8239302.932383398,4940399.25530769],[-8239303.564275754,4940397.736498696],[-8239305.7163224565,4940396.407115185],[-8239306.412822813,4940394.697361277],[-8239309.323055866,4940391.152429247],[-8239308.501052864,4940390.455778005],[-8239311.348053533,4940386.277261698],[-8239313.057855615,4940386.403433257],[-8239314.63919663,4940383.745181166],[-8239316.096006999,4940383.934590958],[-8239323.056297181,4940371.272855078],[-8239319.701126807,4940368.931407941],[-8239311.980879983,4940381.782928224],[-8239304.355002431,4940392.482244203],[-8239296.44469305,4940403.624105464],[-8239291.223647134,4940411.728282869],[-8239286.666143768,4940419.451463222],[-8239281.414530226,4940426.858859346],[-8239272.395870915,4940441.926048026],[-8239236.841681356,4940419.108130884],[-8239244.741280575,4940405.823313534],[-8239244.974266917,4940404.799698527],[-8239246.374004587,4940403.022617665],[-8239254.066778842,4940389.968307474],[-8239254.772761499,4940389.832488393],[-8239255.612964334,4940388.884001217],[-8239255.719448724,4940387.494492652],[-8239262.261445865,4940378.398318068],[-8239268.502152897,4940369.721188397],[-8239268.543959186,4940367.680921244],[-8239273.579000892,4940357.871189216],[-8239275.060835892,4940357.394171252],[-8239275.50920666,4940353.155458598],[-8239271.250503039,4940351.2581830565],[-8239265.617821409,4940363.902433373],[-8239265.830476128,4940367.499653213],[-8239264.815121702,4940368.735230394],[-8239263.130458905,4940367.692408202],[-8239250.677344535,4940387.81674261],[-8239241.738870883,4940402.260744509],[-8239236.187239713,4940411.969241007],[-8239235.890830007,4940411.75578687],[-8239233.799682879,4940414.410343442],[-8239234.362218047,4940414.852767883],[-8239232.985995114,4940416.598259784],[-8239217.7553137755,4940406.903750453],[-8239219.154573045,4940401.226536056],[-8239220.593008425,4940401.580727542],[-8239235.280117294,4940341.995386824],[-8239235.279811318,4940340.122370466],[-8239225.35221021,4940338.688191124],[-8239220.236059894,4940360.540587262],[-8239217.33435966,4940369.269895818],[-8239216.071719233,4940380.149795071],[-8239215.064057682,4940379.905066348],[-8239210.971611012,4940396.733721845],[-8239212.469321988,4940397.098318984],[-8239211.031169204,4940403.064437136],[-8239195.3955730805,4940467.329827611],[-8239238.552143708,4940488.055831861],[-8239168.258906371,4940478.928382118],[-8239169.972003132,4940488.105753931],[-8239172.70374705,4940497.954803924],[-8239180.092297871,4940513.446290851],[-8239198.467878288,4940546.328341913],[-8239202.048167918,4940611.426614053],[-8239201.813202264,4940613.067647141],[-8239201.352936625,4940614.66024091],[-8239200.676265128,4940616.173619894],[-8239199.796263755,4940617.578539331]]]]},"properties":{"shape_area":11989896.2552,"ntaname":"The Battery-Governors Island-Ellis Island-Liberty Island","cdtaname":"MN01 Financial District-Tribeca (CD 1 Equivalent)","shape_leng":49169.681822,"boroname":"Manhattan","ntatype":9,"nta2020":"MN0191","borocode":1,"countyfips":"061","ntaabbrev":"Btry_GvIsl","cdta2020":"MN01"}}]} \ No newline at end of file diff --git a/tests/fixtures/runtime/mnt_neighs_proj.geojson b/tests/fixtures/runtime/mnt_neighs_proj.geojson new file mode 100644 index 00000000..b2e0faaa --- /dev/null +++ b/tests/fixtures/runtime/mnt_neighs_proj.geojson @@ -0,0 +1,40 @@ +{"type":"FeatureCollection", "features": [ +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8237729.476530929,4939486.723352737],[-8237720.358874049,4939501.544867631],[-8237672.23274935,4939586.072452652],[-8237909.265717435,4939695.158246201],[-8237935.70874332,4939644.024264006],[-8237977.828631048,4939558.58263889],[-8237749.03886236,4939454.9231396075],[-8237729.476530929,4939486.723352737]]],[[[-8237589.301427131,4939821.404307374],[-8237583.77207122,4939840.316885006],[-8237587.012725554,4939843.589038526],[-8237576.3489652015,4939866.797874732],[-8237592.658057331,4939875.579759889],[-8237596.421683646,4939868.6798342755],[-8237590.149048499,4939865.543428275],[-8237597.0489784395,4939851.743642718],[-8237690.350101905,4939891.263798308],[-8237696.791120582,4939872.795361239],[-8237589.301427131,4939821.404307374]]],[[[-8237516.657981151,4939906.637363263],[-8237461.714625767,4940039.109280011],[-8237696.607617705,4940140.686476795],[-8237759.0962517,4940016.429762579],[-8237516.657981151,4939906.637363263]]],[[[-8237419.632356523,4940146.523672253],[-8237363.906676442,4940269.559542211],[-8237583.835715242,4940369.592531709],[-8237644.367463572,4940244.28159099],[-8237422.02036553,4940141.265625634],[-8237419.632356523,4940146.523672253]]],[[[-8237229.314594233,4940530.839151823],[-8237221.129538602,4940545.304824099],[-8237128.521718537,4940708.973414901],[-8237114.6811444685,4940733.430168655],[-8237101.113143409,4940757.405117231],[-8237184.363546495,4940799.233813706],[-8237218.44439219,4940816.357226915],[-8237435.130754037,4940544.531651108],[-8237397.597035381,4940515.366436343],[-8237392.975325424,4940521.22017157],[-8237367.096037779,4940502.117574327],[-8237288.533210941,4940467.300480571],[-8237274.707158257,4940450.654381632],[-8237265.765010679,4940466.455978008],[-8237252.510031823,4940489.844330125],[-8237229.314594233,4940530.839151823]]],[[[-8237088.0973573215,4940782.82082595],[-8237063.499859484,4940803.897261015],[-8237083.721111736,4940817.0263839755],[-8237118.389846316,4940842.236856135],[-8237135.330013549,4940801.461332419],[-8237090.833774301,4940780.476137386],[-8237088.0973573215,4940782.82082595]]],[[[-8237060.044434256,4940900.575747768],[-8237067.506524534,4940905.467711655],[-8237072.590809772,4940908.73182727],[-8237088.978363566,4940879.4103815155],[-8237077.889487127,4940874.014977182],[-8237075.750438863,4940877.818261731],[-8237072.629967545,4940876.459444738],[-8237060.044434256,4940900.575747768]]],[[[-8237800.31953413,4941758.066985747],[-8237809.489778417,4941767.039374147],[-8237828.105530949,4941786.041686358],[-8237933.790581336,4941895.244578574],[-8237944.41123671,4941906.32865029],[-8238001.393760265,4941967.330242689],[-8238006.774151418,4941973.051297426],[-8238027.629691407,4941995.004442811],[-8238035.381069921,4942003.479945837],[-8238057.647997198,4942027.524646664],[-8238080.567462152,4942048.287309539],[-8238084.397267781,4942051.756762751],[-8238088.382909413,4942056.005735866],[-8238113.096579365,4942081.47172398],[-8238133.982356117,4942106.105073077],[-8238139.195998683,4942117.554296995],[-8238143.308956632,4942136.959525567],[-8238142.059364997,4942157.725008234],[-8238133.404955119,4942179.816910615],[-8238125.654336498,4942197.647117635],[-8238120.006027721,4942210.552596309],[-8238166.246427756,4942161.717758972],[-8238183.073190417,4942143.946763919],[-8238197.487471257,4942132.582017497],[-8238213.067631632,4942122.621833836],[-8238229.597914704,4942114.234317702],[-8238246.836830107,4942107.542040559],[-8238264.519922626,4942102.6205216795],[-8238281.342230898,4942090.727297248],[-8238317.936197715,4942078.975737747],[-8238326.859523881,4942076.183447021],[-8238423.5587773165,4942046.136580999],[-8238477.950824499,4942029.314389585],[-8238484.5787407765,4942028.8343218975],[-8238491.214218979,4942029.1951092305],[-8238497.751011105,4942030.390974725],[-8238504.084442359,4942032.402789402],[-8238510.113105307,4942035.198320572],[-8238515.740467899,4942038.732806447],[-8238522.271147978,4942039.255850402],[-8238528.821424482,4942039.1245619245],[-8238535.325900826,4942038.340251802],[-8238541.719637705,4942036.910750566],[-8238544.922591036,4942035.457603321],[-8238547.884239325,4942033.560452285],[-8238550.971136312,4942030.819873139],[-8238553.553650574,4942027.599575532],[-8238555.558347566,4942023.9911306165],[-8238556.928223493,4942020.097146599],[-8238557.62432609,4942016.028350847],[-8238557.626862273,4942011.900441345],[-8238556.935760781,4942007.830796672],[-8238555.570674165,4942003.935138364],[-8238549.279853727,4942001.256211265],[-8238542.765855358,4941999.178021077],[-8238536.085929993,4941997.71879114],[-8238529.29872751,4941996.891369566],[-8238571.570373433,4941983.90261964],[-8238599.761320107,4941988.831806863],[-8238626.248825451,4942005.175421621],[-8238769.058375801,4942090.133274991],[-8238942.433338367,4942191.932309318],[-8238954.468311393,4942199.276500339],[-8239039.035817401,4942248.447705436],[-8239098.896088975,4942283.608404662],[-8239170.08412175,4942323.2714528125],[-8239138.33851615,4942433.303604083],[-8239117.896761841,4942526.077371007],[-8239113.75262933,4942547.095146103],[-8239100.867511137,4942607.214653602],[-8239092.251283868,4942655.9316142015],[-8239080.708701133,4942721.194340526],[-8239059.490263693,4942825.862092408],[-8239054.747178189,4942852.850265178],[-8239014.379947979,4943083.189876133],[-8239010.61990665,4943107.5495111],[-8239027.254669314,4943110.289100086],[-8239039.9001388,4943112.072654976],[-8239056.263112301,4943115.278624403],[-8239092.88750529,4943121.084244981],[-8239097.529130375,4943092.870333089],[-8239097.047921944,4943090.467234024],[-8239113.208424985,4943001.735187999],[-8239279.015803812,4943031.060359425],[-8239306.005801588,4943035.0662979325],[-8239308.811841636,4943033.942036723],[-8239310.255625071,4943032.177646667],[-8239310.977096516,4943029.7716711555],[-8239311.6877513025,4943024.74658148],[-8239312.178615476,4943021.269539593],[-8239373.43447195,4943031.905770166],[-8239386.764208668,4943034.219563345],[-8239386.565621493,4943036.6688356465],[-8239417.177721561,4943041.755493941],[-8239417.52528334,4943039.750176101],[-8239493.557757829,4943052.891502876],[-8239496.842385705,4943052.987009126],[-8239500.707539052,4943050.47374617],[-8239502.488105307,4943046.864720037],[-8239519.496355883,4942924.373163319],[-8239532.828999296,4942828.351292426],[-8239548.244521247,4942717.328372634],[-8239564.162335575,4942603.34271793],[-8239571.007870812,4942553.926825367],[-8239577.29990381,4942508.507607481],[-8239577.364691719,4942508.064689222],[-8239580.247836084,4942488.367701634],[-8239594.552869613,4942384.604182368],[-8239603.582571579,4942319.762336783],[-8239613.438741759,4942248.802509519],[-8239615.452467895,4942234.304460663],[-8239615.239977365,4942233.858163045],[-8239614.962961485,4942233.448778197],[-8239614.627685494,4942233.085565094],[-8239614.2417323,4942232.776738515],[-8239613.8138309745,4942232.529283133],[-8239613.353659318,4942232.348795613],[-8239612.871624976,4942232.239358034],[-8239612.338148093,4942232.203792398],[-8239611.805963139,4942232.255189676],[-8239611.289152272,4942232.392189844],[-8239610.8013908565,4942232.611167732],[-8239610.355585549,4942232.906328981],[-8239609.963532823,4942233.269863324],[-8239609.635606802,4942233.692151263],[-8239609.380484765,4942234.1620186325],[-8239607.674828762,4942247.599985653],[-8239599.262483642,4942311.155236012],[-8239524.807762996,4942296.6480919765],[-8239483.670748532,4942288.561503206],[-8239459.060169779,4942283.723436232],[-8239467.0338270925,4942238.666465449],[-8239476.556895976,4942228.351898352],[-8239483.667948952,4942217.536443768],[-8239485.267300267,4942213.664174952],[-8239489.275499963,4942203.7845105585],[-8239492.270615205,4942188.363064055],[-8239492.245654052,4942172.799257304],[-8239489.362398494,4942157.936807521],[-8239484.0593937095,4942144.458230348],[-8239492.921107281,4942100.75733363],[-8239623.847815526,4942128.232174783],[-8239615.065885142,4942192.458461764],[-8239614.924083532,4942201.322026111],[-8239616.217920083,4942201.992265366],[-8239618.565502538,4942201.848195097],[-8239620.481921677,4942199.836229929],[-8239631.969167107,4942123.846245622],[-8239637.413619041,4942083.483594274],[-8239643.679764749,4942042.093807616],[-8239651.200943734,4941981.847986947],[-8239658.379011123,4941935.784659792],[-8239662.189001492,4941915.160750993],[-8239675.468101982,4941844.992090303],[-8239684.070163168,4941800.736465393],[-8239698.2584899515,4941725.1191283455],[-8239716.359337921,4941630.256008444],[-8239729.213875462,4941562.888336544],[-8239741.248716349,4941499.540158007],[-8239741.078228776,4941495.806656993],[-8239738.9054850545,4941492.911224649],[-8239738.682770831,4941492.464639243],[-8239735.195336602,4941489.751843378],[-8239718.0810899595,4941484.806243655],[-8239719.047310354,4941481.4614256015],[-8239667.252811443,4941466.49321013],[-8239670.225180824,4941456.207120141],[-8239670.255242944,4941454.875306247],[-8239691.319894995,4941384.025346009],[-8239705.106439282,4941339.096144849],[-8239705.64023582,4941334.355079954],[-8239707.1758769695,4941332.884748788],[-8239708.912012846,4941332.616969066],[-8239709.44727567,4941334.687607415],[-8239709.713793919,4941337.291828435],[-8239710.582355142,4941338.961916032],[-8239711.918414276,4941340.36324266],[-8239713.188712143,4941342.634065015],[-8239713.389343215,4941344.703403986],[-8239714.257659341,4941345.905986659],[-8239715.9943047725,4941347.9090779275],[-8239718.198415175,4941348.909812443],[-8239721.603564947,4941349.043430308],[-8239723.406665196,4941347.90749882],[-8239726.54571562,4941347.306179867],[-8239728.682420532,4941347.43876744],[-8239732.421844946,4941345.9021102395],[-8239734.959803711,4941343.697595742],[-8239736.360814484,4941341.360501429],[-8239740.568445769,4941341.960343132],[-8239740.501553798,4941343.829547373],[-8239739.433675392,4941345.5672706645],[-8239737.3647819655,4941348.639673389],[-8239734.493678107,4941351.044232297],[-8239732.223127592,4941352.780915209],[-8239729.551722915,4941353.582499565],[-8239730.888277452,4941357.389117481],[-8239729.61996063,4941357.990111706],[-8239726.347864597,4941358.258150688],[-8239722.540883647,4941358.592200103],[-8239718.200405646,4941358.259888545],[-8239717.800589432,4941361.73239308],[-8239716.665043707,4941361.399300028],[-8239713.66006092,4941360.26446405],[-8239709.652334724,4941357.994365416],[-8239706.981864638,4941362.068024447],[-8239710.7221198045,4941364.405304631],[-8239716.2657959135,4941366.741024239],[-8239719.037320527,4941367.007822762],[-8239722.843105555,4941367.140154297],[-8239726.917529112,4941367.273554222],[-8239730.7900897795,4941366.4705849765],[-8239734.729801075,4941365.000930254],[-8239738.402124535,4941363.196796094],[-8239741.740854191,4941360.99217728],[-8239744.545016628,4941358.854967346],[-8239747.282966017,4941355.71529277],[-8239749.218670726,4941352.777257893],[-8239750.4210474165,4941351.306989142],[-8239751.354510474,4941348.902697714],[-8239752.223205565,4941345.830551698],[-8239761.104893855,4941348.232714728],[-8239764.755194024,4941348.317521284],[-8239767.580420998,4941346.9574699765],[-8239768.805384805,4941346.367141165],[-8239771.054567276,4941342.765186532],[-8239782.14902458,4941283.853657207],[-8239795.521100831,4941213.724226824],[-8239795.41098369,4941210.340203733],[-8239749.120641236,4941020.649319637],[-8239744.863300317,4941014.122456582],[-8239742.125988963,4941012.727536041],[-8239658.651873799,4940988.44489695],[-8239653.315738797,4941000.472330743],[-8239652.0723407185,4940999.547704667],[-8239650.84847287,4940999.332995562],[-8239648.762029104,4941001.923963548],[-8239646.962694487,4941000.843817302],[-8239645.882689989,4941002.499648207],[-8239644.875012885,4941002.067604522],[-8239643.723743592,4941001.20343761],[-8239642.643590681,4941002.138662567],[-8239639.83780161,4941002.283599016],[-8239639.549349014,4941003.362410178],[-8239636.094313519,4941003.86692372],[-8239635.158942201,4941002.786745428],[-8239628.250193536,4941004.153987796],[-8239627.026461986,4941004.729574751],[-8239624.507954279,4941004.298865479],[-8239623.571405969,4941005.018068806],[-8239621.98817977,4941005.593728209],[-8239620.117678549,4941004.729606172],[-8239619.972776646,4941005.809833729],[-8239618.174657578,4941006.241787433],[-8239616.375501201,4941005.666176791],[-8239614.719673509,4941006.889870722],[-8239611.193391474,4941006.025763059],[-8239609.969672332,4941007.033353806],[-8239605.363406313,4941006.744897825],[-8239601.328607565,4941007.558896697],[-8239597.860420112,4941005.673926301],[-8239598.613929104,4941005.147007006],[-8239598.237178687,4941004.392267194],[-8239596.880429954,4941004.845350053],[-8239595.7488815915,4941004.543719259],[-8239593.939074809,4941005.598818678],[-8239588.284126854,4941005.070534079],[-8239592.24252346,4940992.397566751],[-8239719.41405064,4940970.186179438],[-8239695.834475127,4940931.214690113],[-8239628.749357752,4940942.870691559],[-8239641.031197033,4940903.924068735],[-8239639.690123645,4940903.542528817],[-8239636.305969126,4940915.437684217],[-8239624.72030487,4940912.141601086],[-8239628.016513958,4940902.005034107],[-8239625.763313971,4940898.385716048],[-8239622.84751537,4940900.285848893],[-8239618.300727903,4940915.49408454],[-8239630.023506375,4940918.999361503],[-8239623.1982661,4940941.825851163],[-8239598.241756287,4940934.363506845],[-8239549.359423708,4940948.794206209],[-8239517.607625399,4941000.334917705],[-8239509.184557032,4940999.94074736],[-8239501.334209788,4941000.087024985],[-8239494.373557845,4941000.119103769],[-8239493.389744948,4941016.282030118],[-8239493.295887037,4941024.404155811],[-8239481.361617679,4941024.446088184],[-8239465.197289216,4941030.116917266],[-8239456.420148957,4941024.48962577],[-8239446.397616827,4941025.159748862],[-8239408.145386527,4941029.3427260155],[-8239385.310923258,4941029.327401464],[-8239356.3701531105,4941024.50917135],[-8239343.01810448,4941021.651086288],[-8239331.002014435,4941018.439751712],[-8239308.921891005,4941011.75766107],[-8239300.469293313,4941009.496590462],[-8239287.6869025165,4941006.072937233],[-8239275.052516491,4941002.1380835315],[-8239258.357333093,4940996.062085458],[-8239242.014457308,4940989.09380805],[-8239232.988354876,4940987.782875498],[-8239230.92104043,4940983.563338348],[-8239229.580175487,4940979.059968032],[-8239229.002752587,4940974.396894991],[-8239229.204627729,4940969.702548125],[-8239244.403025703,4940848.777970234],[-8239248.640146485,4940783.227254025],[-8239246.744231659,4940772.366437443],[-8239243.908382943,4940761.712352623],[-8239240.154212192,4940751.346181607],[-8239235.510365571,4940741.346867954],[-8239230.012190497,4940731.790644234],[-8239223.70158445,4940722.750325623],[-8239182.358087038,4940674.261614145],[-8239180.691649818,4940649.540202345],[-8239179.585526596,4940636.54882987],[-8239179.45480811,4940635.227627606],[-8239179.536889753,4940633.902514442],[-8239179.829662006,4940632.607544869],[-8239180.325600752,4940631.375998644],[-8239181.011960634,4940630.239525578],[-8239181.871102677,4940629.227332175],[-8239182.88094749,4940628.365430994],[-8239184.0155427875,4940627.67597221],[-8239185.245730314,4940627.176674368],[-8239186.539895181,4940626.880368996],[-8239187.864778312,4940626.794670898],[-8239190.4568532,4940625.97984043],[-8239192.898363497,4940624.787436638],[-8239195.134666808,4940623.244146375],[-8239197.115713401,4940621.384509542],[-8239198.79716645,4940619.250146093],[-8239199.796263755,4940617.578539331],[-8239200.676265141,4940616.173619894],[-8239201.352936625,4940614.66024091],[-8239201.813202264,4940613.067647141],[-8239202.048167918,4940611.426614053],[-8239198.467878288,4940546.328341913],[-8239180.092297871,4940513.446290851],[-8239172.70374705,4940497.954803924],[-8239169.972003132,4940488.105753931],[-8239168.258906371,4940478.928382118],[-8239238.552143708,4940488.055831861],[-8239195.3955730805,4940467.329827611],[-8239151.618369593,4940459.853804436],[-8239146.289097216,4940417.509080897],[-8239153.717629261,4940416.5305688055],[-8239151.012945125,4940396.005591759],[-8239154.6403675405,4940395.794345576],[-8239154.115184814,4940389.21254235],[-8239147.687535178,4940389.724589162],[-8239147.760923538,4940387.021888642],[-8239151.778139111,4940386.582538452],[-8239152.1416425165,4940377.96433553],[-8239148.635484236,4940378.257330937],[-8239148.416669239,4940377.015192299],[-8239151.848835121,4940375.992680654],[-8239151.956917732,4940367.737966998],[-8239148.524521465,4940367.884613951],[-8239148.816683012,4940365.840376173],[-8239151.956578626,4940365.547548037],[-8239152.978717256,4940356.344405633],[-8239157.579726005,4940358.2416806305],[-8239161.011761171,4940346.044423828],[-8239163.248894552,4940342.369101979],[-8239164.589638686,4940340.784456752],[-8239157.410105831,4940334.710782795],[-8239154.782465364,4940334.022195187],[-8239148.788693367,4940338.201130025],[-8239145.521068649,4940348.190975649],[-8239143.2522385465,4940360.905791143],[-8239142.981684977,4940374.71095576],[-8239143.529754286,4940392.329181761],[-8239119.917387856,4940395.056687071],[-8239114.282989472,4940371.173475295],[-8239112.556168468,4940362.635901699],[-8239111.829708723,4940357.459636362],[-8239110.14598732,4940337.479948801],[-8239109.235258847,4940324.947369788],[-8239108.053116515,4940312.324086026],[-8239106.59847237,4940299.70071237],[-8239105.144714805,4940293.6156520005],[-8239102.055444424,4940289.3486153595],[-8239094.336847199,4940289.439279423],[-8239091.793876242,4940292.164826465],[-8239090.795967256,4940295.797758264],[-8239092.25028929,4940305.242712111],[-8239098.203411173,4940337.481739163],[-8239099.931474483,4940355.099724102],[-8239101.887758496,4940375.079482232],[-8239101.890205351,4940391.426728558],[-8239101.436531516,4940396.604488042],[-8239089.70066104,4940397.858850644],[-8239079.321742019,4940398.968107966],[-8239078.868065459,4940395.062820821],[-8239076.323473332,4940386.981111917],[-8239074.597301204,4940381.986649291],[-8239071.05419866,4940373.9947069185],[-8239066.784212137,4940366.275456255],[-8239059.9714808,4940354.379936311],[-8239053.930806442,4940343.846232208],[-8239050.297482567,4940339.214301893],[-8239046.482818847,4940338.216398536],[-8239038.49085064,4940342.123024337],[-8239035.494686499,4940344.392793168],[-8239035.586211186,4940349.38875136],[-8239037.040552559,4940359.286938222],[-8239039.903596025,4940374.997580333],[-8239043.903097478,4940397.247822625],[-8239044.2678283155,4940405.875147328],[-8239021.745340281,4940413.143816484],[-8239018.4752278095,4940406.6059746],[-8239013.614500065,4940393.801175449],[-8239006.528247114,4940375.729457531],[-8239004.52924409,4940369.373104859],[-8238997.3529341165,4940356.477907724],[-8238995.717870668,4940353.298427772],[-8238992.356335509,4940343.309596184],[-8238987.995563194,4940335.045091135],[-8238982.636463158,4940329.414974304],[-8238973.918128479,4940327.872435485],[-8238970.467446515,4940329.871036054],[-8238962.203355165,4940338.046450762],[-8238967.563991442,4940355.573156802],[-8238975.559562844,4940377.731856166],[-8238982.445958547,4940399.749854523],[-8238985.869449963,4940410.102278763],[-8238987.625187967,4940419.577751256],[-8238987.801930708,4940422.034172766],[-8238977.217264381,4940425.474657676],[-8238961.876067613,4940430.461094651],[-8238961.262114482,4940429.145241722],[-8238959.682018264,4940422.740025531],[-8238958.804410916,4940419.845126603],[-8238953.362662581,4940405.895683363],[-8238950.466616131,4940400.017711779],[-8238947.657547535,4940392.3849849515],[-8238945.11409344,4940392.73573034],[-8238942.305527516,4940389.929261504],[-8238937.129042457,4940389.403141825],[-8238935.1983199045,4940391.50943108],[-8238934.760071546,4940394.491535947],[-8238937.130929762,4940403.616515162],[-8238940.904904316,4940415.635203748],[-8238942.836926026,4940423.794706427],[-8238944.767039724,4940427.918536588],[-8238948.014862836,4940435.024177423],[-8238922.816946064,4940441.59762351],[-8238919.094749219,4940431.924775671],[-8238915.00094695,4940421.044283528],[-8238910.813342745,4940408.767483433],[-8238909.325838558,4940405.79084502],[-8238906.999623831,4940405.140519554],[-8238904.024286517,4940405.512359525],[-8238901.327015839,4940407.186765447],[-8238900.024601516,4940409.97805064],[-8238900.58396962,4940417.139345931],[-8238906.725878015,4940446.994816436],[-8238883.568379891,4940453.787827923],[-8238881.707859478,4940449.322870097],[-8238878.4508531485,4940440.952645221],[-8238875.101329387,4940431.838277278],[-8238871.7058328735,4940422.258911267],[-8238867.2387825465,4940407.378194342],[-8238859.144492026,4940383.103983464],[-8238850.31037716,4940357.267091611],[-8238846.273133933,4940351.699134249],[-8238844.523908025,4940352.689204028],[-8238842.94169014,4940356.389982449],[-8238856.109052423,4940388.403073449],[-8238853.989798779,4940389.872539114],[-8238856.254336332,4940394.31270032],[-8238857.836356036,4940393.534456469],[-8238860.947471044,4940401.035071882],[-8238858.54796412,4940403.680909197],[-8238858.4648998445,4940405.2841602685],[-8238859.728338903,4940406.60772763],[-8238862.1880663615,4940406.424841682],[-8238865.543695516,4940415.1501652915],[-8238863.490375837,4940416.8017664645],[-8238865.869531261,4940420.870321162],[-8238867.235969571,4940420.0972955525],[-8238867.784505299,4940422.363056514],[-8238866.023536415,4940423.39217357],[-8238867.576806821,4940426.433775673],[-8238869.059142699,4940426.581103071],[-8238870.112398661,4940429.327373472],[-8238868.545627298,4940430.783301714],[-8238871.006389361,4940433.124615092],[-8238873.449147552,4940439.778336488],[-8238873.04952958,4940441.081631295],[-8238875.596657894,4940446.993485779],[-8238877.227435262,4940446.89212688],[-8238877.281894324,4940449.108727535],[-8238878.951732076,4940454.229560322],[-8238874.30104578,4940456.207231396],[-8238875.558294329,4940462.37677088],[-8238881.158293734,4940478.141237111],[-8238890.1832310865,4940475.169898492],[-8238899.097837002,4940505.213412614],[-8238878.828191364,4940508.9846443795],[-8238886.968908365,4940534.673701397],[-8238873.071268795,4940535.0816718545],[-8238709.297116774,4940591.133068776],[-8238601.932970304,4940403.14428324],[-8238571.755809528,4940420.28475168],[-8238592.5546893915,4940456.902718147],[-8238487.385259196,4940516.636903598],[-8238505.655994069,4940548.804779992],[-8238610.75854619,4940489.10706127],[-8238676.557590332,4940604.690384831],[-8238660.003930948,4940610.947512746],[-8238656.480854277,4940612.559657619],[-8238573.114833632,4940679.942458881],[-8238527.970880329,4940716.21665377],[-8238499.2546887295,4940739.144817113],[-8238493.417614631,4940750.68515351],[-8238488.901329651,4940760.188008284],[-8238480.237184279,4940778.010026214],[-8238479.335835412,4940779.010366408],[-8238446.097371535,4940805.362732045],[-8238420.960694452,4940825.72576759],[-8238397.623886327,4940844.293625827],[-8238394.328153686,4940840.39724353],[-8238372.263845223,4940814.310478759],[-8238371.328688187,4940814.9638499785],[-8238365.255518065,4940805.153687002],[-8238361.474708123,4940800.8619097965],[-8238350.725585283,4940788.660421226],[-8238336.808175546,4940772.862417782],[-8238320.227788954,4940754.041573986],[-8238308.488308915,4940740.71571726],[-8238274.081580909,4940700.352244511],[-8238261.001261292,4940712.101299124],[-8238258.523444558,4940709.3405401865],[-8238264.494017283,4940704.544257406],[-8238264.048686885,4940703.274553096],[-8238264.811296232,4940702.511846442],[-8238264.62023451,4940700.607137518],[-8238265.635604862,4940699.781217418],[-8238265.128416958,4940698.004296371],[-8238266.7155865375,4940695.780767473],[-8238265.889520102,4940693.62196311],[-8238267.22379687,4940692.478443196],[-8238254.760767051,4940677.9426650405],[-8238253.938937154,4940678.017798663],[-8238232.771303303,4940696.81156163],[-8238246.481668012,4940712.253648359],[-8238247.451190766,4940711.3934499845],[-8238251.381917472,4940715.821591172],[-8238255.167310966,4940712.45995025],[-8238257.562175226,4940715.157371564],[-8238252.777070408,4940719.404333172],[-8238261.080638376,4940729.195004435],[-8238297.709161301,4940772.383506348],[-8238299.038849637,4940773.9566753125],[-8238300.379125339,4940775.542463613],[-8238343.665755533,4940826.7564794095],[-8238345.284053699,4940828.671082169],[-8238345.947778887,4940829.466367279],[-8238376.573062078,4940861.521159882],[-8238358.391648257,4940876.922104846],[-8238348.324234471,4940885.498616074],[-8238325.939213226,4940904.6828481285],[-8238312.51608806,4940889.166910759],[-8238298.135094706,4940901.593507053],[-8238272.755087626,4940922.714348005],[-8238237.908284442,4940952.823251463],[-8238227.899108677,4940962.244693084],[-8238222.472362779,4940966.785678073],[-8238219.849861389,4940968.980259643],[-8238190.052705185,4940993.916042738],[-8238181.174973907,4941001.050642533],[-8238152.855364074,4941024.4069112595],[-8238125.501823441,4941048.290930046],[-8238093.904516748,4941075.298853657],[-8237961.460307159,4940918.469645232],[-8237934.973644753,4940940.078608205],[-8238046.505191925,4941071.815677239],[-8238037.792270418,4941078.089462428],[-8238041.2776851775,4941081.923097558],[-8238049.990534359,4941075.649306797],[-8238069.036959216,4941098.1006768225],[-8238048.018338338,4941115.426093777],[-8238046.616736249,4941116.614260929],[-8238041.134512339,4941121.196021157],[-8238012.241463954,4941145.692862031],[-8237908.005755439,4941023.852957679],[-8237909.299297129,4941023.744926834],[-8237919.270628849,4941014.797860392],[-8237919.325933062,4941013.775713025],[-8237912.904186156,4941006.620655202],[-8237911.777903196,4941006.982851787],[-8237902.13068573,4941016.09187566],[-8237902.076746317,4941016.900629837],[-8237885.6211768035,4940996.258508963],[-8237884.0878736405,4940996.196720027],[-8237868.521442662,4941008.828816402],[-8237861.63461647,4941014.4174099425],[-8237861.696465741,4941015.398679367],[-8237907.6881805975,4941068.5564242285],[-8237911.237066641,4941072.65821582],[-8237905.350234583,4941076.996157675],[-8237941.225675663,4941119.458123005],[-8237928.370897959,4941130.331248034],[-8237882.56658889,4941105.981600145],[-8237878.626255027,4941109.182699232],[-8237871.950868366,4941114.605865602],[-8237871.795554581,4941114.741581707],[-8237871.099990519,4941113.914335415],[-8237804.21889632,4941035.2098397715],[-8237736.409836271,4941090.973373895],[-8237708.264601638,4941113.728421086],[-8237798.813307566,4941226.149414507],[-8237798.908147442,4941226.26280205],[-8237798.759349017,4941226.372027495],[-8237751.719815043,4941260.97889271],[-8237773.232738507,4941291.823970036],[-8237773.278922737,4941292.918657985],[-8237772.46603872,4941293.568130223],[-8237773.143161164,4941294.920883992],[-8237771.492751292,4941297.111533755],[-8237778.909164554,4941307.5193413],[-8237779.479556722,4941308.318920381],[-8237778.53762335,4941309.039463864],[-8237768.255084716,4941316.907750992],[-8237774.4385324735,4941325.798051838],[-8237740.568280292,4941349.997235277],[-8237736.910622457,4941352.610546805],[-8237728.229763169,4941358.027397794],[-8237703.820418875,4941373.259404733],[-8237686.877481035,4941384.3775093565],[-8237668.34571094,4941397.084080992],[-8237657.757445615,4941405.555622208],[-8237641.92353588,4941418.222809235],[-8237639.226969029,4941420.380625573],[-8237637.499973486,4941424.008461777],[-8237636.592374556,4941428.0301184235],[-8237636.202425498,4941431.531553551],[-8237636.592371738,4941432.829000121],[-8237629.291003577,4941437.596337294],[-8237598.846258374,4941457.47443849],[-8237594.617685678,4941451.217141578],[-8237592.359990306,4941447.875225259],[-8237590.93324619,4941447.746093018],[-8237585.615308371,4941451.896855362],[-8237588.007441917,4941455.82631952],[-8237589.247069392,4941457.862893135],[-8237582.242996334,4941462.792270778],[-8237577.897154026,4941466.099917494],[-8237573.551284236,4941469.407734942],[-8237567.325739562,4941474.985140713],[-8237559.881577392,4941480.00925004],[-8237552.652086779,4941484.8885732135],[-8237545.954821278,4941490.011506812],[-8237555.8939534575,4941500.347383092],[-8237563.984681811,4941508.761101587],[-8237568.235522539,4941513.616666655],[-8237572.94633053,4941518.907033324],[-8237586.115384329,4941533.410753788],[-8237615.490048737,4941563.36265813],[-8237627.783280741,4941575.898290748],[-8237637.681538216,4941586.143168399],[-8237649.53620289,4941598.413956662],[-8237657.399276715,4941606.553670626],[-8237665.263736053,4941614.693458563],[-8237695.138865481,4941646.406053194],[-8237778.587098531,4941734.986076845],[-8237800.31953413,4941758.066985747]]]]},"properties":{"shape_area":19245011.7093,"ntaname":"Financial District-Battery Park City","cdtaname":"MN01 Financial District-Tribeca (CD 1 Equivalent)","shape_leng":45306.3321199,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0101","borocode":1,"countyfips":"061","ntaabbrev":"FiDi","cdta2020":"MN01"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8237565.777214913,4942889.7408028655],[-8237637.443797543,4942958.044358142],[-8237701.535587562,4943018.490199773],[-8237783.998736478,4943096.207410126],[-8237798.2279014345,4943109.549946408],[-8237852.051499515,4943160.019400287],[-8237931.751332763,4943234.031243485],[-8238005.170203167,4943303.334585252],[-8238033.257560163,4943329.562015653],[-8238079.724477665,4943373.175300909],[-8238158.302453943,4943446.214759518],[-8238224.203588022,4943503.346116274],[-8238233.40961125,4943515.064408371],[-8238300.539484476,4943577.798303314],[-8238343.455213876,4943621.189137034],[-8238491.579573225,4943759.927711398],[-8238523.085093321,4943789.493494369],[-8238541.596661361,4943808.090116276],[-8238566.511147144,4943830.176468443],[-8238596.427943042,4943858.203551994],[-8238604.542649245,4943865.80668044],[-8238679.549587092,4943936.075081645],[-8238725.6268651225,4943978.335717166],[-8238761.456405217,4944015.513214433],[-8238845.947456763,4944095.310914751],[-8238858.236110565,4944095.947271896],[-8238860.080931399,4944079.920417106],[-8238860.457803936,4944076.645857539],[-8238873.088677546,4944001.460412006],[-8238880.470709569,4943953.477023264],[-8238883.4490525685,4943934.117228564],[-8238892.960728626,4943872.290948531],[-8238906.271662024,4943873.980994626],[-8238915.738964644,4943875.047017612],[-8238932.648021379,4943876.969010816],[-8238974.180730257,4943881.689301665],[-8238988.687678677,4943790.559364096],[-8239003.846065482,4943695.335861199],[-8239019.420636567,4943597.496999405],[-8239032.889533682,4943508.562945203],[-8239038.126866069,4943468.095017279],[-8239101.023611427,4943475.618349301],[-8239101.98140466,4943469.081721143],[-8239118.192126944,4943471.69383529],[-8239117.931042291,4943474.047013391],[-8239151.93243957,4943478.149342412],[-8239151.953520992,4943479.425217985],[-8239157.998507619,4943480.2400025455],[-8239160.186275678,4943470.053298656],[-8239118.631042144,4943463.505843217],[-8239117.725053781,4943466.155388511],[-8239102.45562207,4943463.787146972],[-8239107.357347913,4943431.631558575],[-8239381.402751642,4943473.988141825],[-8239389.446837516,4943424.676042635],[-8239078.039872373,4943375.166584057],[-8239089.4505746,4943296.896029008],[-8239147.268196332,4943305.981388493],[-8239256.139899014,4943323.32445742],[-8239457.464570968,4943354.532740647],[-8239465.56308027,4943303.2631703485],[-8239391.48293618,4943292.557149611],[-8239391.56921346,4943287.763832502],[-8239375.968763257,4943285.3263878105],[-8239375.968484365,4943283.75769995],[-8239230.510172354,4943260.861875574],[-8239230.248935952,4943262.082030337],[-8239200.791533766,4943257.9907378815],[-8239200.181786945,4943260.692494274],[-8239135.532205994,4943251.0114501985],[-8239140.128721692,4943219.148641827],[-8239079.33395383,4943209.366938558],[-8239084.740523074,4943175.029679176],[-8239084.489692316,4943173.462639322],[-8239090.777637032,4943133.888158647],[-8239092.88750529,4943121.084244981],[-8239056.263112301,4943115.278624403],[-8239039.9001388,4943112.072654976],[-8239027.254669314,4943110.289100086],[-8239010.61990665,4943107.5495111],[-8239014.379947979,4943083.189876133],[-8239054.747178189,4942852.850265178],[-8239059.490263693,4942825.862092408],[-8239080.708701133,4942721.194340526],[-8239092.251283868,4942655.9316142015],[-8239100.867511137,4942607.214653602],[-8239113.75262933,4942547.095146103],[-8239117.896761841,4942526.077371007],[-8239138.33851615,4942433.303604083],[-8239170.08412175,4942323.2714528125],[-8239098.896088975,4942283.608404662],[-8239039.035817401,4942248.447705436],[-8238954.468311393,4942199.276500339],[-8238942.433338367,4942191.932309318],[-8238769.058375801,4942090.133274991],[-8238626.248825451,4942005.175421621],[-8238599.761320107,4941988.831806863],[-8238571.570373433,4941983.90261964],[-8238529.29872751,4941996.891369566],[-8238536.085929993,4941997.71879114],[-8238542.765855358,4941999.178021077],[-8238549.279853727,4942001.256211265],[-8238555.570674165,4942003.935138364],[-8238556.935760781,4942007.830796672],[-8238557.626862273,4942011.900441345],[-8238557.62432609,4942016.028350847],[-8238556.928223493,4942020.097146599],[-8238555.558347566,4942023.9911306165],[-8238553.553650574,4942027.599575532],[-8238550.971136312,4942030.819873139],[-8238547.884239325,4942033.560452285],[-8238544.922591036,4942035.457603321],[-8238541.719637705,4942036.910750566],[-8238535.325900826,4942038.340251802],[-8238528.821424482,4942039.1245619245],[-8238522.271147978,4942039.255850402],[-8238515.740467899,4942038.732806447],[-8238510.113105307,4942035.198320572],[-8238504.084442359,4942032.402789402],[-8238497.751011105,4942030.390974725],[-8238491.214218979,4942029.1951092305],[-8238484.5787407765,4942028.8343218975],[-8238477.950824499,4942029.314389585],[-8238423.5587773165,4942046.136580999],[-8238326.859523881,4942076.183447021],[-8238317.936197715,4942078.975737747],[-8238281.342230898,4942090.727297248],[-8238264.519922626,4942102.6205216795],[-8238246.836830107,4942107.542040559],[-8238229.597914704,4942114.234317702],[-8238213.067631632,4942122.621833836],[-8238197.487471257,4942132.582017497],[-8238183.073190417,4942143.946763919],[-8238166.246427756,4942161.717758972],[-8238120.006027721,4942210.552596309],[-8238125.654336498,4942197.647117635],[-8238133.404955119,4942179.816910615],[-8238142.059364997,4942157.725008234],[-8238143.308956632,4942136.959525567],[-8238139.195998683,4942117.554296995],[-8238133.982356117,4942106.105073077],[-8238113.096579365,4942081.47172398],[-8238088.382909413,4942056.005735866],[-8238084.397267781,4942051.756762751],[-8238080.567462152,4942048.287309539],[-8238057.647997198,4942027.524646664],[-8238035.381069921,4942003.479945837],[-8238027.629691407,4941995.004442811],[-8238006.774151418,4941973.051297426],[-8238001.393760265,4941967.330242689],[-8237944.41123671,4941906.32865029],[-8237933.790581336,4941895.244578574],[-8237828.105530949,4941786.041686358],[-8237809.489778417,4941767.039374147],[-8237800.31953413,4941758.066985747],[-8237785.09544753,4941784.290379603],[-8237779.7721830765,4941793.254909574],[-8237768.82954691,4941810.902006918],[-8237754.000474462,4941836.951142871],[-8237743.211507713,4941855.062020415],[-8237726.130294009,4941883.734941383],[-8237716.197951464,4941900.407881909],[-8237699.076092533,4941929.149314699],[-8237711.364513891,4941943.444037972],[-8237721.811470289,4941959.557897757],[-8237730.039901782,4941977.109129187],[-8237735.78518377,4941995.622246055],[-8237738.92338641,4942014.567422285],[-8237739.47424305,4942033.411295414],[-8237741.578060778,4942223.928689593],[-8237741.787482408,4942228.418957971],[-8237742.105775624,4942230.973743172],[-8237746.039747545,4942262.550139808],[-8237782.249457638,4942296.714119721],[-8237816.684595924,4942323.967097307],[-8237750.120922893,4942356.522798241],[-8237725.421243908,4942444.997900859],[-8237692.92785773,4942423.739879753],[-8237701.022800357,4942541.512752517],[-8237730.211139916,4942559.201816089],[-8237706.704949575,4942600.156502246],[-8237657.704066467,4942692.15529433],[-8237634.635085794,4942742.510419996],[-8237580.912845467,4942857.488537143],[-8237565.777214913,4942889.7408028655]]]},"properties":{"shape_area":13577181.4049,"ntaname":"Tribeca-Civic Center","cdtaname":"MN01 Financial District-Tribeca (CD 1 Equivalent)","shape_leng":21222.4437187,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0102","borocode":1,"countyfips":"061","ntaabbrev":"TriBeCa","cdta2020":"MN01"}}, +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8238858.863865515,4938054.262987884],[-8238703.520440219,4938178.586968017],[-8238599.746861389,4938261.6364725055],[-8238589.975061184,4938269.384691598],[-8238580.203318934,4938277.132735551],[-8238551.147197455,4938300.411315497],[-8238572.333527704,4938333.385012227],[-8238599.447390704,4938311.795270028],[-8238610.308343424,4938303.131337483],[-8238620.3607621435,4938296.299804976],[-8238768.455095196,4938178.579685174],[-8238997.741053573,4937996.3130074525],[-8238970.649559089,4937965.979365056],[-8238858.863865515,4938054.262987884]]],[[[-8238199.331630654,4938509.716849829],[-8238468.322853804,4938597.5055336235],[-8238514.277334727,4938477.408184631],[-8238269.03212048,4938389.541025435],[-8238199.331630654,4938509.716849829]]],[[[-8238067.602408826,4938703.290010755],[-8238350.824097302,4938811.513484986],[-8238397.5529187815,4938688.261461171],[-8238153.57621044,4938598.811836986],[-8238067.602408826,4938703.290010755]]],[[[-8237973.566957503,4938922.207384819],[-8238236.998136463,4939019.46512322],[-8238283.408395941,4938894.394845081],[-8238021.830845168,4938803.264330013],[-8237973.566957503,4938922.207384819]]],[[[-8242526.752543546,4938887.269245432],[-8242607.854257239,4938943.692051086],[-8242747.4610085795,4939014.275339864],[-8242749.25828182,4939015.299128171],[-8242751.992193208,4939016.765980912],[-8242757.115165717,4939019.236591442],[-8242762.367839595,4939021.417920481],[-8242767.557430464,4939022.682713068],[-8242772.830706073,4939023.53366787],[-8242774.465124988,4939023.71126892],[-8242775.283723159,4939023.785172345],[-8242776.923402851,4939023.903136165],[-8242779.387170523,4939024.005083339],[-8242782.125421625,4939020.3980601635],[-8242873.990358096,4939013.28238164],[-8242875.094063086,4939013.211448776],[-8242877.834729914,4939012.85200233],[-8242878.919467123,4939012.636012357],[-8242880.529320206,4939012.235497203],[-8242885.186657262,4939010.496726242],[-8242889.482204024,4939007.994251417],[-8242893.29183694,4939004.800384676],[-8242895.840463159,4939001.38598362],[-8242897.84596647,4938997.626785151],[-8242899.262625886,4938993.608492182],[-8242900.05814503,4938989.422714324],[-8242900.214388412,4938985.16487927],[-8242899.727794841,4938980.932057266],[-8242898.609458481,4938976.820748021],[-8242898.226608163,4938975.804975609],[-8242897.80505581,4938974.804732843],[-8242897.3453901615,4938973.82136691],[-8242896.585717385,4938972.3813301455],[-8242896.033589612,4938971.446671811],[-8242894.729998689,4938969.120066749],[-8242887.22769316,4938955.731373636],[-8242886.636203986,4938928.618324935],[-8242896.6312845815,4938921.949924502],[-8242862.318314306,4938868.465515712],[-8242922.342403616,4938831.142646325],[-8242939.536738034,4938857.264480905],[-8242955.556398882,4938847.43844522],[-8242927.380134813,4938802.987759132],[-8242852.3879456,4938852.98674199],[-8242793.671410473,4938761.462829963],[-8242627.649709705,4938654.447504166],[-8242583.25839646,4938642.823900802],[-8242537.5470768735,4938652.509355041],[-8242500.388710137,4938679.440154783],[-8242486.539027527,4938741.516293505],[-8242481.101166605,4938806.734519405],[-8242394.9716167655,4938859.421924363],[-8242388.903019086,4938849.318874861],[-8242379.377185618,4938854.376508435],[-8242396.137808152,4938882.520584073],[-8242405.0488078445,4938877.514914986],[-8242399.59479732,4938867.1628288515],[-8242485.38002992,4938814.1007863],[-8242526.752543546,4938887.269245432]]],[[[-8238133.172267546,4939229.653462522],[-8238176.0013669245,4939117.453768177],[-8237720.137492702,4938949.304105656],[-8237789.886417199,4939073.795810778],[-8237861.549629896,4939129.385939033],[-8238133.172267546,4939229.653462522]]],[[[-8239506.648660397,4939348.7849523695],[-8239574.712216297,4939343.300870377],[-8239641.660663821,4939337.906072508],[-8239667.400815844,4939335.831805198],[-8239734.802988589,4939330.39981994],[-8239740.774321463,4939330.39853675],[-8239742.450597183,4939330.40780485],[-8239758.7676021755,4939330.717166384],[-8239775.072200138,4939331.424605043],[-8239785.344125581,4939332.227379333],[-8239795.645960437,4939332.056780024],[-8239805.885672453,4939330.9143312285],[-8239815.971784388,4939328.810239459],[-8239818.074747492,4939328.267510119],[-8239823.938771034,4939326.14836992],[-8239829.450380782,4939323.232951382],[-8239834.50208002,4939319.578116794],[-8239838.9953423245,4939315.255149848],[-8239842.842533189,4939310.348365278],[-8239845.804018698,4939305.286349451],[-8239846.118027355,4939304.645426921],[-8239852.705202429,4939284.8532418525],[-8239891.55955342,4939157.277867185],[-8239920.347918794,4939125.697914938],[-8239948.434592488,4939091.511012798],[-8239948.824823633,4939091.036123487],[-8239977.797391688,4939053.5097525725],[-8240003.730223491,4939021.273129337],[-8240033.056228836,4938983.637505571],[-8240035.286746369,4938980.775125094],[-8240080.151200999,4938924.678318801],[-8240119.304340714,4938877.6964367125],[-8240140.299079097,4938851.021506604],[-8240154.285207567,4938834.340711062],[-8240167.563435037,4938818.504276152],[-8240217.577958387,4938756.787023533],[-8240246.119548442,4938720.781042477],[-8240262.383721472,4938701.261008819],[-8240277.736939412,4938683.205694957],[-8240292.996266658,4938664.517005198],[-8240310.5632001655,4938642.731943044],[-8240310.860939176,4938642.362801606],[-8240327.342171942,4938621.189708079],[-8240335.706512986,4938610.444218656],[-8240349.952225056,4938593.698313872],[-8240351.749179373,4938591.585506035],[-8240364.843036967,4938576.193665074],[-8240376.759777654,4938562.16914635],[-8240397.368692265,4938536.192591383],[-8240420.849283961,4938504.191668389],[-8240423.583499949,4938500.812999957],[-8240442.478974364,4938477.764907439],[-8240446.227631599,4938473.192455069],[-8240468.38577677,4938445.506076429],[-8240489.839958469,4938420.726334805],[-8240512.566047737,4938391.076575457],[-8240543.955763985,4938352.449407304],[-8240556.093124062,4938337.60723224],[-8240561.144008476,4938331.098942429],[-8240571.435479801,4938318.017953044],[-8240578.505651779,4938308.688361583],[-8240584.022233192,4938300.502274185],[-8240591.840090664,4938282.955789563],[-8240600.1015567705,4938261.025113131],[-8240604.408969259,4938240.173882394],[-8240605.842074523,4938224.356423655],[-8240606.555347143,4938205.663369692],[-8240604.39182546,4938183.735879896],[-8240600.791067207,4938163.965800054],[-8240594.314464722,4938144.5561658],[-8240585.680204989,4938121.552332545],[-8240573.810895425,4938099.6280234195],[-8240562.494209308,4938084.794546193],[-8240550.62744322,4938069.18431204],[-8240541.805364466,4938057.579242271],[-8240523.825751372,4938037.4540983355],[-8240500.094589613,4938016.971265952],[-8240477.444424367,4938004.755983404],[-8240449.403230869,4937996.136760272],[-8240424.533478806,4937990.014210962],[-8240407.70150402,4937984.286263996],[-8240386.131798611,4937979.259625728],[-8240362.046471427,4937975.671689036],[-8240337.96166021,4937974.240503518],[-8240308.125905377,4937974.967642278],[-8240289.79379122,4937977.848383618],[-8240262.958038724,4937982.83015648],[-8240248.841618255,4937987.600161466],[-8240246.946382453,4937988.384941153],[-8240231.251960693,4937994.88413264],[-8240217.705931199,4938000.54934349],[-8240204.56438263,4938006.82102427],[-8240175.837913364,4938023.989253787],[-8240132.339296579,4937950.210820013],[-8240224.33291893,4937836.236309438],[-8240208.87277335,4937824.018478226],[-8240106.097540221,4937948.42016103],[-8240157.426283044,4938034.992878517],[-8240135.577606006,4938048.050461708],[-8240128.698421088,4938052.057858077],[-8240125.65992248,4938047.290223612],[-8240128.9863144625,4938045.306020377],[-8240127.770268877,4938043.514990295],[-8240124.443844827,4938045.17929832],[-8240122.459627227,4938041.78899133],[-8240121.500041891,4938042.173044847],[-8240113.4354126975,4938028.931651642],[-8240111.836308422,4938030.08367977],[-8240119.709100656,4938043.645018989],[-8240114.463494693,4938046.333458586],[-8240106.590691409,4938032.708147291],[-8240104.543738948,4938033.860287833],[-8240112.352551597,4938047.485618602],[-8240111.648813813,4938047.933570763],[-8240118.152006891,4938058.2015998345],[-8240073.435665642,4938084.250558672],[-8239986.959127327,4938134.625577454],[-8239936.910893039,4938163.779667269],[-8239899.677429547,4938100.715552594],[-8239900.8798423195,4938097.105454541],[-8239981.005281933,4938048.7157841185],[-8239974.745910521,4938038.610036697],[-8239801.7413751315,4938141.407881007],[-8239808.548559206,4938151.2865522],[-8239886.830153153,4938104.207930923],[-8239891.736858901,4938104.327135026],[-8239929.229911909,4938168.411963068],[-8239886.137423816,4938194.400739224],[-8239548.218703414,4938398.188220675],[-8239509.53224392,4938333.176580413],[-8239510.3866142705,4938325.9032732],[-8239498.405935166,4938319.487964831],[-8239492.416616199,4938321.628325763],[-8239328.131150039,4938338.771542974],[-8239328.491404537,4938344.161466314],[-8239328.62106899,4938346.102869363],[-8239328.988962793,4938351.606469544],[-8239495.414284799,4938335.746295514],[-8239503.116020881,4938340.023281975],[-8239540.301113701,4938402.962853041],[-8239532.687144304,4938407.554366843],[-8239481.23607106,4938438.581227931],[-8239434.327874136,4938467.103771679],[-8239428.696567446,4938470.5278093545],[-8239298.861790524,4938458.326258657],[-8239142.063769971,4938569.100865612],[-8239136.06925399,4938574.970859878],[-8239126.229753865,4938580.962147971],[-8239118.956948374,4938585.241739179],[-8239108.3144679,4938589.9711763],[-8239062.144486807,4938672.712462941],[-8239056.433486337,4938682.947001806],[-8239012.314656051,4938754.553696083],[-8239003.902066706,4938927.290036155],[-8238993.98400548,4938926.260031892],[-8238992.367770552,4938937.1801631255],[-8238990.143225363,4938937.180470857],[-8238989.132513964,4938939.809531993],[-8238984.481581805,4938940.821248097],[-8238983.673872205,4938949.112456157],[-8238975.989388274,4938948.506884359],[-8238977.405824297,4938954.977768177],[-8238976.597766815,4938961.8534356365],[-8238974.981454376,4938972.571451912],[-8238976.801486442,4938972.773354945],[-8238978.014518374,4938969.5376558425],[-8238984.080836356,4938967.919044658],[-8238987.316884639,4938970.951979426],[-8238987.519454969,4938973.9851859985],[-8239001.549066307,4938975.604792905],[-8238999.9188457215,4939009.077293183],[-8238999.85999275,4939010.2873265585],[-8238959.059025456,4939007.729494912],[-8238958.755565647,4939015.5476359],[-8238998.949382808,4939017.479275909],[-8238998.585289897,4939025.989645005],[-8239036.910592332,4939027.4698718265],[-8239044.669496925,4939054.964065955],[-8238980.096964057,4939073.080079954],[-8238981.957065126,4939079.642176],[-8239035.983831646,4939064.104027307],[-8239037.406392416,4939068.806651843],[-8239039.921786087,4939067.931304001],[-8239038.827448892,4939063.337954598],[-8239043.2020648895,4939062.134204497],[-8239043.201797782,4939060.274933728],[-8239045.898137744,4939059.317653906],[-8239047.740292106,4939065.845209488],[-8239050.590144743,4939064.4883266045],[-8239062.128494326,4939075.750654338],[-8239068.983937191,4939079.210749694],[-8239074.496568379,4939086.080685959],[-8239075.579226906,4939087.430410962],[-8239077.803969457,4939090.202276167],[-8239077.993295732,4939090.438041982],[-8239073.873003519,4939094.254297081],[-8239081.457517616,4939104.947184347],[-8239079.707973595,4939107.0862739375],[-8239110.240444597,4939146.163596867],[-8239117.890427164,4939140.15101816],[-8239118.9451615065,4939141.4652485205],[-8239119.10584324,4939141.66674261],[-8239120.064859909,4939142.861056497],[-8239124.672458813,4939148.294300549],[-8239125.702703451,4939150.152402441],[-8239126.533996385,4939151.970941656],[-8239126.689502307,4939152.451745136],[-8239126.781728397,4939152.969235887],[-8239126.808076603,4939153.517093129],[-8239126.770015541,4939154.086006352],[-8239126.501491009,4939155.2371416],[-8239126.279034253,4939155.7864088705],[-8239126.005266649,4939156.294718724],[-8239125.769524267,4939156.642469333],[-8239125.503593937,4939156.970550436],[-8239124.876661245,4939157.567239259],[-8239124.118222073,4939158.092748862],[-8239123.217497098,4939158.553794022],[-8239121.215717998,4939159.24421083],[-8239117.062132405,4939160.245818655],[-8239116.406322377,4939160.421098303],[-8239114.338687958,4939160.973118941],[-8239070.354103226,4939172.708870512],[-8239040.379091164,4939180.56515959],[-8239038.24561543,4939181.123838896],[-8239029.515588627,4939183.412726469],[-8239025.938775645,4939174.873867857],[-8239023.636118836,4939171.9491173215],[-8239021.5683393935,4939170.317632623],[-8239018.174002151,4939168.077857978],[-8239014.254270124,4939165.652563174],[-8239000.543523685,4939158.883865756],[-8238994.723593777,4939157.866594489],[-8238994.063838552,4939158.052377243],[-8238993.439558933,4939158.227644199],[-8238993.326296492,4939158.260662417],[-8238974.694183179,4939163.500877793],[-8238968.266087343,4939165.309911334],[-8238967.365424246,4939165.564229356],[-8238963.089624949,4939169.568482724],[-8238960.231072868,4939174.674786491],[-8238960.156094083,4939174.809208038],[-8238959.725571877,4939175.57837095],[-8238951.1565591,4939190.88830153],[-8238950.360848253,4939198.582386788],[-8238950.359550942,4939198.591550294],[-8238950.430734834,4939198.840420507],[-8238954.079197176,4939211.513402392],[-8238955.756480047,4939217.338364274],[-8238957.208550065,4939222.419144157],[-8238961.331445738,4939228.05536931],[-8238961.36565545,4939228.102749502],[-8238983.016340495,4939240.332117885],[-8238989.684384483,4939241.450697119],[-8238991.206766926,4939241.025051791],[-8238991.305673526,4939240.997425518],[-8239011.249537864,4939235.423617212],[-8239014.309237681,4939234.213716122],[-8239017.1284244675,4939232.517306303],[-8239019.630395458,4939230.380543022],[-8239021.747078623,4939227.861562408],[-8239022.790486372,4939226.225005025],[-8239023.669900711,4939224.494788593],[-8239036.36136095,4939201.899796247],[-8239034.1002930505,4939196.715025681],[-8239134.423099455,4939169.328213163],[-8239135.591905477,4939169.091258424],[-8239136.783707071,4939169.048084456],[-8239137.9665946895,4939169.199847205],[-8239139.108897449,4939169.542483356],[-8239140.180031086,4939170.066819104],[-8239141.151316817,4939170.758815817],[-8239141.996749216,4939171.599945838],[-8239142.693692435,4939172.567688598],[-8239225.418878791,4939272.66650982],[-8239329.490440591,4939300.835890542],[-8239316.534758794,4939318.793295932],[-8239310.349894077,4939314.145113852],[-8239305.782588359,4939319.191491424],[-8239317.443041693,4939327.779975478],[-8239326.447115806,4939334.222361586],[-8239330.521247992,4939327.420517263],[-8239326.179136193,4939324.516456738],[-8239340.999551372,4939303.950949945],[-8239346.526581257,4939305.4469065955],[-8239340.468949965,4939328.824986696],[-8239333.188529549,4939327.612830461],[-8239331.572163084,4939336.106744934],[-8239355.436351971,4939342.169523828],[-8239357.659288326,4939333.473162511],[-8239351.591995364,4939331.451996902],[-8239357.038655113,4939308.292144681],[-8239506.648660397,4939348.7849523695]]],[[[-8237813.943114008,4939267.263792293],[-8237783.71104509,4939341.866418045],[-8238026.758883471,4939452.370488162],[-8238059.279109191,4939388.687779288],[-8238088.482736389,4939329.016349705],[-8237836.417569382,4939211.80362184],[-8237813.943114008,4939267.263792293]]],[[[-8242089.577691489,4940452.651398186],[-8242127.168000856,4940416.52064582],[-8242129.886611553,4940413.907570996],[-8242184.243615848,4940361.6608451],[-8242217.395230224,4940394.482487979],[-8242425.166369946,4940176.02171581],[-8242396.101475579,4940146.94994069],[-8242502.139063059,4940039.225648369],[-8242499.951898899,4940036.906159376],[-8242504.054043933,4940032.835911879],[-8242279.946382101,4939803.582082481],[-8242085.35655007,4939986.271320075],[-8242289.650074451,4940197.7423224775],[-8242233.425899383,4940254.621708298],[-8242123.546049085,4940147.567671664],[-8242087.608833209,4940109.48046513],[-8242021.669330788,4940046.746997552],[-8241983.827589993,4940083.952726787],[-8241898.012050603,4940083.76270214],[-8241883.353182095,4940181.893257254],[-8241840.315525067,4940226.43616049],[-8242026.3897766555,4940406.878706128],[-8242034.1652388135,4940400.364780123],[-8242089.577691489,4940452.651398186]]],[[[-8239199.796263755,4940617.578539331],[-8239198.79716645,4940619.250146093],[-8239197.115713401,4940621.384509542],[-8239195.134666808,4940623.244146375],[-8239192.898363497,4940624.787436638],[-8239190.4568532,4940625.97984043],[-8239187.864778312,4940626.794670898],[-8239186.539895181,4940626.880368996],[-8239185.245730314,4940627.176674368],[-8239184.0155427875,4940627.67597221],[-8239182.88094749,4940628.365430994],[-8239181.871102677,4940629.227332175],[-8239181.011960634,4940630.239525578],[-8239180.325600752,4940631.375998644],[-8239179.829662006,4940632.607544869],[-8239179.536889753,4940633.902514442],[-8239179.45480811,4940635.227627606],[-8239179.585526596,4940636.54882987],[-8239180.691649818,4940649.540202345],[-8239182.358087038,4940674.261614145],[-8239223.70158445,4940722.750325623],[-8239230.012190497,4940731.790644234],[-8239235.510365571,4940741.346867954],[-8239240.154212192,4940751.346181607],[-8239243.908382943,4940761.712352623],[-8239246.744231659,4940772.366437443],[-8239248.640146485,4940783.227254025],[-8239244.403025703,4940848.777970234],[-8239229.204627729,4940969.702548125],[-8239229.002752587,4940974.396894991],[-8239229.580175487,4940979.059968032],[-8239230.92104043,4940983.563338348],[-8239232.988354876,4940987.782875498],[-8239242.014457308,4940989.09380805],[-8239258.357333093,4940996.062085458],[-8239275.052516491,4941002.1380835315],[-8239287.6869025165,4941006.072937233],[-8239300.469293313,4941009.496590462],[-8239308.921891005,4941011.75766107],[-8239331.002014435,4941018.439751712],[-8239343.01810448,4941021.651086288],[-8239356.3701531105,4941024.50917135],[-8239385.310923258,4941029.327401464],[-8239408.145386527,4941029.3427260155],[-8239446.397616827,4941025.159748862],[-8239456.420148957,4941024.48962577],[-8239465.197289216,4941030.116917266],[-8239481.361617679,4941024.446088184],[-8239493.295887037,4941024.404155811],[-8239493.389744948,4941016.282030118],[-8239494.373557845,4941000.119103769],[-8239501.334209788,4941000.087024985],[-8239509.184557032,4940999.94074736],[-8239517.607625399,4941000.334917705],[-8239549.359423708,4940948.794206209],[-8239598.241756287,4940934.363506845],[-8239604.500571108,4940910.903572796],[-8239605.326481036,4940911.08648466],[-8239606.428992682,4940910.720008442],[-8239607.62199937,4940908.882297414],[-8239607.529502283,4940907.229124525],[-8239606.335750446,4940905.852910473],[-8239607.98990326,4940897.616031026],[-8239608.864828618,4940892.726322215],[-8239609.811384862,4940883.237998332],[-8239610.1017965125,4940873.896282851],[-8239609.62863525,4940865.308015633],[-8239610.793022306,4940865.211573814],[-8239609.48106885,4940849.896022042],[-8239620.608987643,4940848.670750214],[-8239621.177817549,4940853.71832503],[-8239633.922905103,4940852.643047508],[-8239633.226474366,4940846.291196629],[-8239632.532278924,4940839.961134326],[-8239620.165618249,4940840.937258213],[-8239620.608565214,4940846.588535352],[-8239609.201976402,4940847.685162831],[-8239608.271086486,4940841.6390604675],[-8239606.882300402,4940833.53848348],[-8239604.438167511,4940823.030171194],[-8239603.413262546,4940823.101588732],[-8239601.1498589,4940814.0527391005],[-8239598.812811347,4940806.1710070595],[-8239594.90520384,4940795.735427314],[-8239589.720904637,4940783.840725962],[-8239583.734569109,4940772.529680283],[-8239579.098987554,4940763.991142247],[-8239570.191681836,4940749.324444998],[-8239567.41810955,4940744.726626808],[-8239562.945491639,4940738.697749716],[-8239562.1030158475,4940737.56202327],[-8239562.782669009,4940737.137761572],[-8239550.9003472,4940718.091949848],[-8239550.007371995,4940718.968166988],[-8239535.191749791,4940696.031928208],[-8239523.236316686,4940676.627920338],[-8239514.556244737,4940664.442883998],[-8239482.71787275,4940620.263647263],[-8239480.777080921,4940617.449238258],[-8239467.073773401,4940599.955543384],[-8239450.647124765,4940578.749248524],[-8239438.893540183,4940563.141463936],[-8239436.353701951,4940560.915961909],[-8239413.449909231,4940536.071486287],[-8239400.44498493,4940519.390471612],[-8239387.561394267,4940506.096776829],[-8239327.953844904,4940445.668776169],[-8239314.463908045,4940467.552310306],[-8239293.002211136,4940453.252888873],[-8239291.041134463,4940454.582196034],[-8239275.78264794,4940444.457111069],[-8239279.010177751,4940439.265596228],[-8239281.700563185,4940435.594301653],[-8239292.078325484,4940419.008032601],[-8239299.41949056,4940407.23265667],[-8239305.653594096,4940399.001988361],[-8239305.084518948,4940398.875620895],[-8239303.627922221,4940399.952055096],[-8239302.932383398,4940399.25530769],[-8239303.564275754,4940397.736498696],[-8239305.7163224565,4940396.407115185],[-8239306.412822813,4940394.697361277],[-8239309.323055866,4940391.152429247],[-8239308.501052864,4940390.455778005],[-8239311.348053533,4940386.277261698],[-8239313.057855615,4940386.403433257],[-8239314.63919663,4940383.745181166],[-8239316.096006999,4940383.934590958],[-8239323.056297181,4940371.272855078],[-8239319.701126807,4940368.931407941],[-8239311.980879983,4940381.782928224],[-8239304.355002431,4940392.482244203],[-8239296.44469305,4940403.624105464],[-8239291.223647134,4940411.728282869],[-8239286.666143768,4940419.451463222],[-8239281.414530226,4940426.858859346],[-8239272.395870915,4940441.926048026],[-8239236.841681356,4940419.108130884],[-8239244.741280575,4940405.823313534],[-8239244.974266917,4940404.799698527],[-8239246.374004587,4940403.022617665],[-8239254.066778842,4940389.968307474],[-8239254.772761499,4940389.832488393],[-8239255.612964334,4940388.884001217],[-8239255.719448724,4940387.494492652],[-8239262.261445865,4940378.398318068],[-8239268.502152897,4940369.721188397],[-8239268.543959186,4940367.680921244],[-8239273.579000892,4940357.871189216],[-8239275.060835892,4940357.394171252],[-8239275.50920666,4940353.155458598],[-8239271.250503039,4940351.2581830565],[-8239265.617821409,4940363.902433373],[-8239265.830476128,4940367.499653213],[-8239264.815121702,4940368.735230394],[-8239263.130458905,4940367.692408202],[-8239250.677344535,4940387.81674261],[-8239241.738870883,4940402.260744509],[-8239236.187239713,4940411.969241007],[-8239235.890830007,4940411.75578687],[-8239233.799682879,4940414.410343442],[-8239234.362218047,4940414.852767883],[-8239232.985995114,4940416.598259784],[-8239217.7553137755,4940406.903750453],[-8239219.154573045,4940401.226536056],[-8239220.593008425,4940401.580727542],[-8239235.280117294,4940341.995386824],[-8239235.279811318,4940340.122370466],[-8239225.35221021,4940338.688191124],[-8239220.236059894,4940360.540587262],[-8239217.33435966,4940369.269895818],[-8239216.071719233,4940380.149795071],[-8239215.064057682,4940379.905066348],[-8239210.971611012,4940396.733721845],[-8239212.469321988,4940397.098318984],[-8239211.031169204,4940403.064437136],[-8239195.3955730805,4940467.329827611],[-8239238.552143708,4940488.055831861],[-8239168.258906371,4940478.928382118],[-8239169.972003132,4940488.105753931],[-8239172.70374705,4940497.954803924],[-8239180.092297871,4940513.446290851],[-8239198.467878288,4940546.328341913],[-8239202.048167918,4940611.426614053],[-8239201.813202264,4940613.067647141],[-8239201.352936625,4940614.66024091],[-8239200.676265128,4940616.173619894],[-8239199.796263755,4940617.578539331]]]]},"properties":{"shape_area":11989896.2552,"ntaname":"The Battery-Governors Island-Ellis Island-Liberty Island","cdtaname":"MN01 Financial District-Tribeca (CD 1 Equivalent)","shape_leng":49169.681822,"boroname":"Manhattan","ntatype":9,"nta2020":"MN0191","borocode":1,"countyfips":"061","ntaabbrev":"Btry_GvIsl","cdta2020":"MN01"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8238909.494644581,4944452.27036838],[-8238927.170377368,4944191.525756734],[-8239016.507461865,4944202.073493838],[-8239135.867286396,4944216.940292945],[-8239234.935038525,4944229.683741269],[-8239325.866312085,4944242.241721782],[-8239328.05816373,4944230.035740973],[-8239331.128907979,4944210.847132744],[-8239333.111466021,4944192.087072911],[-8239334.849774597,4944179.030004818],[-8239330.282989642,4944172.849602472],[-8239287.422124827,4944167.28844874],[-8239213.699489309,4944157.65387344],[-8239013.316657233,4944131.0198447015],[-8238973.513991872,4944122.789962003],[-8238939.554305983,4944107.303459035],[-8238939.652861681,4944106.661420738],[-8238957.379913089,4943991.158541482],[-8238974.180693501,4943881.689387467],[-8238932.648021379,4943876.969010816],[-8238915.738964644,4943875.047017612],[-8238906.271662024,4943873.980994626],[-8238892.960728626,4943872.290948531],[-8238883.4490525685,4943934.117228564],[-8238880.470709569,4943953.477023264],[-8238873.088677546,4944001.460412006],[-8238860.457803936,4944076.645857539],[-8238860.080931399,4944079.920417106],[-8238858.236110565,4944095.947271896],[-8238845.947456763,4944095.310914751],[-8238761.456405217,4944015.513214433],[-8238725.6268651225,4943978.335717166],[-8238679.549587092,4943936.075081645],[-8238604.542649245,4943865.80668044],[-8238596.427943042,4943858.203551994],[-8238566.511147144,4943830.176468443],[-8238541.596661361,4943808.090116276],[-8238523.085093321,4943789.493494369],[-8238491.579573225,4943759.927711398],[-8238343.455213876,4943621.189137034],[-8238300.539484476,4943577.798303314],[-8238233.40961125,4943515.064408371],[-8238224.203588022,4943503.346116274],[-8238158.302453943,4943446.214759518],[-8238079.724477665,4943373.175300909],[-8238033.257560163,4943329.562015653],[-8238005.170203167,4943303.334585252],[-8237931.751332763,4943234.031243485],[-8237852.051499515,4943160.019400287],[-8237798.2279014345,4943109.549946408],[-8237783.998736478,4943096.207410126],[-8237701.535587562,4943018.490199773],[-8237637.443797543,4942958.044358142],[-8237565.777214913,4942889.7408028655],[-8237485.275289403,4942823.796611333],[-8237393.281333963,4942782.014570375],[-8237306.551528013,4942742.017241014],[-8237203.578101134,4942696.823837947],[-8237175.792291856,4942757.249841398],[-8237133.450384634,4942850.092217437],[-8237064.324405589,4943022.468088203],[-8237018.780673582,4943168.730418421],[-8237000.529461979,4943226.43613406],[-8236993.109993679,4943249.894350796],[-8236982.486966809,4943283.7865881985],[-8236979.027307576,4943295.124130063],[-8236956.359225881,4943369.914578235],[-8236951.685658483,4943385.334176692],[-8236921.782409735,4943487.128398488],[-8236903.359335914,4943552.495715374],[-8236900.838768805,4943561.438782721],[-8236892.043215328,4943592.646079243],[-8236873.312682568,4943660.779867422],[-8236822.17786892,4943841.7438681675],[-8236818.913140016,4943853.388793091],[-8236826.549835158,4943856.8718861295],[-8236914.032120069,4943896.764963403],[-8236994.36171107,4943934.737032795],[-8237075.5101973275,4943972.6083047595],[-8237122.354049598,4943991.997641326],[-8237179.872408734,4944010.231345969],[-8237282.952348051,4944042.947719807],[-8237376.2867816845,4944096.384354333],[-8237459.887377356,4944150.572548967],[-8237544.1623534085,4944205.42190366],[-8237632.944558154,4944262.840309074],[-8237712.716173574,4944314.332536599],[-8237797.090053484,4944368.96975756],[-8237881.534677362,4944423.896381851],[-8237912.0065086,4944444.407546826],[-8237934.42052137,4944459.49476943],[-8237945.11847261,4944465.958490737],[-8237955.935190769,4944471.857591648],[-8237990.489664413,4944476.34586178],[-8238233.836117722,4944507.176561084],[-8238431.59701385,4944531.724769466],[-8238598.855862603,4944552.653754614],[-8238683.113589041,4944563.230340679],[-8238816.596979856,4944579.784170553],[-8238827.080644778,4944443.905526945],[-8238840.997424459,4944444.176299777],[-8238852.454207328,4944445.132795641],[-8238867.359473661,4944446.99775305],[-8238909.494644581,4944452.27036838]]]},"properties":{"shape_area":12916760.8492,"ntaname":"SoHo-Little Italy-Hudson Square","cdtaname":"MN02 Greenwich Village-SoHo (CD 2 Equivalent)","shape_leng":20252.9064821,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0201","borocode":1,"countyfips":"061","ntaabbrev":"SoHo","cdta2020":"MN02"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8236518.321317574,4945360.362742308],[-8236613.0550486045,4945419.538786436],[-8236662.595025361,4945444.887656855],[-8236678.642183224,4945453.184585419],[-8236720.03518394,4945474.58582745],[-8236931.753327332,4945590.080514908],[-8237290.542276784,4945788.586684842],[-8237346.356652727,4945687.166921294],[-8237398.251767377,4945594.677964119],[-8237450.202249338,4945500.318935566],[-8237478.703071331,4945448.622915519],[-8237499.210478712,4945411.446992665],[-8237551.699419792,4945317.0148710925],[-8237592.880721689,4945239.748323578],[-8237615.347654353,4945200.342476874],[-8237649.729851819,4945140.037684032],[-8237699.165356601,4945050.183621141],[-8237748.564435403,4944961.045665357],[-8237799.549007835,4944867.267169546],[-8237829.853617793,4944796.48571291],[-8237882.389381777,4944675.793835711],[-8237887.596839368,4944660.643407727],[-8237923.515100576,4944566.210044989],[-8237952.17763574,4944482.4433295205],[-8237955.935190769,4944471.857591644],[-8237945.11847261,4944465.958490737],[-8237934.42052137,4944459.494769422],[-8237912.0065086,4944444.407546826],[-8237881.534677368,4944423.896381843],[-8237797.090053489,4944368.96975756],[-8237712.716173574,4944314.332536599],[-8237632.944558162,4944262.840309074],[-8237544.162353403,4944205.421903655],[-8237459.887377368,4944150.572548967],[-8237376.28678168,4944096.384354328],[-8237282.952348063,4944042.9477198],[-8237179.872408734,4944010.231345969],[-8237122.354049598,4943991.997641326],[-8237075.510197333,4943972.6083047595],[-8236994.3617110755,4943934.737032795],[-8236914.03212008,4943896.764963403],[-8236826.549835158,4943856.871886123],[-8236818.913140028,4943853.388793085],[-8236816.313952566,4943862.915017169],[-8236788.239802707,4943966.004522212],[-8236775.714861835,4944013.778553122],[-8236758.562628345,4944074.066444012],[-8236728.826654267,4944183.39722653],[-8236711.524501504,4944247.994447987],[-8236703.631315764,4944277.463243265],[-8236701.9000074435,4944286.197530925],[-8236683.744618061,4944345.220691017],[-8236679.726630698,4944353.956060589],[-8236666.535984811,4944378.745043081],[-8236673.6060612975,4944382.907078409],[-8236665.983891066,4944429.347238125],[-8236656.378832865,4944504.785975408],[-8236637.542767555,4944593.689293992],[-8236616.466752552,4944674.17801253],[-8236609.864105638,4944693.631381301],[-8236585.405202767,4944794.055746786],[-8236572.549383536,4944899.939298223],[-8236552.713378848,4945003.518139307],[-8236541.758715246,4945062.663535415],[-8236540.506942211,4945069.2294833325],[-8236532.970603798,4945114.415920487],[-8236514.487564949,4945227.531561487],[-8236518.321317574,4945360.362742308]]]},"properties":{"shape_area":10599574.0092,"ntaname":"Greenwich Village","cdtaname":"MN02 Greenwich Village-SoHo (CD 2 Equivalent)","shape_leng":12973.9498078,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0202","borocode":1,"countyfips":"061","ntaabbrev":"GrnwchVlg","cdta2020":"MN02"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8238661.276210289,4946540.164880042],[-8238665.551116269,4946510.372258217],[-8238675.4805502,4946512.217706432],[-8238679.9432308655,4946503.29004893],[-8238769.499469101,4946551.821695782],[-8238787.908240901,4946520.016417385],[-8238849.007969471,4946554.88097374],[-8238910.925907553,4946441.609636215],[-8238798.213620829,4946379.412938296],[-8238778.689207772,4946410.102097432],[-8238713.126977965,4946374.401181159],[-8238718.147402063,4946364.915668568],[-8238704.198050585,4946357.942903987],[-8238689.991254152,4946343.736606462],[-8238695.728208835,4946306.89015258],[-8238700.007380881,4946296.189451905],[-8238701.906814302,4946292.749934306],[-8238701.665431645,4946288.046123758],[-8238698.912793495,4946283.527458673],[-8238699.503787647,4946281.229995423],[-8238714.487448313,4946269.280690953],[-8238835.486211003,4946275.168199939],[-8238908.509805297,4946280.781536215],[-8238919.695297241,4946275.3828098625],[-8239042.01978376,4946282.436758659],[-8239042.019919782,4946283.384380698],[-8239051.543537744,4946284.248861397],[-8239051.33679686,4946276.786660564],[-8238995.265318579,4946273.508906198],[-8238995.676409254,4946263.518805581],[-8238918.122393443,4946260.330660064],[-8238924.153439881,4946170.8557078075],[-8238925.548589096,4946156.3528842535],[-8238928.635629846,4946109.951926943],[-8238936.733182678,4946110.793007769],[-8238937.356108107,4946102.599056394],[-8238932.1952490425,4946098.557226834],[-8238932.309806019,4946068.221765337],[-8238931.448985011,4946067.188611693],[-8238920.774891195,4946066.592363131],[-8238910.023289578,4946060.093376423],[-8238838.64270374,4946054.221826276],[-8238817.26698095,4946055.729615753],[-8238811.016382066,4946053.855370036],[-8238799.265585875,4946053.981715502],[-8238803.233921065,4945985.059434026],[-8238806.089243694,4945935.466601313],[-8238872.539556963,4945940.138230724],[-8238872.874092982,4945938.897489456],[-8238874.412723975,4945939.219557196],[-8238877.045301094,4945939.517025747],[-8238879.694594177,4945939.503019379],[-8238882.323878883,4945939.1777322525],[-8238881.544691947,4945936.240001922],[-8238880.329404406,4945933.454241249],[-8238878.706075894,4945930.884769355],[-8238876.712187051,4945928.590911479],[-8238874.393774125,4945926.62562928],[-8238876.525283763,4945895.694741797],[-8238808.568272902,4945891.478512009],[-8238812.933958759,4945823.0045270305],[-8238818.754851248,4945731.7052523205],[-8238824.431593124,4945639.969977033],[-8238828.339971962,4945640.314408356],[-8238828.441885281,4945633.427950275],[-8238830.738896661,4945631.790682737],[-8238832.744730295,4945629.807368655],[-8238834.407826253,4945627.528989479],[-8238835.715190501,4945624.941279223],[-8238836.579838433,4945622.174001686],[-8238836.978291703,4945619.302299897],[-8238836.899731094,4945616.404152312],[-8238836.346290265,4945613.558255356],[-8238835.332997767,4945610.841886499],[-8238833.887368916,4945608.328805886],[-8238832.048658599,4945606.087253475],[-8238830.082846984,4945604.34048771],[-8238830.904356099,4945597.659468373],[-8238827.083157102,4945596.549830637],[-8238834.464632867,4945496.182032482],[-8238843.231511804,4945376.973147359],[-8238846.082568024,4945338.214075754],[-8238978.290875489,4945346.2567164535],[-8238980.147375149,4945306.06280722],[-8238848.225561847,4945297.772379814],[-8238851.97716642,4945238.598292435],[-8238848.239266389,4945237.104283473],[-8238844.810308432,4945234.995681863],[-8238841.790420992,4945232.334061049],[-8238839.84726882,4945229.934917594],[-8238838.335052222,4945227.243277634],[-8238837.29680643,4945224.335741748],[-8238836.762078036,4945221.2950544935],[-8238836.746084169,4945218.207749579],[-8238837.249279508,4945215.1616872195],[-8238838.257343381,4945212.243553768],[-8238839.74158737,4945209.536394757],[-8238841.75684667,4945207.265541436],[-8238844.099391481,4945205.334084809],[-8238846.712727992,4945203.788604346],[-8238849.533831995,4945202.666371176],[-8238852.494668763,4945201.994449297],[-8238855.52383379,4945201.7890428975],[-8238856.135167979,4945193.94661137],[-8239200.743974211,4945199.018833878],[-8239201.03671513,4945160.34461833],[-8238918.133084386,4945154.551284823],[-8238860.285540928,4945151.529236293],[-8238866.110502949,4945050.9437344605],[-8238874.814957273,4944923.933655874],[-8238876.820008765,4944894.677556494],[-8238884.862067818,4944777.332517087],[-8239202.519580719,4944811.306033892],[-8239205.018290701,4944812.971205473],[-8239210.469199969,4944808.428050029],[-8239209.409250547,4944805.09698082],[-8239225.202315779,4944641.645187695],[-8239239.153910911,4944509.76502551],[-8239244.283949624,4944486.478775178],[-8238909.494644581,4944452.27036838],[-8238867.359473661,4944446.99775305],[-8238852.454207328,4944445.132795641],[-8238840.997424463,4944444.176299771],[-8238827.080644778,4944443.905526939],[-8238816.596979861,4944579.784170553],[-8238683.113589041,4944563.230340673],[-8238598.855862603,4944552.653754611],[-8238431.597013856,4944531.724769466],[-8238233.836117722,4944507.176561079],[-8237990.489664419,4944476.34586178],[-8237955.935190769,4944471.857591644],[-8237952.17763574,4944482.4433295205],[-8237923.515100576,4944566.210044989],[-8237887.596839368,4944660.643407727],[-8237882.389381777,4944675.793835711],[-8237829.853617793,4944796.48571291],[-8237799.549007835,4944867.267169546],[-8237748.564435403,4944961.045665357],[-8237699.165356601,4945050.183621141],[-8237649.729851819,4945140.037684032],[-8237615.347654353,4945200.342476874],[-8237592.880721689,4945239.748323578],[-8237551.699419792,4945317.0148710925],[-8237499.210478712,4945411.446992665],[-8237478.703071331,4945448.622915519],[-8237450.202249338,4945500.318935566],[-8237398.251767377,4945594.677964119],[-8237346.356652727,4945687.166921294],[-8237290.542276784,4945788.586684842],[-8237607.167316882,4945962.976191878],[-8237923.268410227,4946138.625461595],[-8237988.225932782,4946174.464520628],[-8238212.890595335,4946299.261466853],[-8238239.2382367775,4946313.793292453],[-8238248.135798354,4946318.570026594],[-8238418.30207549,4946412.969232308],[-8238509.626644558,4946463.275481211],[-8238555.585748666,4946489.133501784],[-8238610.689410875,4946520.484672686],[-8238620.906663848,4946522.448970189],[-8238632.327237173,4946523.769651417],[-8238646.227428995,4946531.572277287],[-8238661.276210289,4946540.164880042]],[[-8238685.521752993,4946494.641257722],[-8238690.263461056,4946486.55044588],[-8238670.66590057,4946478.025990673],[-8238675.1328739235,4946449.776152591],[-8238682.683956682,4946395.886660494],[-8238684.4637540355,4946383.184631036],[-8238702.249024934,4946392.256484599],[-8238707.548549107,4946383.04988152],[-8238773.947772113,4946419.587652785],[-8238735.736764783,4946491.846035552],[-8238777.585673864,4946516.39093169],[-8238765.313470723,4946538.989325676],[-8238685.521752993,4946494.641257722]]]},"properties":{"shape_area":14417744.1253,"ntaname":"West Village","cdtaname":"MN02 Greenwich Village-SoHo (CD 2 Equivalent)","shape_leng":23882.339337,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0203","borocode":1,"countyfips":"061","ntaabbrev":"WstVlg","cdta2020":"MN02"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8235916.473649084,4941713.429681866],[-8235934.148334804,4941853.150028258],[-8235935.224690138,4941860.378422468],[-8235936.30227265,4941867.605792817],[-8235937.306315469,4941873.677549294],[-8235939.440729028,4941887.858046059],[-8235948.488006703,4941964.920342341],[-8235956.140977269,4942033.5831269985],[-8235967.007030392,4942129.754596093],[-8235975.267119036,4942202.828967556],[-8235976.008872243,4942209.390758395],[-8235978.949612347,4942235.407066353],[-8235985.223977418,4942339.136257772],[-8235999.610046813,4942347.757273034],[-8236010.825089053,4942437.862193948],[-8236037.654037191,4942435.057105614],[-8236047.252591996,4942434.017366752],[-8236067.634997087,4942431.799209069],[-8236213.675492252,4942415.987410219],[-8236269.4044943685,4942409.954844147],[-8236351.534640776,4942401.064249591],[-8236384.621299622,4942397.478361513],[-8236455.270486149,4942389.846693141],[-8236553.628112834,4942429.576072037],[-8236554.450052057,4942442.264423219],[-8236553.787180521,4942455.005246143],[-8236551.650305396,4942467.53926035],[-8236501.759358514,4942597.441966557],[-8236435.018472086,4942766.611416129],[-8236523.579046868,4942805.137413684],[-8236607.3702046815,4942838.5872099325],[-8236660.061271399,4942859.414352724],[-8236674.301447665,4942865.079563725],[-8236687.500958001,4942870.331023074],[-8236776.324625766,4942905.57182544],[-8236873.582038593,4942944.201257622],[-8236955.238699867,4942976.6812922675],[-8237064.324405589,4943022.468088203],[-8237133.450384634,4942850.092217437],[-8237175.792291856,4942757.249841398],[-8237203.578101134,4942696.823837947],[-8237306.551528013,4942742.017241014],[-8237393.281333963,4942782.014570375],[-8237485.275289403,4942823.796611333],[-8237565.777214913,4942889.7408028655],[-8237580.912845467,4942857.488537143],[-8237634.635085794,4942742.510419996],[-8237657.704066467,4942692.15529433],[-8237706.704949575,4942600.156502246],[-8237730.211139916,4942559.201816089],[-8237701.022800357,4942541.512752517],[-8237692.92785773,4942423.739879753],[-8237725.421243908,4942444.997900859],[-8237750.120922893,4942356.522798241],[-8237816.684595924,4942323.967097307],[-8237782.249457638,4942296.714119721],[-8237746.039747545,4942262.550139808],[-8237742.105775624,4942230.973743172],[-8237741.787482408,4942228.418957971],[-8237741.578060778,4942223.928689593],[-8237739.47424305,4942033.411295414],[-8237738.92338641,4942014.567422285],[-8237735.78518377,4941995.622246055],[-8237730.039901782,4941977.109129187],[-8237721.811470289,4941959.557897757],[-8237711.364513891,4941943.444037972],[-8237699.076092533,4941929.149314699],[-8237716.197951464,4941900.407881909],[-8237726.130294009,4941883.734941383],[-8237743.211507713,4941855.062020415],[-8237754.000474462,4941836.951142871],[-8237768.82954691,4941810.902006918],[-8237779.7721830765,4941793.254909574],[-8237785.09544753,4941784.290379603],[-8237800.31953413,4941758.066985747],[-8237778.587098531,4941734.986076845],[-8237695.138865481,4941646.406053194],[-8237665.263736053,4941614.693458563],[-8237657.399276715,4941606.553670626],[-8237649.53620289,4941598.413956662],[-8237637.681538216,4941586.143168399],[-8237627.783280741,4941575.898290748],[-8237615.490048737,4941563.36265813],[-8237586.115384329,4941533.410753788],[-8237572.94633053,4941518.907033324],[-8237568.235522539,4941513.616666655],[-8237563.984681811,4941508.761101587],[-8237555.8939534575,4941500.347383092],[-8237545.954747781,4941490.011543572],[-8237536.873249943,4941497.347484812],[-8237529.152380182,4941506.140359922],[-8237517.786549554,4941512.574055372],[-8237511.138056542,4941518.25682333],[-8237509.677384137,4941521.863233521],[-8237503.610799414,4941525.303277872],[-8237500.0501165865,4941527.322359086],[-8237482.146230594,4941538.707711669],[-8237478.410309176,4941541.084142302],[-8237451.519678277,4941555.479148929],[-8237428.976219482,4941567.135757851],[-8237339.84939193,4941599.611895128],[-8237270.957990362,4941613.550738464],[-8237199.154023567,4941626.616951742],[-8237175.2061485015,4941630.974839675],[-8237105.288286859,4941643.888948471],[-8237038.8557022605,4941658.031915537],[-8237016.5067339875,4941667.052901521],[-8236990.056840487,4941672.996290075],[-8236975.314759924,4941673.612528195],[-8236945.473008027,4941674.876526279],[-8236846.21374444,4941694.736107129],[-8236791.967078923,4941706.80355384],[-8236768.3148377435,4941711.812493092],[-8236761.92806655,4941713.2188022435],[-8236756.13425706,4941714.491033509],[-8236741.236812775,4941715.7311043115],[-8236725.913922737,4941720.069851337],[-8236718.656611445,4941721.563076577],[-8236679.526709023,4941729.619199324],[-8236647.99460158,4941735.016742317],[-8236616.969364744,4941739.7384522315],[-8236586.364695809,4941746.230442422],[-8236546.740519377,4941754.494066044],[-8236500.754195453,4941763.035710603],[-8236481.756915393,4941766.739733403],[-8236469.085874968,4941768.964118895],[-8236457.618421411,4941770.675647367],[-8236406.500901137,4941778.538535198],[-8236395.226245946,4941780.275234476],[-8236388.506914337,4941780.880047508],[-8236386.379974773,4941777.420046996],[-8236383.974612304,4941774.693197295],[-8236380.791219539,4941773.46217465],[-8236378.323169286,4941773.008542914],[-8236377.348580624,4941772.29576784],[-8236361.840849479,4941664.66346009],[-8236359.881732078,4941661.67064599],[-8236356.192791831,4941658.448488057],[-8236349.970684503,4941657.443293271],[-8236323.790598001,4941660.689814853],[-8236322.766593025,4941662.156349876],[-8236128.183179352,4941686.647317334],[-8235916.473649084,4941713.429681866]]]},"properties":{"shape_area":11542590.3767,"ntaname":"Chinatown-Two Bridges","cdtaname":"MN03 Lower East Side-Chinatown (CD 3 Equivalent)","shape_leng":16668.1111161,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0301","borocode":1,"countyfips":"061","ntaabbrev":"Chntwn","cdta2020":"MN03"}}, +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8235689.661916239,4941133.0864809565],[-8235680.135055518,4941133.223418022],[-8235675.241128579,4941134.493593058],[-8235672.192124841,4941161.547240687],[-8235529.178546892,4941170.401044978],[-8235529.909601261,4941185.23497854],[-8235685.641741279,4941173.992063899],[-8235689.661916239,4941133.0864809565]]],[[[-8234700.351229013,4943042.922209416],[-8234820.21431326,4943075.701202864],[-8234824.499234459,4943076.884049448],[-8234830.655498531,4943078.557842423],[-8234835.695918585,4943075.463432764],[-8234845.347051743,4943069.538464405],[-8234850.791689874,4943067.6467285985],[-8234858.754278556,4943064.706925965],[-8234867.556358455,4943065.353572659],[-8234872.526069217,4943065.718747085],[-8234914.222153757,4943082.6950010015],[-8234909.6116409255,4943094.032209135],[-8234916.118456739,4943095.795709304],[-8234982.003991912,4943113.6573631475],[-8235010.197253858,4943121.293164331],[-8235024.287294696,4943125.206222246],[-8235053.578988795,4943133.341681954],[-8235106.882797423,4943147.174148212],[-8235196.765963403,4943195.247458593],[-8235277.040934629,4943238.461629624],[-8235378.104367347,4943279.300518071],[-8235424.900021623,4943297.810443237],[-8235561.561617164,4943351.864913263],[-8235657.063797062,4943389.916412444],[-8235750.230831589,4943427.0546482345],[-8235841.600381512,4943463.661127641],[-8235936.659382171,4943501.371295],[-8236030.063055373,4943538.626033147],[-8236068.2434139745,4943555.3802466355],[-8236115.654213453,4943576.184559814],[-8236210.287787726,4943609.528858892],[-8236291.447071049,4943642.783331384],[-8236366.999594628,4943673.002131225],[-8236377.576439193,4943677.40654619],[-8236390.734959509,4943682.768580142],[-8236461.461696924,4943712.57240978],[-8236559.582567915,4943749.438707166],[-8236643.126524978,4943782.12858184],[-8236670.841405736,4943793.889832097],[-8236679.931352864,4943797.5194535125],[-8236773.872138058,4943835.268330397],[-8236810.983378817,4943850.186853834],[-8236818.913140016,4943853.388793091],[-8236822.17786892,4943841.7438681675],[-8236873.312682568,4943660.779867422],[-8236892.043215328,4943592.646079243],[-8236900.838768805,4943561.438782721],[-8236903.359335914,4943552.495715374],[-8236921.782409735,4943487.128398488],[-8236951.685658483,4943385.334176692],[-8236956.359225881,4943369.914578235],[-8236979.027307576,4943295.124130063],[-8236982.486966809,4943283.7865881985],[-8236993.109993679,4943249.894350796],[-8237000.529461979,4943226.43613406],[-8237018.780673582,4943168.730418421],[-8237064.324405589,4943022.468088203],[-8236955.238699867,4942976.6812922675],[-8236873.582038593,4942944.201257622],[-8236776.324625766,4942905.57182544],[-8236687.500958001,4942870.331023074],[-8236674.301447665,4942865.079563725],[-8236660.061271399,4942859.414352724],[-8236607.3702046815,4942838.5872099325],[-8236523.579046868,4942805.137413684],[-8236435.018472086,4942766.611416129],[-8236501.759358514,4942597.441966557],[-8236551.650305396,4942467.53926035],[-8236553.787180521,4942455.005246143],[-8236554.450052057,4942442.264423219],[-8236553.628112834,4942429.576072037],[-8236455.270486149,4942389.846693141],[-8236384.621299622,4942397.478361513],[-8236351.534640776,4942401.064249591],[-8236269.4044943685,4942409.954844147],[-8236213.675492252,4942415.987410219],[-8236067.634997087,4942431.799209069],[-8236047.252591996,4942434.017366752],[-8236037.654037191,4942435.057105614],[-8236010.825089053,4942437.862193948],[-8235999.610046813,4942347.757273034],[-8235985.223977418,4942339.136257772],[-8235978.949612347,4942235.407066353],[-8235976.008872243,4942209.390758395],[-8235975.267119036,4942202.828967556],[-8235967.007030392,4942129.754596093],[-8235956.140977269,4942033.5831269985],[-8235948.488006703,4941964.920342341],[-8235939.440729028,4941887.858046059],[-8235937.306315469,4941873.677549294],[-8235936.30227265,4941867.605792817],[-8235935.224690138,4941860.378422468],[-8235934.148334804,4941853.150028258],[-8235916.473649084,4941713.429681866],[-8235821.392846602,4941725.456795817],[-8235756.588210351,4941733.653688168],[-8235554.610229274,4941759.73733242],[-8235559.138686793,4941817.102141531],[-8235472.753063002,4941825.353393538],[-8235452.76464439,4941827.136206347],[-8235453.390667417,4941837.707379698],[-8235447.4816526435,4941838.089912385],[-8235419.983468208,4941840.516298656],[-8235336.909009801,4941847.793241269],[-8235327.681957053,4941848.601029025],[-8235325.64251985,4941848.784914813],[-8235279.688681993,4941852.93833076],[-8235261.818321355,4941854.469528739],[-8235234.643053191,4941856.799283178],[-8235221.740661078,4941857.999090788],[-8235213.539693054,4941859.309259242],[-8235202.167530228,4941863.022917228],[-8235192.927883119,4941867.2311473405],[-8235182.101872322,4941872.914424891],[-8235173.354407677,4941879.582151345],[-8235162.253466427,4941889.036871068],[-8235140.600645018,4941908.4932562085],[-8235106.69843876,4941941.505018323],[-8235088.270431218,4941960.361298653],[-8235080.94246157,4941968.9975234615],[-8235061.803189521,4941992.829479335],[-8235049.934378498,4942009.828372988],[-8235031.886455087,4942038.9074786],[-8235026.911922299,4942048.3069048],[-8235018.376388056,4942064.434704222],[-8235005.3023064565,4942095.099981529],[-8234989.053413515,4942139.215035786],[-8234984.419851552,4942145.085463038],[-8234976.196180574,4942166.132632564],[-8234967.138258585,4942193.252739702],[-8234947.866931962,4942248.910874726],[-8234948.733544778,4942250.096077097],[-8234886.794419137,4942425.594404663],[-8234886.789930632,4942425.606363774],[-8234884.433563437,4942432.282507947],[-8234883.13935825,4942435.948589402],[-8234880.316254365,4942443.945570114],[-8234877.5441985605,4942451.0768382745],[-8234876.004795769,4942455.102722658],[-8234873.691123834,4942461.208203001],[-8234849.205897238,4942533.046551466],[-8234825.520105623,4942605.153020938],[-8234802.63668964,4942677.518719174],[-8234780.558491087,4942750.134721571],[-8234759.288252613,4942822.992072252],[-8234738.828617304,4942896.081785174],[-8234719.182128428,4942969.394845255],[-8234700.351229013,4943042.922209416]]]]},"properties":{"shape_area":16473191.2173,"ntaname":"Lower East Side","cdtaname":"MN03 Lower East Side-Chinatown (CD 3 Equivalent)","shape_leng":22123.0274583,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0302","borocode":1,"countyfips":"061","ntaabbrev":"LES","cdta2020":"MN03"}}, +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8233985.585980986,4943001.7945941435],[-8233978.90142452,4943014.909728832],[-8233976.420582654,4943019.777235909],[-8233989.0842235,4943026.217704036],[-8233994.393799989,4943016.21209094],[-8234002.996758835,4943001.347003065],[-8233990.2408282,4942994.335564998],[-8233989.195089376,4942995.912319846],[-8233923.9029161185,4942950.8585967105],[-8233920.064135604,4942957.864652307],[-8233985.585980986,4943001.7945941435]]],[[[-8233906.796543819,4943137.433900242],[-8233909.230097539,4943154.58588623],[-8233907.068245438,4943156.118944756],[-8233906.091510472,4943157.791825682],[-8233905.672431989,4943159.883271172],[-8233906.507775415,4943163.299787333],[-8233908.528778777,4943165.252751021],[-8233912.154138136,4943165.672440386],[-8233914.733975613,4943164.90655647],[-8233916.32947469,4943163.390705683],[-8233917.114352842,4943162.127374377],[-8233917.420231984,4943159.730823015],[-8233916.985262013,4943157.551988406],[-8233915.72231447,4943155.7649372835],[-8233913.936296162,4943154.32625451],[-8233910.973583598,4943154.052830738],[-8233909.493106719,4943137.468512617],[-8233914.64200248,4943131.523265981],[-8233918.529485912,4943134.269563339],[-8233928.256134969,4943120.892027351],[-8233899.21422213,4943099.608090465],[-8233895.95096554,4943104.212589022],[-8233830.735405006,4943056.35415498],[-8233828.810256136,4943058.964659617],[-8233897.936069495,4943109.233140006],[-8233893.331637912,4943117.07477109],[-8233898.24697103,4943116.553797101],[-8233906.088242522,4943122.308685852],[-8233907.445575695,4943128.06101687],[-8233909.3800220415,4943127.395286092],[-8233913.283281196,4943129.837044419],[-8233906.796543819,4943137.433900242]]],[[[-8233749.785869047,4943303.330739644],[-8233812.731530108,4943357.084196284],[-8233821.450503928,4943347.501025176],[-8233759.201823284,4943294.445175083],[-8233749.785869047,4943303.330739644]]],[[[-8233727.694902854,4943364.55910608],[-8233718.626351315,4943376.547467467],[-8233729.777247627,4943387.567925168],[-8233738.426870791,4943377.113219774],[-8233749.587574048,4943363.870620863],[-8233737.878501538,4943353.547237726],[-8233731.042633871,4943361.632105281],[-8233679.050484086,4943312.806535208],[-8233675.012128762,4943317.986783617],[-8233727.694902854,4943364.55910608]]],[[[-8233637.730775183,4943377.0463554505],[-8233669.1549814725,4943402.618470154],[-8233676.566630747,4943393.383354179],[-8233642.154373194,4943365.197717256],[-8233637.730775183,4943377.0463554505]]],[[[-8233661.611234854,4943337.737653101],[-8233657.295888229,4943343.83343777],[-8233728.64251449,4943462.388683582],[-8233740.637048514,4943456.536915571],[-8233665.38804775,4943332.402602557],[-8233661.611234854,4943337.737653101]]],[[[-8234700.351229013,4943042.922209416],[-8234685.819679765,4943103.2593037505],[-8234672.035649205,4943163.7719397405],[-8234659.001260983,4943224.450858705],[-8234646.718524127,4943285.286776211],[-8234635.189332782,4943346.270383436],[-8234624.415465791,4943407.392348628],[-8234582.158346281,4943642.753575577],[-8234569.840363608,4943708.878954522],[-8234565.576823066,4943732.764556927],[-8234561.560401308,4943754.922886567],[-8234554.87247845,4943791.6427639425],[-8234551.36013555,4943811.00421701],[-8234549.520802748,4943823.18895483],[-8234543.836407027,4943852.732242085],[-8234542.748821261,4943858.323875],[-8234536.228047439,4943895.377603438],[-8234534.723861025,4943901.887543909],[-8234534.38778152,4943906.227776226],[-8234533.384579634,4943911.9025392365],[-8234532.715446051,4943915.239756363],[-8234529.455008894,4943933.51700904],[-8234525.942739065,4943956.550926304],[-8234521.092479165,4943985.594195052],[-8234518.666468677,4944002.869067724],[-8234517.997488374,4944005.539709982],[-8234515.4886041675,4944022.232260102],[-8234512.561072277,4944041.844573672],[-8234511.388666312,4944053.361825768],[-8234509.716725086,4944065.046183698],[-8234504.94641424,4944098.992820223],[-8234502.155010964,4944098.883867628],[-8234500.226961632,4944099.916942875],[-8234499.497201067,4944101.675994439],[-8234496.25546958,4944126.019172195],[-8234494.085906372,4944143.918429658],[-8234491.517866031,4944161.453420628],[-8234488.607139158,4944183.47504129],[-8234483.847703794,4944218.026377355],[-8234502.6658977,4944230.497260151],[-8234509.422156731,4944237.082783737],[-8234515.62599438,4944241.812577461],[-8234522.795808905,4944247.570090602],[-8234530.956018964,4944254.972729442],[-8234669.862065772,4944335.022682427],[-8234933.673325753,4944486.4033401],[-8234949.7727671685,4944494.778527799],[-8234992.505565431,4944515.588081473],[-8235071.32408456,4944558.879510376],[-8235141.908998881,4944598.158461534],[-8235196.293078301,4944628.421495813],[-8235316.636064478,4944698.295236539],[-8235407.145654072,4944749.752860419],[-8235453.785764376,4944776.130482857],[-8235601.450497694,4944857.505281325],[-8235628.450455762,4944871.638483335],[-8235690.750132664,4944903.796681249],[-8235700.493947386,4944908.955106443],[-8235745.924976313,4944933.935915191],[-8235773.78447697,4944949.056273368],[-8235801.811355779,4944964.701480711],[-8235812.28419668,4944970.429676628],[-8235966.459997934,4945055.224848887],[-8236215.258025186,4945193.146292796],[-8236258.8346583415,4945217.269535306],[-8236393.237868525,4945291.670897641],[-8236399.920854035,4945295.341112846],[-8236518.321317568,4945360.362742308],[-8236514.487564936,4945227.531561487],[-8236532.970603792,4945114.415920487],[-8236540.506942206,4945069.2294833325],[-8236541.758715239,4945062.66353542],[-8236552.713378841,4945003.518139307],[-8236572.549383523,4944899.939298227],[-8236585.405202767,4944794.055746793],[-8236609.864105626,4944693.631381301],[-8236616.46675254,4944674.17801253],[-8236637.54276755,4944593.689293992],[-8236656.378832852,4944504.785975408],[-8236665.983891072,4944429.347238125],[-8236673.606061286,4944382.907078409],[-8236666.535984817,4944378.745043084],[-8236679.726630686,4944353.956060589],[-8236683.744618061,4944345.220691017],[-8236701.90000743,4944286.197530925],[-8236703.631315747,4944277.463243269],[-8236711.524501504,4944247.994447996],[-8236728.826654267,4944183.39722653],[-8236758.562628339,4944074.066444012],[-8236775.714861823,4944013.778553126],[-8236788.239802701,4943966.0045222165],[-8236816.313952554,4943862.915017178],[-8236818.913140016,4943853.388793091],[-8236810.983378817,4943850.186853834],[-8236773.872138058,4943835.268330397],[-8236679.931352864,4943797.5194535125],[-8236670.841405736,4943793.889832097],[-8236643.126524978,4943782.12858184],[-8236559.582567915,4943749.438707166],[-8236461.461696924,4943712.57240978],[-8236390.734959509,4943682.768580142],[-8236377.576439193,4943677.40654619],[-8236366.999594628,4943673.002131225],[-8236291.447071049,4943642.783331384],[-8236210.287787726,4943609.528858892],[-8236115.654213453,4943576.184559814],[-8236068.2434139745,4943555.3802466355],[-8236030.063055373,4943538.626033147],[-8235936.659382171,4943501.371295],[-8235841.600381512,4943463.661127641],[-8235750.230831589,4943427.0546482345],[-8235657.063797062,4943389.916412444],[-8235561.561617164,4943351.864913263],[-8235424.900021623,4943297.810443237],[-8235378.104367347,4943279.300518071],[-8235277.040934629,4943238.461629624],[-8235196.765963403,4943195.247458593],[-8235106.882797423,4943147.174148212],[-8235053.578988795,4943133.341681954],[-8235024.287294696,4943125.206222246],[-8235010.197253858,4943121.293164331],[-8234982.003991912,4943113.6573631475],[-8234916.118456739,4943095.795709304],[-8234909.6116409255,4943094.032209135],[-8234914.222153757,4943082.6950010015],[-8234872.526069217,4943065.718747085],[-8234867.556358455,4943065.353572659],[-8234858.754278556,4943064.706925965],[-8234850.791689874,4943067.6467285985],[-8234845.347051743,4943069.538464405],[-8234835.695918585,4943075.463432764],[-8234830.655498531,4943078.557842423],[-8234824.499234459,4943076.884049448],[-8234820.21431326,4943075.701202864],[-8234700.351229013,4943042.922209416]]]]},"properties":{"shape_area":18984761.5581,"ntaname":"East Village","cdtaname":"MN03 Lower East Side-Chinatown (CD 3 Equivalent)","shape_leng":22489.5237874,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0303","borocode":1,"countyfips":"061","ntaabbrev":"EstVlg","cdta2020":"MN03"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8237841.384693299,4949438.303595094],[-8237887.312945535,4949355.425886285],[-8238132.097971613,4949493.474835754],[-8238135.140840609,4949494.111445773],[-8238137.636476685,4949492.480462617],[-8238149.536567705,4949471.939438412],[-8238149.728928306,4949469.54000723],[-8238148.289146546,4949467.332999667],[-8237902.280953904,4949328.384399781],[-8237952.556719496,4949238.38234134],[-8237991.126071929,4949260.25120806],[-8238019.122076154,4949209.087511605],[-8238041.734049554,4949168.510418462],[-8238068.182095601,4949121.871572311],[-8238079.642813845,4949102.8224522155],[-8238040.00874091,4949079.916649604],[-8238050.740326513,4949059.334453935],[-8238100.0221556565,4949086.716664964],[-8238102.259248846,4949082.124855451],[-8238112.383137821,4949089.658673286],[-8238111.0398217635,4949091.336935529],[-8238117.507750143,4949096.418821428],[-8238123.095811029,4949089.304784314],[-8238116.503951284,4949084.125446666],[-8238114.856123978,4949085.774709362],[-8238104.142988241,4949078.358255969],[-8238110.136003896,4949067.4013901325],[-8238115.089382617,4949058.345468139],[-8238096.489931492,4949047.633530106],[-8238066.065065838,4949030.75179504],[-8238077.2907434385,4949010.98223225],[-8238110.560902757,4949029.43072531],[-8238143.794188499,4948968.4249343],[-8238345.319025645,4949079.369565148],[-8238359.609070888,4949086.343734637],[-8238363.210780341,4949086.803620592],[-8238366.66086995,4949086.573926809],[-8238369.190435793,4949084.043984485],[-8238421.954667918,4948989.9269594345],[-8238422.710973593,4948985.805859414],[-8238420.8596898345,4948981.7692636065],[-8238417.579639714,4948979.413707991],[-8238202.372704928,4948861.850758275],[-8238229.968503181,4948813.765430863],[-8238195.328416583,4948792.627342195],[-8238177.089254051,4948781.747073165],[-8238222.502202388,4948697.153890383],[-8238245.543261132,4948710.496486228],[-8238289.016112961,4948632.351902997],[-8238301.757384744,4948607.596070723],[-8238306.552117581,4948598.50603227],[-8238321.837492911,4948569.5757101225],[-8238333.110606754,4948549.128293243],[-8238363.0406898735,4948494.7747606905],[-8238374.542310302,4948473.887398503],[-8238388.484166069,4948448.341818658],[-8238399.998968527,4948427.76053614],[-8238417.010426831,4948396.875262145],[-8238437.661243558,4948360.524903142],[-8238451.813083142,4948368.300096777],[-8238467.599146663,4948339.56522559],[-8238453.281130694,4948331.699132744],[-8238454.277112845,4948329.886298207],[-8238474.051173152,4948293.850726108],[-8238487.92269293,4948301.687049343],[-8238504.382215917,4948273.324648453],[-8238490.517130253,4948265.279110521],[-8238503.789594108,4948219.945537852],[-8238522.016094687,4948157.065976692],[-8238534.1572968755,4948115.176403045],[-8238572.229228771,4947982.594733559],[-8238609.453777123,4948003.565461589],[-8238621.05089837,4947982.799455073],[-8238625.974141062,4947985.63682874],[-8238625.263689381,4947986.880336694],[-8238736.652579529,4948048.866130851],[-8238737.540715935,4948047.62241526],[-8238743.0177771775,4948050.419762855],[-8238732.03979876,4948070.078032691],[-8238746.514775649,4948078.161572077],[-8238752.239477998,4948067.910678131],[-8238746.619886162,4948064.772327905],[-8238754.46255353,4948050.728559064],[-8238745.0846655,4948045.491537242],[-8238743.182327112,4948048.897790893],[-8238738.401491911,4948046.2279593],[-8238740.560020147,4948043.003296708],[-8238627.689407781,4947981.356690373],[-8238626.318059447,4947983.777096222],[-8238622.234906095,4947981.733225753],[-8238623.91150714,4947978.731106256],[-8238580.337304533,4947954.359114585],[-8238586.191736338,4947933.971162199],[-8238680.701575962,4947985.926950403],[-8238682.142093645,4947983.307647907],[-8238606.853734994,4947941.9191233935],[-8238597.5465333145,4947937.676240212],[-8238598.6122407,4947935.3667720435],[-8238607.7077852525,4947940.364928471],[-8238634.701263652,4947955.204946995],[-8238636.276688109,4947952.33195673],[-8238645.043273724,4947957.139136371],[-8238643.899024384,4947958.934402475],[-8238650.621717539,4947962.629872986],[-8238655.99044545,4947952.057134986],[-8238656.1698425505,4947951.7040115865],[-8238649.659679566,4947948.576893348],[-8238648.531093308,4947949.573128409],[-8238639.454463641,4947944.459991248],[-8238640.757161184,4947942.493542665],[-8238604.473363208,4947924.885317523],[-8238605.361357866,4947922.398299639],[-8238615.88302927,4947926.040753868],[-8238658.288417309,4947948.36409614],[-8238659.870263662,4947945.538677735],[-8238617.483000333,4947923.642779309],[-8238592.159966331,4947914.215067026],[-8238601.99272168,4947880.396329227],[-8238607.1570915505,4947876.827524159],[-8238614.291288497,4947853.069470875],[-8238611.486950826,4947847.347521452],[-8238644.303501309,4947733.372981973],[-8238818.368464634,4947831.927353493],[-8238830.918686559,4947788.190745191],[-8238655.8305859,4947692.809363896],[-8238672.403500173,4947636.327343195],[-8238706.727562088,4947634.09786222],[-8238749.74496518,4947482.161069879],[-8238875.512747587,4947499.675636773],[-8238878.957036836,4947499.416888691],[-8238881.627397829,4947498.210522104],[-8238883.435603626,4947495.88511788],[-8238890.835954748,4947447.738519548],[-8238890.83560187,4947444.982439446],[-8238888.510068394,4947441.882497117],[-8238885.409609508,4947440.16005558],[-8238701.949298481,4947415.055909455],[-8238702.455113537,4947411.311125411],[-8238690.51156858,4947409.697822613],[-8238690.404661825,4947408.622173926],[-8238652.421487304,4947403.568648902],[-8238652.09883578,4947405.721154191],[-8238641.876638542,4947404.32359962],[-8238641.876825947,4947406.151848441],[-8238638.190105233,4947406.032340072],[-8238650.703973381,4947314.366292251],[-8238896.456613378,4947347.750144485],[-8238900.621899054,4947347.874961013],[-8238903.762646972,4947345.826238256],[-8238905.453348316,4947342.259145613],[-8238912.111118865,4947291.664526195],[-8238910.779316851,4947288.788681266],[-8238906.546705256,4947286.083096335],[-8238744.5585622005,4947263.551520801],[-8238745.127468331,4947258.921082564],[-8238750.880988849,4947259.62695734],[-8238751.4894071035,4947254.669478017],[-8238705.592846912,4947249.039836613],[-8238705.464219331,4947253.6306918245],[-8238707.453230702,4947253.6859169975],[-8238707.384962688,4947256.144092523],[-8238706.648223442,4947256.1230357615],[-8238706.586425699,4947258.32827581],[-8238659.485253988,4947252.028899766],[-8238671.261073601,4947164.649349614],[-8238916.360534438,4947197.362441333],[-8238920.681775716,4947197.992383484],[-8238923.809776266,4947197.116329092],[-8238926.17438299,4947194.51434367],[-8238932.341147212,4947144.9309471715],[-8238932.649006662,4947141.042049589],[-8238930.0435612565,4947137.814031469],[-8238926.258522868,4947136.101874108],[-8238684.672929967,4947102.121802166],[-8238686.092102443,4947091.22633628],[-8238686.542569727,4947087.767986954],[-8238687.078445513,4947083.653627834],[-8238687.6841227915,4947079.0033391295],[-8238688.895353353,4947069.706005002],[-8238689.551989848,4947064.664907473],[-8238690.837795771,4947054.7926440025],[-8238691.442170996,4947050.152839264],[-8238692.687351754,4947040.594139194],[-8238693.358512938,4947035.440946888],[-8238694.551357407,4947026.283864012],[-8238695.835070831,4947016.428925923],[-8238749.733806302,4947023.751328964],[-8238749.578569538,4947026.58261026],[-8238769.786062513,4947029.922886216],[-8238769.7863147305,4947032.1042834],[-8238816.887914612,4947038.785284635],[-8238817.323648608,4947034.133681107],[-8238937.466711291,4947049.837625858],[-8238942.402826561,4947049.99387631],[-8238946.007415158,4947047.329575711],[-8238946.869066274,4947044.195214441],[-8238953.969354214,4946994.682817359],[-8238952.143293148,4946991.229675512],[-8238952.577790397,4946987.541717556],[-8238953.059827546,4946983.4525512885],[-8238959.943920629,4946925.069240643],[-8238941.492393732,4946923.314764652],[-8238941.306790392,4946925.269399153],[-8238953.966763481,4946926.385437195],[-8238955.362871242,4946929.270453875],[-8238949.163385362,4946982.731290878],[-8238885.434561301,4946974.012806595],[-8238883.997576629,4946971.9673994435],[-8238885.39571822,4946963.027258456],[-8238886.985229411,4946952.86226024],[-8238887.779946991,4946947.779826315],[-8238889.756508602,4946935.140302237],[-8238890.305142935,4946931.632332676],[-8238892.478875398,4946917.731358586],[-8238909.766629987,4946919.513736508],[-8238909.991638809,4946917.333610282],[-8238871.797004969,4946913.39630509],[-8238871.599521293,4946915.555363182],[-8238883.8969427,4946916.67933422],[-8238885.961262463,4946918.086755045],[-8238884.770476324,4946928.027722223],[-8238884.229349505,4946932.546170851],[-8238883.025406484,4946942.597716391],[-8238882.292046362,4946948.719949062],[-8238881.3157990435,4946956.869633437],[-8238880.710371133,4946961.924265949],[-8238879.772986791,4946970.466625923],[-8238878.365583015,4946972.156529736],[-8238816.205433102,4946964.0279768985],[-8238814.631537527,4946962.551916753],[-8238814.928683704,4946959.969566757],[-8238815.601873488,4946954.121893775],[-8238816.802702994,4946943.689814037],[-8238818.2653530575,4946930.982254255],[-8238818.744296451,4946926.821127157],[-8238820.7236783635,4946909.625045662],[-8238822.997200029,4946908.244795861],[-8238840.290132945,4946910.105072815],[-8238840.5837209085,4946907.368871859],[-8238802.197898111,4946903.240112833],[-8238801.934572822,4946905.693001065],[-8238814.722408795,4946907.068798201],[-8238816.789152132,4946908.543417453],[-8238814.477825506,4946926.294160512],[-8238814.158571713,4946928.7455413425],[-8238813.492626293,4946933.859697671],[-8238812.477636321,4946941.654313061],[-8238811.543805901,4946948.825666723],[-8238810.832095875,4946954.291681496],[-8238810.268719272,4946958.616993905],[-8238809.877145809,4946961.624562537],[-8238807.744728468,4946963.242132722],[-8238754.506591787,4946955.912002316],[-8238752.613761843,4946953.928643568],[-8238753.001840342,4946951.084508137],[-8238753.643516846,4946946.381653807],[-8238754.6792238075,4946938.791856626],[-8238755.435876451,4946933.24765426],[-8238756.4197880775,4946926.037177338],[-8238757.076896179,4946921.221686953],[-8238758.142144492,4946913.4157595625],[-8238759.79761233,4946901.283838191],[-8238761.497763305,4946899.933878517],[-8238773.842584039,4946901.352052147],[-8238774.204748677,4946898.1913536955],[-8238741.277055647,4946894.410989487],[-8238740.984718705,4946896.960118961],[-8238754.050632528,4946898.459863748],[-8238755.316131309,4946900.0545389615],[-8238754.308324143,4946907.5667208405],[-8238753.438324706,4946914.050562129],[-8238752.564404418,4946920.565597889],[-8238751.906739941,4946925.466982265],[-8238750.896747113,4946932.995881938],[-8238750.001925201,4946939.665925449],[-8238749.214949358,4946945.532233603],[-8238748.607554825,4946950.059542178],[-8238748.207353208,4946953.042095908],[-8238746.23047018,4946954.531661825],[-8238706.347052475,4946948.906811217],[-8238712.352659503,4946906.325443602],[-8238708.136643621,4946905.5944176465],[-8238637.488059905,4946893.344814286],[-8238614.122672246,4946889.293467006],[-8238613.893019522,4946885.882339164],[-8238622.245838572,4946824.571939128],[-8238633.098864372,4946748.254357971],[-8238648.877891668,4946750.588011762],[-8238688.083979436,4946755.688085425],[-8238692.953025786,4946756.05965089],[-8238698.594133598,4946715.725574971],[-8238701.191666085,4946712.858615249],[-8238975.66268288,4946750.835245192],[-8238980.47361027,4946751.451407446],[-8238985.837517969,4946748.922722973],[-8238988.180573241,4946745.284710457],[-8238994.64255231,4946695.76457199],[-8238993.433347217,4946691.757950496],[-8238991.444170868,4946689.516414745],[-8238709.276331308,4946652.473082442],[-8238707.471915911,4946651.63365513],[-8238713.229289998,4946606.768199859],[-8238670.993351833,4946601.1937077185],[-8238670.925908823,4946599.196972026],[-8238654.387473932,4946597.240136946],[-8238661.276210289,4946540.164880042],[-8238646.227428995,4946531.572277287],[-8238632.327237173,4946523.769651417],[-8238620.906663848,4946522.448970189],[-8238610.689410875,4946520.484672686],[-8238555.585748666,4946489.133501784],[-8238509.626644558,4946463.275481211],[-8238418.30207549,4946412.969232308],[-8238248.135798354,4946318.570026594],[-8238239.2382367775,4946313.793292453],[-8238212.890595335,4946299.261466853],[-8237988.225932782,4946174.464520628],[-8237923.268410227,4946138.625461595],[-8237607.167316882,4945962.976191878],[-8237290.542276784,4945788.586684842],[-8237235.1530816285,4945888.740495639],[-8237183.025012875,4945982.510290993],[-8237135.509744403,4946067.7520214],[-8237087.723376568,4946154.339326435],[-8237040.375056053,4946239.513894305],[-8236992.299908966,4946325.402203568],[-8236971.141976037,4946364.862913069],[-8236944.739407354,4946412.248290698],[-8236895.33505841,4946501.832128151],[-8236841.19783767,4946600.734034637],[-8236787.58799813,4946697.051478495],[-8236759.162366311,4946748.002140753],[-8236737.405172523,4946787.427905783],[-8236687.039948167,4946877.751710426],[-8237003.632582028,4947053.582869398],[-8237087.316506272,4947100.1787765855],[-8237319.384036853,4947229.391050848],[-8237269.069293101,4947319.794515584],[-8237218.92103125,4947410.0481512],[-8237169.050017556,4947499.756695715],[-8237118.918346748,4947590.434781616],[-8237068.314997247,4947681.426965977],[-8237016.643931222,4947774.682229293],[-8236968.641708371,4947861.314871561],[-8236914.754180735,4947959.073439965],[-8236860.786538117,4948056.176445351],[-8236810.60169881,4948147.00967097],[-8236760.557027974,4948237.35949972],[-8236710.876414252,4948327.488616241],[-8237026.526744695,4948503.457108014],[-8237086.390875116,4948536.812618046],[-8237098.285153911,4948542.530673815],[-8237171.980529738,4948583.33600113],[-8237341.618555154,4948678.860970613],[-8237334.3796667885,4948693.866607422],[-8237299.652602291,4948756.529063086],[-8237291.934830326,4948770.455085721],[-8237286.196113047,4948778.719265948],[-8237267.7610228285,4948811.476918858],[-8237242.616595152,4948859.236616502],[-8237189.414921587,4948955.462445642],[-8237138.542532635,4949047.752544438],[-8237201.193552936,4949082.459176309],[-8237220.609724094,4949093.214908217],[-8237251.189547839,4949110.154455916],[-8237454.423852568,4949222.731988733],[-8237574.771061803,4949289.345964492],[-8237757.631660753,4949390.551223635],[-8237770.396359289,4949398.963627568],[-8237784.580855518,4949406.285723812],[-8237799.978971724,4949415.1763314605],[-8237841.384693299,4949438.303595094]]]},"properties":{"shape_area":29671753.3197,"ntaname":"Chelsea-Hudson Yards","cdtaname":"MN04 Chelsea-Hell's Kitchen (CD 4 Approximation)","shape_leng":40095.236706,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0401","borocode":1,"countyfips":"061","ntaabbrev":"Chls_HdsYd","cdta2020":"MN04"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8236950.731820354,4950991.686313011],[-8236955.597784783,4950995.777684831],[-8236992.578511909,4950930.676160543],[-8237002.2085076105,4950936.429977695],[-8237012.464108445,4950936.180582907],[-8237210.497033189,4951046.091141365],[-8237222.613054647,4951024.651489908],[-8237136.388309597,4950976.871899766],[-8237023.94037067,4950914.559780656],[-8237060.571871915,4950849.022200789],[-8237294.213135755,4950979.039946122],[-8237316.5178370355,4950938.828940482],[-8237083.148092376,4950809.374043989],[-8237083.306473814,4950807.393570352],[-8237086.7403099965,4950801.84256919],[-8237077.463702732,4950788.474354132],[-8237077.245079809,4950784.01760931],[-8237078.487328171,4950781.461043391],[-8237080.971639342,4950778.6855704775],[-8237084.624174724,4950777.443753852],[-8237099.519919071,4950778.912173727],[-8237119.747570536,4950741.6128467815],[-8237155.080976934,4950762.251811183],[-8237155.832757473,4950760.543780452],[-8237169.348248895,4950764.788516741],[-8237166.977034184,4950768.381351638],[-8237177.900339178,4950774.655439293],[-8237173.794723346,4950782.540673455],[-8237180.829563061,4950786.005379437],[-8237185.086021929,4950778.377466012],[-8237185.897770405,4950778.847792735],[-8237197.236382022,4950757.933419041],[-8237189.0298219025,4950753.194743948],[-8237187.33769125,4950756.526318348],[-8237176.6554453,4950750.473229824],[-8237174.525281619,4950754.287062799],[-8237164.601204114,4950745.977420017],[-8237169.932453661,4950736.4387200745],[-8237133.003574476,4950717.542158553],[-8237171.140348874,4950647.695117484],[-8237185.994979134,4950656.264989912],[-8237198.9255228555,4950633.089909947],[-8237183.687244449,4950625.143552645],[-8237231.913820538,4950537.035028093],[-8237504.512796027,4950687.297021428],[-8237533.985727365,4950634.32339186],[-8237260.633600697,4950482.234256824],[-8237296.461637217,4950417.086101675],[-8237309.552848693,4950424.284369116],[-8237325.097646458,4950397.723407929],[-8237311.286620084,4950390.128326402],[-8237316.3485602895,4950380.922645444],[-8237583.601827528,4950531.965713593],[-8237588.386815234,4950532.45635288],[-8237592.802602496,4950530.00260695],[-8237612.9195006415,4950494.183524813],[-8237612.06116307,4950489.399851538],[-8237247.930895177,4950288.118517119],[-8237287.875733139,4950215.174322278],[-8237325.713768741,4950146.0767610725],[-8237685.272985393,4950347.47350201],[-8237688.8117252765,4950348.686513117],[-8237692.957200237,4950347.473465138],[-8237714.621301968,4950309.55708359],[-8237714.695057572,4950305.91671606],[-8237712.26897072,4950303.389214312],[-8237350.448401607,4950101.374580479],[-8237388.660350515,4950031.935352072],[-8237427.111304499,4949962.06110307],[-8237436.175763442,4949966.999016102],[-8237437.132879078,4949965.304769381],[-8237786.356121268,4950162.586908663],[-8237788.670031589,4950164.001477716],[-8237794.197018677,4950163.744359778],[-8237797.539438173,4950160.916228736],[-8237816.178123587,4950126.721097218],[-8237817.463578371,4950123.894232957],[-8237813.993251179,4950119.137247184],[-8237451.028079111,4949917.536068557],[-8237449.836513963,4949915.260674035],[-8237452.095785421,4949910.698376173],[-8237459.565197958,4949895.614428746],[-8237464.432959267,4949885.784388293],[-8237468.471466992,4949880.704597866],[-8237473.420839763,4949877.97009004],[-8237485.273734461,4949874.843005573],[-8237490.224614185,4949872.498947272],[-8237494.00185738,4949869.111951901],[-8237496.346227151,4949864.94429247],[-8237497.12807151,4949859.99496345],[-8237496.086610792,4949854.523366941],[-8237493.741104908,4949849.7044585915],[-8237490.354101313,4949846.968618423],[-8237486.447638988,4949845.145148214],[-8237483.842639342,4949834.2034112],[-8237518.167608527,4949771.359616389],[-8237543.0579671385,4949784.993661486],[-8237553.260044696,4949790.764512788],[-8237867.689831856,4949965.192206577],[-8237871.318218442,4949962.867733068],[-8237897.020337099,4949916.623753739],[-8237895.499940824,4949911.299886988],[-8237547.80632603,4949718.049330933],[-8237579.234753459,4949662.216442836],[-8237580.694700445,4949659.62272906],[-8237588.626080022,4949644.994297493],[-8237600.310576043,4949623.443351046],[-8237602.080664138,4949624.905932334],[-8237634.206147104,4949601.108475973],[-8237723.699098442,4949652.169273317],[-8237760.722810898,4949671.630219868],[-8237986.182400381,4949796.982120737],[-8237987.507254637,4949797.182132621],[-8237988.846936783,4949797.159876903],[-8237990.16441633,4949796.915968738],[-8237991.423276474,4949796.457150101],[-8237992.588720751,4949795.796103318],[-8237993.628534841,4949794.951100608],[-8238001.074242821,4949781.412222201],[-8237994.3048953265,4949777.689420799],[-8237991.2589259725,4949783.104802084],[-8237983.135829008,4949785.474369858],[-8237970.499153948,4949778.307999622],[-8237992.9494825,4949736.735347514],[-8237750.641496847,4949603.120589188],[-8237796.611633743,4949519.642838999],[-8238044.42117179,4949655.3548304215],[-8238060.585032938,4949626.834217913],[-8237812.981574947,4949489.556875917],[-8237841.384693299,4949438.303595094],[-8237799.978971719,4949415.176331466],[-8237784.580855518,4949406.285723817],[-8237770.396359283,4949398.963627572],[-8237757.631660747,4949390.55122364],[-8237574.771061796,4949289.345964492],[-8237454.423852568,4949222.731988738],[-8237251.1895478275,4949110.154455916],[-8237220.609724082,4949093.214908223],[-8237201.193552931,4949082.459176315],[-8237138.5425326405,4949047.752544438],[-8237189.414921593,4948955.462445642],[-8237242.61659514,4948859.236616502],[-8237267.76102281,4948811.476918865],[-8237286.196113047,4948778.719265955],[-8237291.934830332,4948770.455085721],[-8237299.652602291,4948756.529063086],[-8237334.3796667885,4948693.866607427],[-8237341.618555143,4948678.860970613],[-8237171.980529734,4948583.33600113],[-8237098.285153919,4948542.5306738205],[-8237086.39087511,4948536.812618046],[-8237026.526744701,4948503.457108014],[-8236710.876414247,4948327.488616241],[-8236660.189239083,4948418.695307244],[-8236609.982099709,4948509.5386741245],[-8236559.871662975,4948599.88567523],[-8236505.841622717,4948697.319908003],[-8236450.896706844,4948795.917310403],[-8236400.275716693,4948887.687245494],[-8236349.948961393,4948980.018129058],[-8236298.292323408,4949071.677023376],[-8236248.069428303,4949161.64138179],[-8236196.851145497,4949254.25209305],[-8236146.338862462,4949345.425174187],[-8236095.429758329,4949437.634983196],[-8236044.525180429,4949529.127130934],[-8235993.846353019,4949621.1878093425],[-8235942.908259558,4949712.756049831],[-8235892.632357973,4949803.926921391],[-8235841.375991666,4949895.676888601],[-8235790.694599378,4949987.443477707],[-8235764.544507995,4950034.55076938],[-8235736.210039617,4950085.592585633],[-8235679.036906484,4950184.053838596],[-8235872.497241086,4950291.829726318],[-8235996.705224471,4950360.71499038],[-8236313.486306158,4950535.858450708],[-8236576.922953766,4950681.381209501],[-8236595.519949958,4950691.653753979],[-8236603.617016273,4950696.125954079],[-8236630.766823769,4950711.120653876],[-8236936.114668069,4950880.711259098],[-8236950.122693996,4950888.490607191],[-8236909.483700305,4950962.5115120085],[-8236899.854290463,4950980.049449641],[-8236899.283018903,4950981.089768483],[-8236908.773782926,4950988.032813099],[-8236928.856777702,4950975.027773211],[-8236943.8274367675,4950985.880824145],[-8236950.731820354,4950991.686313011]]]},"properties":{"shape_area":18382316.3513,"ntaname":"Hell's Kitchen","cdtaname":"MN04 Chelsea-Hell's Kitchen (CD 4 Approximation)","shape_leng":36275.2815012,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0402","borocode":1,"countyfips":"061","ntaabbrev":"HllsKtchn","cdta2020":"MN04"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8235510.610956008,4947309.203099865],[-8235520.847485467,4947315.014246597],[-8235691.123980269,4947408.902627828],[-8235869.707238456,4947507.065009152],[-8236212.163607352,4947695.938009131],[-8236227.867608606,4947704.598732528],[-8236280.77655564,4947607.318912796],[-8236299.457737647,4947617.263340138],[-8236406.128668531,4947676.6441250555],[-8236598.38672577,4947783.664669367],[-8236717.997827271,4947849.984254859],[-8236773.208123026,4947880.595509912],[-8236914.754180735,4947959.073439965],[-8236968.641708371,4947861.314871561],[-8237016.643931222,4947774.682229293],[-8237068.314997247,4947681.426965977],[-8237118.918346748,4947590.434781616],[-8237169.050017556,4947499.756695715],[-8237218.92103125,4947410.0481512],[-8237269.069293101,4947319.794515584],[-8237319.384036853,4947229.391050848],[-8237087.316506272,4947100.1787765855],[-8237003.632582028,4947053.582869398],[-8236687.039948167,4946877.751710426],[-8236737.405172523,4946787.427905783],[-8236759.162366311,4946748.002140753],[-8236787.58799813,4946697.051478495],[-8236841.19783767,4946600.734034637],[-8236895.33505841,4946501.832128151],[-8236944.739407354,4946412.248290698],[-8236971.141976037,4946364.862913069],[-8236992.299908966,4946325.402203568],[-8237040.375056053,4946239.513894305],[-8237087.723376568,4946154.339326435],[-8237135.509744403,4946067.7520214],[-8237183.025012875,4945982.510290993],[-8237235.1530816285,4945888.740495639],[-8237290.542276784,4945788.586684842],[-8236931.753327332,4945590.080514908],[-8236720.03518394,4945474.58582745],[-8236678.642183224,4945453.184585419],[-8236662.595025361,4945444.887656855],[-8236613.0550486045,4945419.538786436],[-8236518.321317574,4945360.362742308],[-8236525.388008145,4945438.702555219],[-8236525.488041417,4945447.71007343],[-8236524.757635095,4945456.68849463],[-8236523.203002029,4945465.561420035],[-8236520.837408599,4945474.253386657],[-8236517.680938566,4945482.690397715],[-8236513.760448968,4945490.800660203],[-8236515.121230236,4945502.824269618],[-8236468.423494996,4945587.556863358],[-8236441.130027596,4945634.369423642],[-8236419.540019228,4945671.400189904],[-8236372.49535218,4945756.634586114],[-8236326.422409164,4945842.715757242],[-8236277.624620462,4945927.81002851],[-8236228.733494932,4946014.280538338],[-8236199.164720697,4946067.458431109],[-8236178.376660377,4946104.843612],[-8236124.0785288345,4946201.874034681],[-8236070.3907146165,4946299.66944279],[-8236020.135176226,4946390.023307559],[-8235970.049941183,4946480.562556339],[-8235919.80169186,4946571.025883885],[-8235869.539329466,4946661.430329266],[-8235819.484188864,4946751.940975094],[-8235769.2700962825,4946842.276953276],[-8235719.481985951,4946933.039168343],[-8235669.59556711,4947023.369706351],[-8235620.0498882,4947113.976215864],[-8235565.173437763,4947210.884101972],[-8235510.610956008,4947309.203099865]]]},"properties":{"shape_area":14878105.1605,"ntaname":"Midtown South-Flatiron-Union Square","cdtaname":"MN05 Midtown-Flatiron-Union Square (CD 5 Approximation)","shape_leng":18800.765628,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0501","borocode":1,"countyfips":"061","ntaabbrev":"MdtwnSth","cdta2020":"MN05"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8234638.347894098,4949728.82805847],[-8234707.474442331,4949771.43819859],[-8234769.4174922025,4949806.081092683],[-8234996.687041155,4949933.181349686],[-8235233.490513298,4950064.818723825],[-8235313.134936679,4950109.090862897],[-8235581.930025641,4950256.255607518],[-8235586.045502283,4950250.551286662],[-8235590.835045988,4950245.399913984],[-8235596.2250731345,4950240.880630011],[-8235602.132745519,4950237.062898238],[-8235608.467327393,4950234.005332784],[-8235615.131499461,4950231.754906658],[-8235621.173432302,4950230.780057402],[-8235627.287004978,4950230.498086106],[-8235633.3930200245,4950230.912645474],[-8235639.412377108,4950232.018318168],[-8235645.267106101,4950233.800825205],[-8235650.8813631125,4950236.237075868],[-8235679.036906489,4950184.053838588],[-8235736.21003963,4950085.592585633],[-8235764.544507995,4950034.55076938],[-8235790.694599389,4949987.443477707],[-8235841.375991659,4949895.676888596],[-8235892.632357973,4949803.926921385],[-8235942.908259569,4949712.756049831],[-8235993.846353032,4949621.187809334],[-8236044.525180429,4949529.12713093],[-8236095.429758329,4949437.634983189],[-8236146.338862474,4949345.42517418],[-8236196.851145503,4949254.25209305],[-8236248.069428312,4949161.64138179],[-8236298.292323425,4949071.677023376],[-8236349.948961393,4948980.018129052],[-8236400.275716704,4948887.687245494],[-8236450.896706844,4948795.917310396],[-8236505.841622717,4948697.319908003],[-8236559.871662981,4948599.88567523],[-8236609.9820997035,4948509.53867412],[-8236660.189239089,4948418.695307244],[-8236710.876414252,4948327.488616241],[-8236760.557027974,4948237.35949972],[-8236810.60169881,4948147.00967097],[-8236860.786538117,4948056.176445351],[-8236914.754180735,4947959.073439965],[-8236773.208123026,4947880.595509912],[-8236717.997827271,4947849.984254859],[-8236598.38672577,4947783.664669367],[-8236406.128668531,4947676.6441250555],[-8236299.457737647,4947617.263340138],[-8236280.77655564,4947607.318912796],[-8236227.867608606,4947704.598732528],[-8236212.163607352,4947695.938009131],[-8235869.707238456,4947507.065009152],[-8235691.123980269,4947408.902627828],[-8235520.847485467,4947315.014246597],[-8235510.610956008,4947309.203099865],[-8235460.91178154,4947399.391125678],[-8235410.954518521,4947490.052550028],[-8235360.680231481,4947580.4315533405],[-8235310.741020074,4947670.482814711],[-8235260.887628129,4947761.814815303],[-8235255.107542758,4947772.278116335],[-8235210.34068184,4947850.998768313],[-8235200.676540644,4947868.62768665],[-8235157.289688374,4947948.663055159],[-8235170.08209556,4947955.7858102815],[-8235232.114776023,4947991.288631153],[-8235179.3684834475,4948092.039048699],[-8235130.126215809,4948182.60697404],[-8235079.052703321,4948275.062892664],[-8235028.057669965,4948366.641338536],[-8234959.382728948,4948328.574477236],[-8234949.209978831,4948323.165742995],[-8234897.600626658,4948414.280996122],[-8234846.782520267,4948505.642566985],[-8234796.843629165,4948598.237488704],[-8234745.989411449,4948689.506293635],[-8234695.045622291,4948781.541771291],[-8234644.319938412,4948872.75419145],[-8234593.579917441,4948964.664356148],[-8234542.733679764,4949055.714215011],[-8234491.54201917,4949148.154159112],[-8234441.078672227,4949239.472856296],[-8234386.294636928,4949337.897018138],[-8234331.470700842,4949437.459298175],[-8234280.502095224,4949528.927673511],[-8234292.146594115,4949535.447163084],[-8234459.852115225,4949627.600403489],[-8234638.347894098,4949728.82805847]]]},"properties":{"shape_area":24552509.6358,"ntaname":"Midtown-Times Square","cdtaname":"MN05 Midtown-Flatiron-Union Square (CD 5 Approximation)","shape_leng":21258.4010015,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0502","borocode":1,"countyfips":"061","ntaabbrev":"Mdtwn_TmSq","cdta2020":"MN05"}}, +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8233452.917523056,4943864.013301696],[-8233455.640570563,4943861.746149909],[-8233461.538128939,4943862.655940045],[-8233464.715411458,4943859.0278217625],[-8233464.2643806,4943852.676103072],[-8233420.273069379,4943815.909095105],[-8233413.007085052,4943832.23832422],[-8233452.917523056,4943864.013301696]]],[[[-8234797.064820051,4945489.435559733],[-8234822.810890856,4945501.367294181],[-8234831.390799624,4945505.301834747],[-8234839.982453109,4945498.93371483],[-8234847.425798543,4945497.985706436],[-8234854.870760649,4945497.037619376],[-8234867.526485501,4945497.170897652],[-8234883.84299104,4945503.55656273],[-8234958.510005197,4945541.597056338],[-8235011.141465797,4945570.102201406],[-8235095.819830405,4945618.223948952],[-8235193.08148029,4945673.494815921],[-8235230.938785036,4945695.008599553],[-8235253.5076197,4945719.329967844],[-8235257.242606315,4945688.541600576],[-8235294.498575554,4945613.442465827],[-8235343.370243387,4945524.022853725],[-8235356.1695271265,4945500.434097956],[-8235363.388921615,4945487.907197012],[-8235392.444816973,4945437.489828272],[-8235426.003733332,4945376.399117811],[-8235438.7450373955,4945353.123499612],[-8235458.252661919,4945317.861870357],[-8235487.728651086,4945264.439589298],[-8235514.976141397,4945214.354386183],[-8235533.757156138,4945181.331790034],[-8235557.049981313,4945139.37826931],[-8235582.808210669,4945094.578878252],[-8235616.681531037,4945034.024238843],[-8235634.985734614,4945001.502515899],[-8235674.504534524,4944930.577071693],[-8235681.569124212,4944917.897759149],[-8235700.493947386,4944908.955106443],[-8235690.750132664,4944903.796681249],[-8235628.450455762,4944871.638483335],[-8235601.450497694,4944857.505281325],[-8235453.785764376,4944776.130482857],[-8235407.145654072,4944749.752860419],[-8235316.636064478,4944698.295236539],[-8235196.293078301,4944628.421495813],[-8235141.908998881,4944598.158461534],[-8235071.32408456,4944558.879510376],[-8234992.505565431,4944515.588081473],[-8234949.7727671685,4944494.778527799],[-8234933.673325753,4944486.4033401],[-8234669.862065772,4944335.022682427],[-8234530.956018964,4944254.972729442],[-8234522.795808905,4944247.570090602],[-8234515.62599438,4944241.812577461],[-8234509.422156731,4944237.082783737],[-8234502.6658977,4944230.497260151],[-8234483.847654746,4944218.026463136],[-8234468.6435377,4944323.622121725],[-8234468.889441708,4944326.99134688],[-8234471.140532271,4944327.164778605],[-8234472.0250862725,4944328.899880195],[-8234473.817110905,4944329.894814257],[-8234476.519695633,4944330.146958091],[-8234479.291261994,4944330.475434895],[-8234490.323869832,4944331.785590891],[-8234486.5893335845,4944366.512973303],[-8234484.83725002,4944367.209727209],[-8234482.929851604,4944367.587848459],[-8234479.69317395,4944366.0478195455],[-8234477.81589337,4944366.909117547],[-8234477.429488913,4944373.910469868],[-8234475.895359197,4944375.277453705],[-8234470.880341571,4944419.125434119],[-8234466.756368583,4944449.34130201],[-8234464.884378791,4944465.434921716],[-8234464.447312526,4944468.682824692],[-8234461.607423104,4944489.81168654],[-8234460.747965043,4944501.957396732],[-8234459.269118187,4944523.897607225],[-8234459.31553561,4944534.975936063],[-8234459.956183458,4944547.47034417],[-8234460.962621256,4944564.442971909],[-8234462.16777273,4944574.360576809],[-8234463.013451794,4944579.076953671],[-8234464.944423809,4944589.84075309],[-8234467.481082041,4944601.5720275575],[-8234470.621616839,4944612.457542012],[-8234471.808946564,4944616.466412637],[-8234478.306457986,4944633.700502249],[-8234483.884184203,4944646.687270295],[-8234492.009437888,4944662.464587435],[-8234496.980690221,4944672.539024442],[-8234497.345262508,4944673.631538154],[-8234506.440678016,4944688.8028785335],[-8234509.352915394,4944693.172205909],[-8234514.506522183,4944701.243701983],[-8234521.299002017,4944711.438529571],[-8234526.515135252,4944717.993425659],[-8234531.367753952,4944724.063109478],[-8234535.301089275,4944728.702923289],[-8234551.388459629,4944744.407572336],[-8234552.110319645,4944745.11279784],[-8234562.417236273,4944754.781784954],[-8234576.509206643,4944767.916352101],[-8234578.772978801,4944770.034902672],[-8234587.7007510625,4944778.417844551],[-8234596.918544915,4944787.994654118],[-8234608.430261282,4944800.037130944],[-8234618.00810086,4944811.107186969],[-8234627.707966259,4944821.169276246],[-8234637.367019542,4944831.835697974],[-8234642.597787109,4944838.29944593],[-8234647.666491508,4944845.086057899],[-8234652.937072137,4944851.993249943],[-8234658.970726349,4944860.151658127],[-8234662.992145795,4944866.048082915],[-8234668.219367521,4944874.206250706],[-8234671.997901982,4944880.828131882],[-8234674.613088803,4944884.503283314],[-8234677.4253664715,4944889.914536829],[-8234679.195802161,4944892.256494441],[-8234682.69159122,4944899.045319807],[-8234694.7321115155,4944958.713902547],[-8234696.770789298,4944969.567441571],[-8234704.411822542,4945010.301503298],[-8234706.530219694,4945019.777743079],[-8234708.755976587,4945029.735704807],[-8234712.058862216,4945049.452883151],[-8234722.442774284,4945104.800567172],[-8234733.329764853,4945160.005899544],[-8234733.769919671,4945164.288925744],[-8234730.241785384,4945165.54766923],[-8234727.84883355,4945164.917141866],[-8234726.33776434,4945168.064894713],[-8234723.314513285,4945168.567423528],[-8234721.045944615,4945170.834658419],[-8234718.779178748,4945171.3373390185],[-8234717.518590208,4945173.73017655],[-8234716.384865629,4945175.241314203],[-8234713.739853934,4945175.870431364],[-8234713.990588608,4945179.018690278],[-8234711.975124124,4945180.529636961],[-8234713.486182203,4945182.419801091],[-8234715.501273097,4945181.66396796],[-8234717.642546026,4945182.546275824],[-8234719.154477996,4945181.035250276],[-8234721.799150915,4945181.540776079],[-8234723.941180278,4945179.651584809],[-8234726.080718897,4945181.415530909],[-8234723.939973807,4945183.68280768],[-8234726.835464059,4945186.831859953],[-8234729.228833284,4945186.329214504],[-8234731.244311148,4945184.44003192],[-8234730.363307984,4945182.5500828335],[-8234732.126617361,4945182.047150647],[-8234733.006967484,4945186.45556973],[-8234730.739447886,4945189.226245755],[-8234731.620362142,4945191.744808939],[-8234732.753332945,4945193.00494708],[-8234733.130602474,4945195.901568165],[-8234735.900806546,4945195.6507136235],[-8234737.033218576,4945198.295879716],[-8234737.6622292325,4945201.570882851],[-8234740.811081148,4945203.965043274],[-8234741.691137373,4945209.381420971],[-8234743.45344154,4945212.026777396],[-8234747.162600259,4945231.549947521],[-8234750.432682189,4945247.547932736],[-8234754.709162986,4945269.086270103],[-8234759.802583967,4945295.789449054],[-8234764.833252136,4945320.729495522],[-8234766.844949608,4945331.814309144],[-8234768.402646668,4945340.777016967],[-8234771.60302033,4945356.130153304],[-8234776.44679998,4945379.69396683],[-8234778.232875551,4945389.019357578],[-8234783.144081111,4945414.346884851],[-8234787.034986901,4945434.635365044],[-8234789.665409504,4945447.561041373],[-8234791.496764963,4945456.5606577955],[-8234793.793990383,4945469.162841349],[-8234795.452060834,4945477.354235103],[-8234797.064820051,4945489.435559733]]]]},"properties":{"shape_area":5575562.46457,"ntaname":"Stuyvesant Town-Peter Cooper Village","cdtaname":"MN06 East Midtown-Murray Hill (CD 6 Approximation)","shape_leng":11955.5567433,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0601","borocode":1,"countyfips":"061","ntaabbrev":"StyTwn","cdta2020":"MN06"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8235514.976141397,4945214.354386183],[-8235487.728651086,4945264.439589294],[-8235458.252661912,4945317.861870353],[-8235438.745037409,4945353.123499605],[-8235426.00373334,4945376.399117803],[-8235392.444816979,4945437.489828265],[-8235363.388921626,4945487.907197006],[-8235356.169527145,4945500.434097951],[-8235343.3702433985,4945524.022853718],[-8235294.49857556,4945613.442465821],[-8235257.242606315,4945688.541600569],[-8235253.507619706,4945719.329967844],[-8235199.107917229,4945816.841706035],[-8235295.230054335,4945870.070246381],[-8235359.225575,4945905.50664186],[-8235463.082085398,4945963.0659918375],[-8235490.0680474285,4945977.926562394],[-8235500.5376740275,4945983.769131889],[-8235617.216296713,4946048.4164942475],[-8235659.770624155,4946071.993884881],[-8235712.884527364,4946101.42154502],[-8235662.094537772,4946191.59021034],[-8235612.018914324,4946282.122985028],[-8235561.95812605,4946372.54441769],[-8235511.625047631,4946463.117998771],[-8235689.241313235,4946562.2322230805],[-8235862.155245635,4946657.66872919],[-8235869.539329466,4946661.430329266],[-8235919.80169186,4946571.025883885],[-8235970.049941183,4946480.562556339],[-8236020.135176226,4946390.023307559],[-8236070.3907146165,4946299.66944279],[-8236124.0785288345,4946201.874034681],[-8236178.376660377,4946104.843612],[-8236199.164720697,4946067.458431109],[-8236228.733494932,4946014.280538338],[-8236277.624620462,4945927.81002851],[-8236326.422409164,4945842.715757242],[-8236372.49535218,4945756.634586114],[-8236419.540019228,4945671.400189904],[-8236441.130027596,4945634.369423642],[-8236468.423494996,4945587.556863358],[-8236515.121230236,4945502.824269618],[-8236513.760448968,4945490.800660203],[-8236517.680938566,4945482.690397715],[-8236520.837408599,4945474.253386657],[-8236523.203002029,4945465.561420035],[-8236524.757635095,4945456.68849463],[-8236525.488041417,4945447.71007343],[-8236525.388008145,4945438.702555219],[-8236518.321317574,4945360.362742308],[-8236399.9208540395,4945295.341112841],[-8236393.23786853,4945291.670897633],[-8236258.834658352,4945217.269535306],[-8236215.258025192,4945193.146292796],[-8235966.459997946,4945055.224848887],[-8235812.284196692,4944970.429676628],[-8235801.81135579,4944964.701480704],[-8235773.784476978,4944949.05627336],[-8235745.924976324,4944933.935915191],[-8235700.493947386,4944908.955106436],[-8235681.569124217,4944917.897759149],[-8235674.504534524,4944930.577071693],[-8235634.985734614,4945001.502515893],[-8235616.68153105,4945034.024238834],[-8235582.80821068,4945094.578878243],[-8235557.049981326,4945139.37826931],[-8235533.757156144,4945181.331790034],[-8235514.976141397,4945214.354386183]]]},"properties":{"shape_area":7526916.69977,"ntaname":"Gramercy","cdtaname":"MN06 East Midtown-Murray Hill (CD 6 Approximation)","shape_leng":12096.8909253,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0602","borocode":1,"countyfips":"061","ntaabbrev":"Grmrcy","cdta2020":"MN06"}}, +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8233332.529686956,4944734.659456914],[-8233340.131050962,4944733.619734268],[-8233379.588399163,4944729.118181483],[-8233405.955212444,4944709.465540751],[-8233434.616628227,4944704.666521851],[-8233433.844725103,4944707.867490851],[-8233440.262709059,4944709.271225468],[-8233441.939727998,4944701.32266263],[-8233462.447059985,4944686.269657604],[-8233465.375091791,4944687.526062799],[-8233473.330248106,4944673.932003481],[-8233466.219856929,4944669.117550018],[-8233458.892473509,4944682.293499181],[-8233436.083310839,4944699.228201443],[-8233409.723396079,4944703.191411913],[-8233414.956883281,4944694.616801425],[-8233407.636720933,4944690.848171397],[-8233399.472446924,4944704.442109749],[-8233402.191299209,4944705.907629234],[-8233377.302389296,4944723.363470183],[-8233345.577735635,4944727.630088888],[-8233345.912225598,4944720.317314099],[-8233338.912287967,4944720.701486828],[-8233332.319537841,4944720.313325914],[-8233332.529686956,4944734.659456914]]],[[[-8233408.882293629,4944942.170059517],[-8233514.0414845785,4944935.799788601],[-8233513.205816568,4944933.289069967],[-8233556.162201233,4944930.238882238],[-8233554.773558602,4944915.734047392],[-8233586.573011266,4944912.1212286325],[-8233588.520332513,4944924.67384158],[-8233601.352323168,4944921.610912181],[-8233596.900761174,4944894.274112102],[-8233584.90599707,4944896.22159125],[-8233585.738486961,4944906.8212551735],[-8233554.775429361,4944911.271148905],[-8233554.776832435,4944907.92396413],[-8233545.571833906,4944909.0358377285],[-8233545.846642276,4944918.798481389],[-8233512.653429865,4944920.458013682],[-8233512.373166977,4944923.247264585],[-8233411.955805287,4944929.898477751],[-8233411.120769204,4944925.993127948],[-8233375.9398914,4944928.142570312],[-8233378.613174721,4944935.61852039],[-8233380.217501138,4944940.560744311],[-8233408.883867863,4944938.543978239],[-8233408.882293629,4944942.170059517]]],[[[-8233247.127466427,4946775.685161712],[-8233261.7450118875,4946779.340695045],[-8233264.35093965,4946767.7630756255],[-8233252.831688707,4946764.609028705],[-8233247.127466427,4946775.685161712]]],[[[-8234181.16994029,4947654.952259107],[-8234186.534282499,4947657.704121735],[-8234443.257450972,4947802.765266371],[-8234692.636872423,4947940.757548198],[-8234743.464163144,4947849.428683159],[-8234797.495117815,4947750.427607758],[-8234976.021862381,4947849.483151188],[-8235028.077374202,4947878.03890751],[-8235065.442047188,4947898.535514902],[-8235093.97184893,4947914.185561264],[-8235142.400113572,4947940.556262545],[-8235157.289688363,4947948.663055166],[-8235200.676540631,4947868.62768665],[-8235210.340681834,4947850.998768313],[-8235255.107542752,4947772.278116341],[-8235260.887628122,4947761.814815303],[-8235310.741020081,4947670.482814717],[-8235360.68023147,4947580.4315533405],[-8235410.954518504,4947490.052550028],[-8235460.911781546,4947399.391125678],[-8235510.610956008,4947309.203099865],[-8235565.173437763,4947210.884101972],[-8235620.049888189,4947113.976215869],[-8235669.595567104,4947023.369706351],[-8235719.481985946,4946933.039168343],[-8235769.27009629,4946842.276953276],[-8235819.48418886,4946751.940975094],[-8235869.539329461,4946661.430329273],[-8235862.155245635,4946657.668729194],[-8235689.241313223,4946562.232223083],[-8235511.625047619,4946463.117998771],[-8235561.958126056,4946372.54441769],[-8235612.018914324,4946282.122985033],[-8235662.094537754,4946191.59021034],[-8235712.884527357,4946101.42154502],[-8235659.770624155,4946071.993884886],[-8235617.216296702,4946048.416494256],[-8235500.537674014,4945983.769131889],[-8235490.068047422,4945977.926562402],[-8235463.082085385,4945963.0659918375],[-8235359.225574994,4945905.50664186],[-8235295.230054338,4945870.070246381],[-8235199.107917223,4945816.841706035],[-8235253.5076197,4945719.329967844],[-8235230.938785036,4945695.008599553],[-8235193.08148029,4945673.494815921],[-8235095.819830405,4945618.223948952],[-8235011.141465797,4945570.102201406],[-8234958.510005197,4945541.597056338],[-8234883.84299104,4945503.55656273],[-8234867.526485501,4945497.170897652],[-8234854.870760649,4945497.037619376],[-8234847.425798543,4945497.985706436],[-8234839.982453109,4945498.93371483],[-8234831.390799624,4945505.301834747],[-8234822.810890856,4945501.367294181],[-8234797.064820051,4945489.435559733],[-8234697.331289742,4945434.93019607],[-8234694.880077389,4945439.471782671],[-8234687.839209467,4945438.449714498],[-8234683.247651527,4945438.856743419],[-8234677.73699645,4945441.508005664],[-8234673.348894476,4945444.669429276],[-8234669.67340686,4945449.056726202],[-8234667.5288976235,4945454.667451773],[-8234667.017153667,4945460.482901715],[-8234667.730143914,4945464.667417726],[-8234669.974387531,4945468.95363148],[-8234671.912095538,4945471.811291632],[-8234672.523365348,4945472.627293261],[-8234669.971463942,4945478.544955337],[-8234640.794255391,4945461.801949136],[-8234637.016612388,4945468.330736744],[-8234633.4458952835,4945466.085433294],[-8234636.202908452,4945461.800534801],[-8234630.591064944,4945458.8389628185],[-8234633.040933091,4945454.555420017],[-8234630.592987286,4945452.614863002],[-8234618.598517865,4945472.304974184],[-8234621.556395406,4945474.141721044],[-8234623.904948095,4945469.4494502675],[-8234629.106706337,4945473.123944374],[-8234632.679681653,4945467.819428901],[-8234636.353151772,4945470.371859984],[-8234610.319060469,4945516.994585766],[-8234628.935626822,4945531.998952428],[-8234629.650276994,4945530.774756176],[-8234615.165165377,4945519.750425451],[-8234643.036228088,4945469.046857401],[-8234667.895628047,4945483.083090467],[-8234685.541449201,4945493.012952852],[-8234683.301456926,4945496.894025306],[-8234758.039595415,4945537.419576368],[-8234758.35122685,4945536.844553558],[-8234764.282907577,4945541.366051728],[-8234765.541901085,4945539.349548411],[-8234769.113664499,4945539.652599437],[-8234769.422807918,4945542.610839209],[-8234777.483553548,4945541.988841159],[-8234777.373441711,4945541.344580681],[-8234791.633033108,4945541.413925421],[-8234791.675807632,4945540.768018173],[-8234799.817547282,4945540.673112212],[-8234800.504582587,4945549.503215057],[-8234801.1953083705,4945558.377334669],[-8234800.2898273,4945558.37834569],[-8234800.37719544,4945559.842419386],[-8234776.806704393,4945555.2172830785],[-8234775.430665004,4945557.243048608],[-8234775.822298206,4945559.741406123],[-8234799.478655218,4945564.711695883],[-8234798.192227557,4945570.270717506],[-8234802.396053666,4945571.229253319],[-8234804.555494944,4945559.706758643],[-8234805.2966984445,4945573.997931318],[-8234804.215170912,4945595.610699168],[-8234801.254031462,4945595.889477136],[-8234797.914925153,4945594.953868853],[-8234797.796619169,4945594.321738595],[-8234764.851947621,4945579.73387043],[-8234763.665708881,4945582.933589514],[-8234796.294410723,4945597.284295387],[-8234797.084828221,4945596.810466377],[-8234800.857257885,4945598.552294157],[-8234804.090263031,4945598.875388421],[-8234799.939197123,4945630.174382678],[-8234795.629381935,4945628.380489508],[-8234795.8040338,4945627.565986892],[-8234753.220863875,4945604.047428599],[-8234751.008778422,4945607.828711433],[-8234792.486686745,4945630.997843024],[-8234793.824750696,4945631.1727952445],[-8234794.406960735,4945630.06744338],[-8234799.191155939,4945632.451968498],[-8234794.082421843,4945650.068410251],[-8234622.153210802,4945555.889468509],[-8234582.164586437,4945558.270311177],[-8234579.913025874,4945560.633837865],[-8234588.083330544,4945654.6765690725],[-8234587.2917673,4945656.96016195],[-8234589.410466721,4945684.037936849],[-8234589.915776508,4945690.503543194],[-8234590.263825356,4945698.405689855],[-8234591.094011148,4945702.060042982],[-8234592.856167421,4945833.280232809],[-8234593.727831331,4945852.598310989],[-8234592.673727119,4945856.110175602],[-8234593.547973376,4945867.174217186],[-8234594.249610048,4945869.633603675],[-8234592.843700597,4945873.145364464],[-8234593.805523086,4945886.492072407],[-8234594.331895824,4945888.60090422],[-8234594.674595178,4945913.8889460135],[-8234595.868373479,4945914.8790268],[-8234595.80997168,4945916.136327646],[-8234594.89488581,4945917.737295972],[-8234595.289758562,4945935.973274717],[-8234596.431899371,4945937.688259381],[-8234596.489194264,4945939.974832324],[-8234595.230340447,4945940.717745454],[-8234594.312737986,4945949.891463598],[-8234594.485405825,4945958.320899341],[-8234595.272906005,4945969.034423908],[-8234595.092962018,4945979.571128978],[-8234595.267638035,4945985.893177158],[-8234595.616670409,4945990.459897667],[-8234595.7897238275,4945997.661043554],[-8234597.368652303,4946005.915631752],[-8234597.18819359,4946022.42385998],[-8234597.097290634,4946031.028555515],[-8234597.797843388,4946037.000266614],[-8234598.3194745295,4946054.563093791],[-8234598.55862681,4946061.073754932],[-8234574.229950682,4946178.010407953],[-8234562.266155889,4946176.423728177],[-8234559.915813268,4946177.536684988],[-8234558.746087912,4946179.94424354],[-8234553.022276561,4946217.441943363],[-8234553.572768732,4946218.749444559],[-8234555.430707705,4946219.988904202],[-8234555.154978217,4946220.745409198],[-8234549.513061739,4946219.849970619],[-8234547.237942352,4946233.747037735],[-8234551.606746117,4946234.162173905],[-8234551.537846221,4946235.331294581],[-8234550.437227278,4946235.81325009],[-8234549.334954518,4946237.25736209],[-8234543.796069507,4946273.202790245],[-8234543.715482935,4946273.723320022],[-8234544.128499621,4946276.269737636],[-8234545.709365676,4946278.195738899],[-8234547.155151267,4946278.678651563],[-8234549.064641278,4946278.983681158],[-8234557.646817257,4946280.355840622],[-8234557.230648561,4946282.878164935],[-8234547.419504322,4946342.357237511],[-8234542.364290855,4946378.056790759],[-8234536.687451561,4946412.394685138],[-8234533.109311564,4946436.646907803],[-8234530.856910724,4946451.114233634],[-8234520.871039809,4946517.000934193],[-8234500.327463928,4946557.687834215],[-8234496.49636779,4946564.66456659],[-8234498.947190192,4946565.995288139],[-8234513.004154629,4946573.627792157],[-8234489.663004151,4946621.87506566],[-8234485.044328981,4946631.057460992],[-8234469.068975824,4946662.88737789],[-8234453.642475229,4946692.88821051],[-8234445.33628516,4946709.32375772],[-8234436.925507961,4946726.824209993],[-8234440.12892618,4946728.746829318],[-8234453.853462058,4946736.622550699],[-8234444.513222953,4946755.10732487],[-8234437.00365537,4946769.931317568],[-8234434.306465492,4946775.376497005],[-8234433.363786518,4946776.006201391],[-8234409.934863436,4946763.731748252],[-8234409.561719769,4946764.02802086],[-8234399.980293717,4946758.462923272],[-8234395.792048369,4946755.832583614],[-8234395.78305109,4946755.827995406],[-8234391.670447325,4946753.358064224],[-8234390.909807256,4946753.828322106],[-8234387.474298538,4946751.793523975],[-8234386.291926672,4946752.748692342],[-8234385.396795557,4946752.703473041],[-8234383.192895407,4946755.805086923],[-8234381.011096653,4946758.875691059],[-8234381.301851074,4946760.641880949],[-8234382.687526268,4946762.744531161],[-8234386.318168153,4946764.629238668],[-8234387.15194952,4946766.002973795],[-8234389.293472083,4946767.091096385],[-8234389.870566298,4946767.182159588],[-8234426.786257042,4946790.178097934],[-8234415.603477447,4946812.75546143],[-8234412.075001726,4946819.930197287],[-8234407.444387005,4946829.269740314],[-8234406.103067241,4946828.086914263],[-8234404.841609335,4946828.086495535],[-8234403.9746761015,4946827.060804111],[-8234402.634391241,4946826.824378069],[-8234401.767556412,4946825.798711139],[-8234400.348073862,4946825.483181372],[-8234398.139718355,4946823.589794656],[-8234390.492450365,4946819.722866967],[-8234372.176758442,4946856.344768134],[-8234366.049420246,4946867.386478214],[-8234359.017149333,4946880.837366915],[-8234355.321947577,4946887.0546363285],[-8234354.011785207,4946886.835327394],[-8234351.99896345,4946887.659752822],[-8234344.826038076,4946899.651486878],[-8234338.992886311,4946910.036970488],[-8234338.992207617,4946912.04044633],[-8234340.505974355,4946913.896823555],[-8234326.781536155,4946935.990460641],[-8234317.163853129,4946951.327841155],[-8234312.889851183,4946958.026056187],[-8234308.031567131,4946965.597956234],[-8234299.8140974445,4946979.193472636],[-8234294.6131759975,4946975.10170758],[-8234295.097032734,4946974.40058994],[-8234292.969005742,4946972.754981276],[-8234292.242411755,4946973.698424043],[-8234288.887395671,4946971.734829252],[-8234268.980624115,4947002.795706107],[-8234268.7679278,4947004.229614865],[-8234261.773059306,4947015.4279845115],[-8234260.845103499,4947015.927341783],[-8234257.917042167,4947021.206452252],[-8234252.706619282,4947029.409459513],[-8234249.351969199,4947033.831751627],[-8234242.855266991,4947044.8879239755],[-8234239.607355634,4947049.631820089],[-8234237.894085861,4947052.5559904985],[-8234236.038744653,4947055.053158951],[-8234235.9671392245,4947056.050828656],[-8234234.039438454,4947058.261841218],[-8234229.17902286,4947066.561993074],[-8234228.403757617,4947067.88169242],[-8234227.695109815,4947068.235154602],[-8234219.276511315,4947081.713442507],[-8234218.918771019,4947083.41279631],[-8234217.499062568,4947085.53551949],[-8234214.942887606,4947088.789424615],[-8234205.993020454,4947102.870237801],[-8234199.421996034,4947113.4480333235],[-8234193.597591903,4947122.150578808],[-8234193.24207989,4947123.425926098],[-8234191.751574308,4947124.839316355],[-8234191.393999819,4947126.467885197],[-8234190.189117985,4947127.032393261],[-8234188.48327791,4947130.430420801],[-8234187.7081469605,4947130.649018909],[-8234185.070511547,4947128.868487032],[-8234163.886558344,4947161.71095778],[-8234159.217044181,4947168.377942885],[-8234158.528583598,4947170.195700958],[-8234157.156126457,4947171.355711328],[-8234154.727392111,4947175.366561918],[-8234153.988648478,4947176.948804087],[-8234151.242027071,4947181.699167203],[-8234148.81349477,4947184.653021027],[-8234147.968714029,4947186.870294163],[-8234146.912820539,4947187.819979745],[-8234145.962186837,4947189.613861255],[-8234145.645728975,4947191.091699163],[-8234140.683820362,4947197.634293835],[-8234139.627548236,4947199.428065039],[-8234137.408899818,4947204.177325233],[-8234135.826026648,4947205.866522077],[-8234133.1854249155,4947209.981862865],[-8234131.706821267,4947213.14760556],[-8234128.117288741,4947218.318628682],[-8234124.632048457,4947223.807164727],[-8234120.302310973,4947231.2999937115],[-8234118.719065633,4947234.677344858],[-8234116.396765298,4947236.260468201],[-8234115.287224263,4947239.479217811],[-8234113.703470909,4947239.373008047],[-8234113.280952908,4947240.850740505],[-8234113.070182477,4947242.644798224],[-8234105.889632889,4947253.304337767],[-8234120.748122538,4947262.821164389],[-8234106.338218794,4947285.907529785],[-8234099.4379840745,4947296.963819752],[-8234093.334668377,4947305.610925339],[-8234070.3800895745,4947341.487829986],[-8234077.484038328,4947346.462302733],[-8234085.257765775,4947351.450136105],[-8234093.605983799,4947356.920804543],[-8234101.898014,4947360.95707018],[-8234112.973044066,4947374.515214487],[-8234131.484655633,4947388.538544432],[-8234265.59658748,4947461.271854316],[-8234274.859649427,4947466.767635354],[-8234285.480999449,4947462.936946304],[-8234291.574728181,4947466.77998965],[-8234301.277931787,4947472.899412223],[-8234296.048225122,4947478.676199625],[-8234262.931063283,4947515.254525262],[-8234252.039732008,4947522.130084342],[-8234227.071645478,4947567.789137772],[-8234181.16994029,4947654.952259107]]]]},"properties":{"shape_area":15771152.1046,"ntaname":"Murray Hill-Kips Bay","cdtaname":"MN06 East Midtown-Murray Hill (CD 6 Approximation)","shape_leng":25692.7420589,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0603","borocode":1,"countyfips":"061","ntaabbrev":"MryHl_KpBy","cdta2020":"MN06"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8233034.476370143,4948896.523802843],[-8233046.947813803,4948903.312759962],[-8233051.135301699,4948895.768925504],[-8233070.926498501,4948858.650535104],[-8233082.317259539,4948865.76939056],[-8233158.570307019,4948906.655180918],[-8233408.363813976,4949046.669718891],[-8233532.4848804325,4949114.5556052895],[-8233585.412156802,4949143.492347328],[-8233669.719589228,4949190.108212819],[-8233922.397951722,4949329.439832317],[-8234100.898066537,4949428.392257539],[-8234268.003422264,4949521.966003718],[-8234280.502095224,4949528.927673511],[-8234331.470700835,4949437.459298175],[-8234386.2946369145,4949337.897018136],[-8234441.078672233,4949239.472856296],[-8234491.542019157,4949148.154159112],[-8234542.733679757,4949055.714215011],[-8234593.579917428,4948964.664356148],[-8234644.319938405,4948872.75419145],[-8234695.045622291,4948781.5417712955],[-8234745.989411449,4948689.506293635],[-8234796.843629159,4948598.237488708],[-8234846.782520274,4948505.642566985],[-8234897.600626652,4948414.280996122],[-8234949.209978831,4948323.165743002],[-8234959.382728954,4948328.574477243],[-8235028.057669959,4948366.641338543],[-8235079.052703321,4948275.06289267],[-8235130.126215798,4948182.60697404],[-8235179.3684834475,4948092.039048703],[-8235232.114776023,4947991.288631153],[-8235170.08209556,4947955.7858102815],[-8235157.289688363,4947948.663055166],[-8235142.400113572,4947940.556262545],[-8235093.97184893,4947914.185561264],[-8235065.442047188,4947898.535514902],[-8235028.077374202,4947878.03890751],[-8234976.021862381,4947849.483151188],[-8234797.495117815,4947750.427607758],[-8234743.464163144,4947849.428683159],[-8234692.636872423,4947940.757548198],[-8234443.257450972,4947802.765266371],[-8234186.534282499,4947657.704121735],[-8234181.16994029,4947654.952259107],[-8234127.607470134,4947750.277607862],[-8234077.180130844,4947838.406845848],[-8234055.27684368,4947876.5016592415],[-8234057.499729445,4947889.834411438],[-8234046.515010666,4947937.225428961],[-8234042.645383022,4947944.196531209],[-8234010.661407679,4948001.815056391],[-8233993.994858725,4948031.996735615],[-8233980.393952695,4948024.408175571],[-8233976.271535971,4948022.10889064],[-8233970.840412413,4948019.191008676],[-8233955.591983082,4948010.966338595],[-8233792.365885613,4947923.927620927],[-8233782.292586588,4947922.058655527],[-8233780.944074787,4947922.0053240545],[-8233772.472605229,4947922.986972849],[-8233762.925237079,4947920.105772973],[-8233771.7963567795,4947910.341435149],[-8233770.111456555,4947903.214870639],[-8233752.313146689,4947903.818010748],[-8233730.802127775,4947932.877879816],[-8233726.280284967,4947916.5287007205],[-8233720.3202739395,4947900.724256218],[-8233713.801301538,4947890.180121364],[-8233605.397833877,4948045.984057137],[-8233541.391019642,4948137.976712192],[-8233508.911093124,4948186.3281403445],[-8233512.064143455,4948189.696179858],[-8233488.657933663,4948224.661447524],[-8233437.920868669,4948300.454419237],[-8233433.7538159285,4948297.846483635],[-8233427.271926555,4948307.934913243],[-8233370.909172734,4948392.716260949],[-8233303.965340431,4948459.792019335],[-8233278.286292505,4948488.202174453],[-8233278.271741975,4948488.2192858495],[-8233262.607158783,4948505.548545558],[-8233230.120747635,4948556.238187031],[-8233193.881633397,4948622.984056124],[-8233152.136055684,4948694.140178476],[-8233151.435311125,4948701.274373688],[-8233098.371992026,4948788.520462091],[-8233097.438053604,4948789.7896111915],[-8233086.015318392,4948800.669353919],[-8233053.496436663,4948849.246694605],[-8233027.379610998,4948882.142312046],[-8233021.579663474,4948889.503311055],[-8233034.476370143,4948896.523802843]]]},"properties":{"shape_area":13138095.4617,"ntaname":"East Midtown-Turtle Bay","cdtaname":"MN06 East Midtown-Murray Hill (CD 6 Approximation)","shape_leng":16687.5847462,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0604","borocode":1,"countyfips":"061","ntaabbrev":"EstMdtwn","cdta2020":"MN06"}}, +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8233658.450602073,4947141.453715276],[-8233658.408375311,4947150.581188974],[-8233649.714847096,4947171.968741725],[-8233648.748095954,4947176.793767258],[-8233652.606209401,4947179.689967914],[-8233658.880540855,4947175.511593389],[-8233665.158499214,4947162.968625974],[-8233672.396941624,4947160.559456476],[-8233676.499250227,4947156.121866975],[-8233687.742391335,4947146.798454499],[-8233695.506408071,4947135.574395032],[-8233700.164147687,4947128.511834807],[-8233699.736711485,4947119.510424679],[-8233684.379437123,4947113.073656842],[-8233676.857142285,4947125.465827616],[-8233672.364343616,4947132.877691521],[-8233665.279336766,4947132.309477274],[-8233658.450602073,4947141.453715276]]],[[[-8233713.801301538,4947890.180121364],[-8233720.3202739395,4947900.724256218],[-8233726.280284967,4947916.5287007205],[-8233730.802127775,4947932.877879816],[-8233752.313146689,4947903.818010748],[-8233770.111456555,4947903.214870639],[-8233771.7963567795,4947910.341435149],[-8233762.925237079,4947920.105772973],[-8233772.472605229,4947922.986972849],[-8233780.944074787,4947922.0053240545],[-8233782.292586588,4947922.058655527],[-8233792.365885613,4947923.927620927],[-8233955.591983082,4948010.966338595],[-8233970.840412413,4948019.191008676],[-8233976.271535971,4948022.10889064],[-8233980.393952695,4948024.408175571],[-8233993.994858725,4948031.996735615],[-8234010.661407679,4948001.815056391],[-8234042.645383022,4947944.196531209],[-8234046.515010666,4947937.225428961],[-8234057.499729445,4947889.834411438],[-8234055.27684368,4947876.5016592415],[-8234077.180130844,4947838.406845848],[-8234127.607470134,4947750.277607862],[-8234181.16994029,4947654.952259107],[-8234227.071645478,4947567.789137772],[-8234252.039732008,4947522.130084342],[-8234262.931063283,4947515.254525262],[-8234296.048225122,4947478.676199625],[-8234301.277931787,4947472.899412223],[-8234291.574728181,4947466.77998965],[-8234285.480999449,4947462.936946304],[-8234274.859649427,4947466.767635354],[-8234265.59658748,4947461.271854316],[-8234131.484655633,4947388.538544432],[-8234112.973044066,4947374.515214487],[-8234101.898014,4947360.95707018],[-8234093.605983799,4947356.920804543],[-8234085.257765775,4947351.450136105],[-8234077.484038328,4947346.462302733],[-8234070.380040519,4947341.4878790025],[-8233782.210258323,4947791.85665468],[-8233739.952454554,4947852.59354994],[-8233713.801301538,4947890.180121364]]]]},"properties":{"shape_area":1091249.96594,"ntaname":"United Nations","cdtaname":"MN06 East Midtown-Murray Hill (CD 6 Approximation)","shape_leng":5390.773361,"boroname":"Manhattan","ntatype":6,"nta2020":"MN0661","borocode":1,"countyfips":"061","ntaabbrev":"UN","cdta2020":"MN06"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8235021.609919899,4951376.609313475],[-8234965.6607988,4951477.242077486],[-8234910.463110137,4951576.667765811],[-8234859.455922998,4951668.785706151],[-8235174.701300711,4951844.944739596],[-8235333.704435962,4951932.904441182],[-8235354.49813746,4951944.406413244],[-8235490.9195716595,4952019.864751579],[-8235572.832228014,4952065.297105473],[-8235585.035751043,4952072.08317268],[-8235597.142874141,4952078.992169751],[-8235624.025843338,4952093.924521824],[-8235676.633472486,4952123.145181532],[-8235808.442945871,4952196.306378416],[-8235980.523818472,4952291.4488836685],[-8236013.661725411,4952188.7440422075],[-8236048.15238242,4952076.604803803],[-8236138.229033396,4952123.94650121],[-8236144.3801297825,4952127.179301784],[-8236203.110638725,4952163.55660317],[-8236229.421433044,4952183.315426503],[-8236235.645424676,4952186.464295112],[-8236241.28783475,4952190.082477184],[-8236246.616933255,4952193.172184667],[-8236305.775783518,4952227.471299828],[-8236320.673672387,4952236.108494164],[-8236321.833738333,4952232.811170829],[-8236322.534934042,4952230.818030827],[-8236329.908752557,4952210.287785427],[-8236332.357003818,4952211.409814405],[-8236348.903127249,4952174.494966741],[-8236408.816531284,4952063.931515979],[-8236407.970414693,4952048.741021647],[-8236407.972595538,4952044.8853404],[-8236409.578186188,4952039.504220517],[-8236411.086918787,4952024.403828562],[-8236405.129924582,4952013.086644388],[-8236687.17808601,4951994.983513003],[-8236701.78691771,4951968.556338687],[-8236680.4576960495,4951970.176280053],[-8236676.28455993,4951976.90003899],[-8236673.269969138,4951976.667657783],[-8236668.8651871085,4951979.680484017],[-8236667.473594917,4951982.694960198],[-8236657.968256551,4951982.461898546],[-8236646.841131218,4951979.910342677],[-8236636.176656631,4951978.055142451],[-8236626.207112119,4951978.981161498],[-8236612.064552255,4951982.689263476],[-8236598.386714336,4951983.382779775],[-8236585.6347058015,4951982.222246278],[-8236577.057822005,4951981.7584489165],[-8236566.509369069,4951981.293101924],[-8236553.989057135,4951984.304978974],[-8236544.483382122,4951986.854340858],[-8236533.587941964,4951988.012268652],[-8236522.923551543,4951986.389025349],[-8236511.562900442,4951984.0694186],[-8236501.594811289,4951982.676966247],[-8236493.48072966,4951983.603066919],[-8236484.2056437535,4951986.616586188],[-8236473.309863284,4951990.555657073],[-8236463.340006552,4951992.409938674],[-8236448.735357227,4951990.320597632],[-8236442.185819054,4951986.57983304],[-8236403.541438084,4951988.899871767],[-8236403.178859377,4951985.851671939],[-8236427.288143701,4951944.067640473],[-8236437.371869735,4951926.483719112],[-8236440.143999033,4951921.396707171],[-8236440.1749419635,4951921.339941968],[-8236490.036507475,4951829.83738529],[-8236540.396671256,4951737.418418965],[-8236573.224127842,4951677.174467767],[-8236610.433041496,4951673.584381571],[-8236632.336348536,4951674.51119636],[-8236688.258557736,4951573.861559728],[-8236753.85334599,4951455.801686256],[-8236761.523793459,4951442.714764402],[-8236794.736881668,4951380.217626275],[-8236792.033358785,4951372.122787783],[-8236788.425472683,4951367.290632919],[-8236777.017583524,4951369.239682047],[-8236765.719167049,4951373.17349564],[-8236758.340081022,4951371.492889939],[-8236781.494991308,4951315.6189641375],[-8236796.7177264495,4951301.284101071],[-8236804.054640852,4951302.165506051],[-8236810.510963849,4951298.93678458],[-8236816.087938372,4951292.1870855205],[-8236821.077400337,4951294.8286908455],[-8236903.039452522,4951141.488710129],[-8236908.918814544,4951123.292108702],[-8236912.443463123,4951118.597146403],[-8236921.839770938,4951110.3819336025],[-8236932.411512687,4951098.351776614],[-8236940.929019435,4951088.081385223],[-8236948.566061263,4951076.6367713595],[-8236954.736618795,4951061.667688011],[-8236959.145787661,4951045.232696122],[-8236967.38215322,4951032.05149032],[-8237149.425514614,4951133.2668078365],[-8237202.746169471,4951162.284192235],[-8237207.643315553,4951164.816152403],[-8237217.017984773,4951150.285596307],[-8237210.215515201,4951146.386085206],[-8237208.039925154,4951145.138592319],[-8237211.107152422,4951140.000095421],[-8237211.345757466,4951139.600620511],[-8237211.175664412,4951139.508254215],[-8237204.072577761,4951135.632500264],[-8237209.0389129715,4951125.454279141],[-8237209.115357996,4951125.298818292],[-8237218.841119435,4951130.576114452],[-8237223.829887781,4951121.414891492],[-8237091.318915381,4951049.277824427],[-8237087.275583996,4951058.192628611],[-8237095.672259189,4951062.650009467],[-8237093.634724855,4951066.407948469],[-8237088.312305244,4951076.227242572],[-8237007.132921868,4951028.644614493],[-8237009.87186141,4951023.548327637],[-8237016.31931984,4951011.557486258],[-8237005.742105294,4951004.858267786],[-8237007.946867285,4951001.377405091],[-8236989.61385404,4950991.396981901],[-8236987.989201056,4950994.645890702],[-8236975.806972633,4950988.611295661],[-8236970.236796018,4950997.66164331],[-8236962.295862432,4950999.205215971],[-8236961.883179685,4950999.285745447],[-8236955.597747995,4950995.777758415],[-8236950.731820354,4950991.686313011],[-8236943.8274367675,4950985.880824145],[-8236928.856777702,4950975.027773211],[-8236908.773782926,4950988.032813099],[-8236899.283018903,4950981.089768483],[-8236899.854290463,4950980.049449641],[-8236909.483700305,4950962.5115120085],[-8236950.122693996,4950888.490607191],[-8236936.114668069,4950880.711259098],[-8236630.766823769,4950711.120653876],[-8236603.617016273,4950696.125954079],[-8236595.519949958,4950691.653753979],[-8236576.922953766,4950681.381209501],[-8236313.486306158,4950535.858450708],[-8235996.705224471,4950360.71499038],[-8235872.497241086,4950291.829726318],[-8235679.036906484,4950184.053838596],[-8235650.881363109,4950236.237075874],[-8235645.267106087,4950233.800825205],[-8235639.4123770995,4950232.018318168],[-8235633.393020019,4950230.91264548],[-8235627.287004976,4950230.498086113],[-8235621.173432299,4950230.780057408],[-8235615.13149945,4950231.754906665],[-8235608.467327378,4950234.005332784],[-8235602.132745514,4950237.062898235],[-8235596.225073129,4950240.880630011],[-8235590.835045982,4950245.399913983],[-8235586.045502263,4950250.551286667],[-8235581.93002563,4950256.255607528],[-8235579.3426492205,4950261.772087585],[-8235577.335111328,4950267.524991512],[-8235575.928615672,4950273.45356018],[-8235575.138018175,4950279.495178976],[-8235574.971662057,4950285.586024909],[-8235575.431311833,4950291.661783868],[-8235576.51211401,4950297.658285965],[-8235578.202654705,4950303.512198178],[-8235580.908645711,4950309.144907462],[-8235584.243961529,4950314.429367084],[-8235588.164417074,4950319.295568253],[-8235592.61811109,4950323.678963843],[-8235597.545997612,4950327.521556021],[-8235602.882792425,4950330.772437399],[-8235529.797551392,4950460.017267684],[-8235478.680352066,4950552.627315229],[-8235428.3760458175,4950643.855006403],[-8235377.226788646,4950735.656751019],[-8235325.998720724,4950827.309979153],[-8235273.714592274,4950920.676471936],[-8235224.704280836,4951010.433957826],[-8235173.964060631,4951101.851878839],[-8235123.253067672,4951193.34411639],[-8235072.138928443,4951285.320574381],[-8235021.609919899,4951376.609313475]]]},"properties":{"shape_area":15797449.0611,"ntaname":"Upper West Side-Lincoln Square","cdtaname":"MN07 Upper West Side (CD 7 Approximation)","shape_leng":19952.4196524,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0701","borocode":1,"countyfips":"061","ntaabbrev":"UWS_LclnSq","cdta2020":"MN07"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8233654.407544356,4953841.341577955],[-8233794.460735421,4953916.722325897],[-8233907.261508302,4953980.91681264],[-8233970.511065866,4954016.365379364],[-8234142.977252138,4954110.019390947],[-8234288.33276629,4954188.948583957],[-8234237.660747552,4954280.581298703],[-8234384.29726687,4954362.373876397],[-8234394.83324633,4954368.149479332],[-8234406.3425054625,4954374.367369698],[-8234553.939401927,4954456.369636269],[-8234709.211829571,4954542.4207938155],[-8234762.648276994,4954571.053782213],[-8234867.201650328,4954625.188291875],[-8234885.008040207,4954634.9117875835],[-8234903.652134752,4954644.874757882],[-8234914.344816602,4954650.852586027],[-8234925.5146329105,4954656.935397342],[-8234952.89409061,4954671.405122502],[-8235010.2784615215,4954704.388055712],[-8235015.125208844,4954707.199029213],[-8235065.503862591,4954613.176299505],[-8235170.442065856,4954415.906338628],[-8235225.515880613,4954313.682672009],[-8235336.693991595,4954107.277324123],[-8235421.143534382,4953951.412699638],[-8235430.648193617,4953933.871093797],[-8235464.279678992,4953875.357850441],[-8235475.7830905225,4953855.222263292],[-8235521.357743879,4953768.962492173],[-8235559.461311163,4953696.842990938],[-8235570.857196101,4953675.27333602],[-8235625.928593493,4953571.0368083175],[-8235677.155820522,4953474.1051456],[-8235827.278476213,4953190.0383033855],[-8235930.1613241155,4952995.353672984],[-8235987.577815675,4952995.5882328255],[-8236062.95934765,4952999.87178531],[-8236078.231049323,4952980.868403661],[-8236077.484404671,4952975.849986858],[-8236071.8536365675,4952971.9953643745],[-8236055.324430757,4952993.019997702],[-8235934.129891362,4952987.8440635735],[-8235965.399466563,4952928.671802034],[-8235974.414771417,4952911.611897749],[-8235987.180027572,4952918.357712185],[-8235990.631336451,4952912.602181072],[-8235996.018716709,4952915.319866702],[-8235995.360060754,4952916.5082725305],[-8236041.014281631,4952941.695586782],[-8236047.190834707,4952945.103121697],[-8236043.35569635,4952952.021461846],[-8236048.543295037,4952954.897399404],[-8236057.759254785,4952938.272736764],[-8236052.401230156,4952935.302540744],[-8236048.656097264,4952942.058539829],[-8235999.246678823,4952914.778449772],[-8236003.893855928,4952906.39562873],[-8236000.314950265,4952904.128306067],[-8235999.547964151,4952905.509662918],[-8236000.923717572,4952906.041869567],[-8235996.595693008,4952913.993902026],[-8235991.506579568,4952911.479641034],[-8235994.660356987,4952904.628069155],[-8235981.558551362,4952898.0935638435],[-8236020.318165444,4952823.694521778],[-8236094.714826818,4952918.054351089],[-8236086.882046776,4952943.06035957],[-8236091.764567576,4952944.780039881],[-8236098.141756874,4952944.530373392],[-8236102.172226022,4952927.3773777345],[-8236139.258561211,4952902.380047613],[-8236208.413893435,4952793.872526637],[-8236203.218799031,4952790.8438672125],[-8236134.5922016,4952897.695781268],[-8236103.071259951,4952919.552109915],[-8236024.38029168,4952815.896956787],[-8236041.07732401,4952783.8468388645],[-8236096.505891037,4952680.896190595],[-8236111.883875229,4952651.583864296],[-8236136.700735683,4952604.397994295],[-8236292.824491901,4952307.905189031],[-8236297.544142457,4952299.327249984],[-8236302.120291876,4952290.175598579],[-8236308.495239637,4952270.141767843],[-8236314.995092625,4952272.59421241],[-8236317.64947021,4952265.556411718],[-8236311.0809793575,4952263.078814265],[-8236320.6736356,4952236.108530961],[-8236305.775783518,4952227.471299828],[-8236246.616933272,4952193.172184667],[-8236241.287834763,4952190.082477179],[-8236235.645424687,4952186.464295112],[-8236229.421433044,4952183.315426503],[-8236203.11063873,4952163.55660317],[-8236144.380129775,4952127.179301784],[-8236138.229033396,4952123.94650121],[-8236048.15238242,4952076.604803796],[-8236013.661725419,4952188.7440422],[-8235980.523818485,4952291.448883661],[-8235808.442945876,4952196.306378416],[-8235676.633472486,4952123.145181532],[-8235624.025843351,4952093.924521824],[-8235597.142874141,4952078.992169751],[-8235585.0357510485,4952072.08317268],[-8235572.832228026,4952065.297105473],[-8235490.919571667,4952019.864751572],[-8235354.49813746,4951944.406413244],[-8235333.704435962,4951932.904441182],[-8235174.701300723,4951844.944739593],[-8234859.455923009,4951668.785706151],[-8234831.973570162,4951718.122883108],[-8234807.26881624,4951762.507691601],[-8234780.622661675,4951810.862862559],[-8234755.434840309,4951855.883069596],[-8234700.275201957,4951954.35747818],[-8234494.543645791,4952326.859464987],[-8234438.869056035,4952427.145126881],[-8234387.35129474,4952519.977681903],[-8234336.20570338,4952613.015421795],[-8234284.060680074,4952706.086313966],[-8234228.787963541,4952806.0229098005],[-8234203.604458876,4952851.424548332],[-8234200.997479077,4952856.757801718],[-8234174.757549455,4952905.726689889],[-8234122.929972854,4952996.7715187585],[-8234071.964444065,4953088.436840503],[-8234018.767180187,4953182.047390034],[-8233969.863007444,4953272.278358585],[-8233918.934072987,4953364.603676402],[-8233868.031905965,4953456.472027896],[-8233816.044552917,4953548.999076394],[-8233765.846122171,4953640.427828251],[-8233711.1160886055,4953739.102160639],[-8233654.407544356,4953841.341577955]]]},"properties":{"shape_area":25410668.2689,"ntaname":"Upper West Side (Central)","cdtaname":"MN07 Upper West Side (CD 7 Approximation)","shape_leng":24906.0090118,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0702","borocode":1,"countyfips":"061","ntaabbrev":"UWS_C","cdta2020":"MN07"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8233150.226965135,4955129.555382049],[-8233309.534657499,4955223.886553197],[-8233619.372793449,4955395.53075327],[-8233672.974701867,4955425.199379497],[-8233793.603712676,4955491.966137473],[-8233809.030447472,4955500.504408394],[-8233861.353812262,4955529.495832367],[-8233876.931410583,4955538.14005933],[-8233923.9698302755,4955564.241921519],[-8233935.498574936,4955570.804897706],[-8233946.87590889,4955576.530653372],[-8234085.493169448,4955646.284349989],[-8234135.205534119,4955668.049786279],[-8234142.337455309,4955671.172335678],[-8234147.674024922,4955673.716413806],[-8234279.047249072,4955736.645128894],[-8234379.907757334,4955784.956948397],[-8234387.74010271,4955788.927708048],[-8234396.407498496,4955793.025478973],[-8234412.6818697285,4955801.457978802],[-8234426.037912124,4955808.378099812],[-8234446.428851072,4955770.077988932],[-8234463.268886467,4955741.615714111],[-8234482.084585403,4955706.713573784],[-8234483.126733845,4955698.818239231],[-8234493.31375607,4955682.614903884],[-8234503.086965783,4955661.214871524],[-8234505.167897577,4955651.242184307],[-8234520.132526576,4955636.909717035],[-8234531.045269024,4955623.822526203],[-8234544.561369822,4955592.8675218215],[-8234587.077164112,4955516.623902238],[-8234622.420269681,4955451.806818254],[-8234622.006362228,4955446.613095736],[-8234628.139776527,4955436.224868189],[-8234640.614053849,4955410.463723826],[-8234644.563337655,4955407.764500671],[-8234651.007158836,4955395.922845867],[-8234659.533034863,4955374.731261039],[-8234677.5144967595,4955345.440372597],[-8234697.05840071,4955305.34458846],[-8234704.956305401,4955296.620604472],[-8234716.909783251,4955277.716459689],[-8234722.939726566,4955259.848758387],[-8234736.86780579,4955236.998412962],[-8234741.4422734985,4955225.156201426],[-8234763.999450368,4955182.361371805],[-8234802.249603865,4955115.053410777],[-8234822.1025464395,4955079.3222328015],[-8234841.436196428,4955041.927455397],[-8234855.364905416,4955014.298162011],[-8234868.980681438,4954987.915351568],[-8234889.146392626,4954950.313994306],[-8234919.873452529,4954891.045067476],[-8234944.178576648,4954851.775162378],[-8234947.429822347,4954844.370879499],[-8234946.34912272,4954836.552712132],[-8234951.78870962,4954827.181021003],[-8235002.408018679,4954730.933154131],[-8235015.125208833,4954707.199029218],[-8235010.278461517,4954704.388055712],[-8234952.894090598,4954671.405122509],[-8234925.514632897,4954656.935397342],[-8234914.344816602,4954650.852586035],[-8234903.652134739,4954644.874757882],[-8234885.008040207,4954634.911787588],[-8234867.201650328,4954625.188291875],[-8234762.648276982,4954571.053782213],[-8234709.211829567,4954542.4207938155],[-8234553.939401922,4954456.369636269],[-8234406.3425054625,4954374.367369698],[-8234394.83324633,4954368.149479332],[-8234384.297266854,4954362.373876399],[-8234237.660747541,4954280.581298712],[-8234288.33276629,4954188.948583957],[-8234142.977252144,4954110.019390953],[-8233970.511065866,4954016.365379368],[-8233907.261508296,4953980.91681264],[-8233794.460735421,4953916.722325897],[-8233654.407544356,4953841.341577955],[-8233613.222066865,4953917.1930803815],[-8233602.646333301,4953936.106737448],[-8233547.801496089,4954034.177668806],[-8233532.326247722,4954061.905782456],[-8233520.802939314,4954083.628817277],[-8233503.626721823,4954116.007800222],[-8233452.230800792,4954206.065340986],[-8233401.029746366,4954297.937781644],[-8233349.906658899,4954390.447691605],[-8233298.600689997,4954482.410952883],[-8233272.78937863,4954528.894162859],[-8233247.727553246,4954574.668966046],[-8233192.89908114,4954672.760015037],[-8233137.952284856,4954772.431231602],[-8233086.75675191,4954865.260899832],[-8233035.783688725,4954956.785439848],[-8233003.974898873,4955013.62721354],[-8232986.154989597,4955045.486897292],[-8233020.457465528,4955058.368743348],[-8233059.719680253,4955079.906783893],[-8233150.226965135,4955129.555382049]]]},"properties":{"shape_area":13200450.2651,"ntaname":"Upper West Side-Manhattan Valley","cdtaname":"MN07 Upper West Side (CD 7 Approximation)","shape_leng":14867.8726733,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0703","borocode":1,"countyfips":"061","ntaabbrev":"UWS_MnVly","cdta2020":"MN07"}}, +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8232926.354008527,4947405.220592536],[-8232919.756805357,4947417.2493196735],[-8232932.203855738,4947424.055945653],[-8232949.03769896,4947395.216718257],[-8232936.459033295,4947388.784905281],[-8232929.808959983,4947400.409503909],[-8232913.343378918,4947390.183902337],[-8232910.510092856,4947394.991677119],[-8232926.354008527,4947405.220592536]]],[[[-8230469.777987238,4950847.445263461],[-8230474.293502001,4950849.275589969],[-8230484.2905297605,4950825.206807191],[-8230496.706831934,4950829.960600224],[-8230501.390769029,4950817.753489172],[-8230469.5828460185,4950805.03119935],[-8230464.619459375,4950817.796255511],[-8230479.616664352,4950823.668355243],[-8230469.777987238,4950847.445263461]]],[[[-8231163.560772294,4950426.939836294],[-8231103.384409086,4950465.295017289],[-8231098.885755142,4950469.023430228],[-8231087.580754363,4950480.746666019],[-8231055.105054346,4950553.627526769],[-8231002.7049408695,4950639.199484727],[-8230994.302937347,4950664.697152627],[-8230988.6084553115,4950691.280477185],[-8230985.437397805,4950731.700010113],[-8230994.059028577,4950817.334747772],[-8230994.550172214,4950903.472369286],[-8230983.689063815,4950942.055973697],[-8230964.437508689,4950982.683069763],[-8230971.682730354,4950994.957466397],[-8230983.141918943,4950999.808606864],[-8230993.718612258,4950998.069298898],[-8231026.553965122,4950949.355887182],[-8231061.897572761,4950913.884778902],[-8231115.646504279,4950904.606375302],[-8231199.89751488,4950864.419475642],[-8231250.1645591995,4950815.682402703],[-8231276.748362965,4950773.073166083],[-8231319.83261502,4950711.800068649],[-8231408.899163793,4950609.53543845],[-8231438.491269865,4950587.067258393],[-8231470.945835005,4950549.855749643],[-8231483.337660089,4950540.129168229],[-8231489.109740351,4950535.195367986],[-8231488.97040552,4950527.550300657],[-8231529.838630874,4950451.361933725],[-8231537.228518937,4950437.392962224],[-8231547.031495801,4950386.870889383],[-8231562.279858886,4950385.397140622],[-8231564.822029882,4950380.853749585],[-8231582.421862623,4950391.044897142],[-8231589.567510452,4950375.11120977],[-8231573.307290442,4950365.129274402],[-8231586.15828696,4950342.593305178],[-8231598.068061871,4950321.665535546],[-8231603.638561375,4950317.820242239],[-8231613.757603568,4950320.683719071],[-8231614.4933244055,4950309.9251978425],[-8231621.558223029,4950304.9746232],[-8231692.596815802,4950181.1446051495],[-8231820.4306862345,4949984.098636444],[-8231944.224977185,4949777.417612173],[-8231974.630198618,4949726.65346419],[-8231987.707156593,4949727.4757805625],[-8232073.217959721,4949636.1733081825],[-8232118.142487848,4949567.041963434],[-8232157.683959402,4949506.192576923],[-8232180.689407217,4949478.707795656],[-8232184.537350568,4949467.5827768035],[-8232257.24379854,4949348.168860563],[-8232285.254443528,4949284.735212518],[-8232308.592539322,4949228.600459379],[-8232352.926245082,4949169.740707418],[-8232407.840488397,4949096.83289278],[-8232424.163586868,4949110.104194464],[-8232453.87390068,4949072.626452673],[-8232446.282895486,4949065.189633954],[-8232478.5105989305,4949029.495334852],[-8232484.042681364,4949026.213748672],[-8232493.525664434,4949009.261427203],[-8232497.294003988,4949003.2802190855],[-8232504.139262193,4948990.96273315],[-8232511.375299248,4948985.3892784435],[-8232632.280455016,4948799.193855819],[-8232671.005927348,4948739.481748744],[-8232682.5778809525,4948721.638692608],[-8232684.655509919,4948718.4351178855],[-8232689.075053621,4948712.006645345],[-8232692.457888819,4948706.528351395],[-8232696.087190666,4948700.911545945],[-8232698.938720661,4948695.8074056655],[-8232705.870463649,4948677.113739238],[-8232742.661902731,4948622.819374177],[-8232795.856325391,4948544.317834222],[-8232843.866305464,4948473.4665109515],[-8232931.358861182,4948344.34614589],[-8233017.925387536,4948216.590266425],[-8233070.061059806,4948159.976294529],[-8233080.019201186,4948164.743672349],[-8233133.968301676,4948071.250878712],[-8233141.845948675,4948049.049426579],[-8233177.362675939,4947945.521701225],[-8233190.79957429,4947906.589825667],[-8233265.8573423335,4947814.431283895],[-8233313.170654917,4947726.38866981],[-8233339.766916205,4947652.280129558],[-8233341.312163203,4947649.90018812],[-8233343.209681221,4947647.790370312],[-8233345.413030418,4947646.002311344],[-8233347.136308287,4947644.003410462],[-8233348.546850539,4947641.772791968],[-8233349.61397776,4947639.358972488],[-8233350.314479949,4947636.814453244],[-8233350.633121344,4947634.194578174],[-8233349.95566465,4947630.963376753],[-8233348.806486819,4947627.868381857],[-8233348.394816871,4947627.018781208],[-8233347.465124769,4947625.375958477],[-8233345.514883041,4947622.712152473],[-8233343.890340883,4947621.0012695305],[-8233360.810190079,4947597.964009665],[-8233337.421983549,4947580.082184603],[-8233319.200371666,4947604.974167711],[-8233316.6874887785,4947603.211326699],[-8233313.917395035,4947601.888917349],[-8233310.966570002,4947601.043449857],[-8233307.916482938,4947600.698266576],[-8233304.851343624,4947600.862897613],[-8233301.855777389,4947601.532797698],[-8233299.012488761,4947602.689471671],[-8233296.399978081,4947604.300985125],[-8233294.090374292,4947606.322846009],[-8233290.379271724,4947609.507470828],[-8233287.2353467,4947613.253117631],[-8233284.742270327,4947617.460102786],[-8233283.532751575,4947619.87032647],[-8233281.946979173,4947622.051482817],[-8233280.027265353,4947623.945372748],[-8233233.143552389,4947675.9928902425],[-8233178.615380054,4947720.247334144],[-8233140.420381357,4947757.732539829],[-8233081.289150691,4947828.400642863],[-8233081.83591521,4947841.43633906],[-8233043.148867863,4947868.861488387],[-8233034.5330876,4947874.650361618],[-8232957.38504786,4947947.33338739],[-8232878.961622204,4948035.968635099],[-8232847.125108735,4948071.950257952],[-8232721.346254788,4948247.669650445],[-8232654.652387611,4948337.40172513],[-8232641.0979156485,4948340.938330964],[-8232532.228353315,4948466.283524228],[-8232455.479481822,4948554.645482274],[-8232448.770713519,4948562.381369155],[-8232444.4733057665,4948567.205262756],[-8232440.234899844,4948572.580252269],[-8232434.804013911,4948579.284664948],[-8232423.835240264,4948593.479349362],[-8232359.183986589,4948677.14423432],[-8232322.52314447,4948748.632097327],[-8232318.645977519,4948784.893507741],[-8232309.261297942,4948808.580897776],[-8232274.294736444,4948868.516751261],[-8232266.912456406,4948877.292382367],[-8232241.794184119,4948907.151539266],[-8232216.613298588,4948928.18316121],[-8232113.4790249895,4949081.40942402],[-8232085.542088766,4949109.330036452],[-8232044.3138715485,4949150.533938923],[-8232013.087488926,4949182.698121235],[-8231981.431541593,4949231.391109188],[-8231940.299952167,4949294.835250659],[-8231889.839032571,4949372.671929272],[-8231845.890178952,4949440.616225022],[-8231840.645611593,4949448.72433072],[-8231752.267442938,4949585.353969887],[-8231722.860505797,4949632.015750891],[-8231700.2416991275,4949667.906148043],[-8231694.702075437,4949675.9273092095],[-8231657.546835153,4949729.726197036],[-8231501.9652591925,4949954.996282486],[-8231433.29943098,4950078.181378264],[-8231387.752208093,4950159.578619629],[-8231321.51287797,4950257.931317841],[-8231280.998261268,4950318.087160157],[-8231249.024544404,4950347.72392228],[-8231163.560772294,4950426.939836294]]],[[[-8233034.476370143,4948896.523802843],[-8233021.579614407,4948889.503360075],[-8233017.5523985205,4948894.62690585],[-8233014.344666969,4948897.989756958],[-8233009.790012626,4948903.248972648],[-8233007.114625169,4948906.241990919],[-8232995.465607494,4948919.274137455],[-8232967.452920242,4948951.882446175],[-8232926.572089899,4949000.887729392],[-8232922.226959753,4949006.096342268],[-8232890.910719418,4949042.680488826],[-8232853.178020316,4949087.265244612],[-8232781.351612641,4949173.616604593],[-8232778.67846031,4949176.805508595],[-8232730.037080248,4949234.830984958],[-8232714.896778112,4949252.352407848],[-8232568.18270347,4949422.138279823],[-8232451.000926662,4949595.492645677],[-8232333.451199738,4949769.150518011],[-8232272.801802245,4949858.376556704],[-8232212.707941232,4949947.319949355],[-8232150.199176862,4950036.56068557],[-8232084.113487247,4950130.906895848],[-8232029.492094765,4950219.319134427],[-8232006.58261048,4950256.400846726],[-8231974.527759182,4950314.3939628685],[-8231927.639666059,4950399.222111146],[-8231863.315845196,4950489.588234453],[-8231796.904832924,4950583.350964693],[-8231800.558757907,4950585.4801188465],[-8231810.794734121,4950591.444535209],[-8231816.455791754,4950594.786434887],[-8231822.118301315,4950598.127033234],[-8231830.757117254,4950602.971273631],[-8231861.9293297315,4950624.238194162],[-8231878.720996493,4950633.5619258685],[-8232124.406899718,4950769.979349523],[-8232375.517957588,4950909.565887578],[-8232639.043657721,4951055.090703412],[-8232888.319611016,4951193.717031972],[-8232943.946459737,4951093.608601158],[-8232995.325352824,4951000.690350154],[-8233030.4106478365,4950937.3540829625],[-8233046.724374306,4950907.90447606],[-8233098.457432208,4950814.945500885],[-8233149.954647502,4950722.32308801],[-8233201.734241343,4950629.775295147],[-8233256.5317761395,4950529.765326353],[-8233313.10035667,4950429.499502964],[-8233361.845447353,4950336.84161847],[-8233413.75095805,4950246.168906944],[-8233464.497019621,4950154.310832486],[-8233515.555749581,4950062.610300617],[-8233568.196790774,4949967.816653757],[-8233617.329882665,4949879.748970408],[-8233667.9794492815,4949787.846942559],[-8233718.913075564,4949696.353201855],[-8233769.878658411,4949604.7439311715],[-8233820.675852674,4949512.975740431],[-8233871.646166338,4949421.434865995],[-8233922.397951722,4949329.439832317],[-8233669.719589228,4949190.108212819],[-8233585.412156802,4949143.492347328],[-8233532.4848804325,4949114.5556052895],[-8233408.363813976,4949046.669718891],[-8233158.570307019,4948906.655180918],[-8233082.317259539,4948865.76939056],[-8233070.926498501,4948858.650535104],[-8233051.135301699,4948895.768925504],[-8233046.947813803,4948903.312759962],[-8233034.476370143,4948896.523802843]]]]},"properties":{"shape_area":21500955.6247,"ntaname":"Upper East Side-Lenox Hill-Roosevelt Island","cdtaname":"MN08 Upper East Side-Roosevelt Island (CD 8 Equivalent)","shape_leng":38475.0532075,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0801","borocode":1,"countyfips":"061","ntaabbrev":"UES_LnxHl","cdta2020":"MN08"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8232261.046221926,4952324.838160654],[-8232209.846518444,4952416.474308138],[-8232158.624489924,4952508.491857813],[-8232107.797322914,4952600.5751008],[-8232056.741181901,4952692.123419522],[-8232001.949971277,4952791.3548383],[-8232113.974601126,4952853.4557288075],[-8232179.6627385495,4952889.810849286],[-8232220.693095894,4952912.466967091],[-8232324.528151867,4952969.801691413],[-8232350.668822144,4952984.28359468],[-8232360.370799444,4952989.870983532],[-8232372.606041424,4952996.672356159],[-8232456.663518584,4953043.1807405995],[-8232489.723519514,4953061.490218572],[-8232539.964131719,4953089.314521172],[-8232719.476666507,4953189.839675914],[-8232773.349815894,4953088.627920132],[-8232824.258631062,4952997.517965693],[-8232873.662510656,4952906.544694923],[-8232926.378943211,4952813.3718211455],[-8232978.425318516,4952721.038447699],[-8233030.140630848,4952628.836750388],[-8233079.529283597,4952537.719891034],[-8233130.435090595,4952445.753907398],[-8233180.738396543,4952354.899613219],[-8233235.659316286,4952254.044180714],[-8233290.857698575,4952156.330156569],[-8233341.904959391,4952064.466826307],[-8233394.185304324,4951969.928942606],[-8233445.976223222,4951876.622807673],[-8233497.577275236,4951783.779034631],[-8233538.48092765,4951709.828359763],[-8233549.091923023,4951690.644270591],[-8233604.3183964435,4951592.335894108],[-8233608.2381528,4951584.318712234],[-8233660.220188039,4951490.502348722],[-8233711.333275488,4951397.775559086],[-8233762.550902407,4951305.104964141],[-8233814.00638534,4951211.8228772925],[-8233865.748102576,4951118.9544255],[-8233917.142127694,4951026.320080607],[-8233973.169784747,4950926.873286226],[-8234028.218308807,4950826.565864269],[-8234079.346608037,4950734.471211087],[-8234129.249083407,4950642.091880288],[-8234180.788548039,4950551.96954847],[-8234232.028899067,4950459.698548976],[-8234248.861034767,4950429.615583159],[-8234283.203196031,4950368.237757774],[-8234334.087598508,4950277.315495208],[-8234383.74935218,4950184.880906703],[-8234434.670782239,4950093.600650151],[-8234486.159781143,4950001.678826294],[-8234536.340503762,4949910.138857731],[-8234586.990755677,4949819.434237085],[-8234638.347894098,4949728.82805847],[-8234459.852115225,4949627.600403489],[-8234292.146594115,4949535.447163084],[-8234280.502095224,4949528.927673511],[-8234268.003422276,4949521.9660037095],[-8234100.898066537,4949428.392257531],[-8233922.397951734,4949329.43983231],[-8233871.64616635,4949421.434865989],[-8233820.675852693,4949512.975740431],[-8233769.878658411,4949604.743931166],[-8233718.913075577,4949696.353201847],[-8233667.979449289,4949787.846942559],[-8233617.329882665,4949879.748970403],[-8233568.196790774,4949967.816653751],[-8233515.555749592,4950062.610300617],[-8233464.497019634,4950154.31083248],[-8233413.75095805,4950246.1689069355],[-8233361.845447353,4950336.84161847],[-8233313.10035667,4950429.499502959],[-8233256.531776145,4950529.765326353],[-8233201.734241356,4950629.775295141],[-8233149.954647514,4950722.323088003],[-8233098.457432204,4950814.945500885],[-8233046.724374318,4950907.904476051],[-8233030.41064785,4950937.3540829625],[-8232995.325352824,4951000.690350154],[-8232943.946459737,4951093.608601148],[-8232888.319611021,4951193.717031966],[-8232833.037281627,4951293.694610724],[-8232809.257550209,4951336.600486639],[-8232781.544446294,4951386.602845591],[-8232729.715577109,4951479.653956257],[-8232678.281706138,4951572.239086538],[-8232626.841219248,4951665.2955702515],[-8232575.30479319,4951758.243378325],[-8232519.807717277,4951858.327653056],[-8232494.714654651,4951903.622532091],[-8232489.7772560865,4951912.534832552],[-8232465.085462118,4951957.104905451],[-8232414.051748001,4952049.193526951],[-8232362.904781661,4952141.099474748],[-8232312.5676658815,4952233.899218292],[-8232261.046221926,4952324.838160654]]]},"properties":{"shape_area":20065364.1,"ntaname":"Upper East Side-Carnegie Hill","cdtaname":"MN08 Upper East Side-Roosevelt Island (CD 8 Equivalent)","shape_leng":23778.410348,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0802","borocode":1,"countyfips":"061","ntaabbrev":"UES_CrngHl","cdta2020":"MN08"}}, +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8230745.67616916,4952152.3039979795],[-8230747.994382579,4952169.168440484],[-8230746.01931396,4952185.439207191],[-8230736.9009853015,4952219.6386516765],[-8230749.653242019,4952238.545463773],[-8230774.0151595725,4952230.29272997],[-8230823.357677068,4952183.692099449],[-8230832.7533993,4952142.624860787],[-8230833.965039467,4952118.578330048],[-8230821.554120222,4952105.617011932],[-8230876.085863755,4952032.801200915],[-8230885.81385741,4952023.678360548],[-8230893.59169348,4952027.712501805],[-8230906.743392642,4952012.324078772],[-8230899.962123853,4952003.319090645],[-8230916.816938733,4951968.79849367],[-8230910.846674944,4951961.644191868],[-8230887.2538384115,4951989.524318563],[-8230878.263888592,4951991.77833943],[-8230808.638716413,4952080.459508564],[-8230784.617940474,4952069.253106765],[-8230766.974822658,4952068.762015254],[-8230717.432669882,4952083.234399218],[-8230695.865171176,4952099.334090495],[-8230701.955840328,4952146.514001027],[-8230701.1385232285,4952177.444668774],[-8230718.18423587,4952181.856209424],[-8230717.632626356,4952145.2327746125],[-8230729.52707346,4952140.654495898],[-8230745.67616916,4952152.3039979795]]],[[[-8231800.558757907,4950585.4801188465],[-8231796.904747038,4950583.351050489],[-8231732.10437094,4950673.951228758],[-8231676.060334293,4950752.307816547],[-8231588.223794154,4950850.995816275],[-8231506.265469375,4950942.7375509925],[-8231448.940210736,4951021.659104082],[-8231404.068359454,4951083.434861777],[-8231371.059211439,4951128.878616423],[-8231334.83911708,4951183.120886647],[-8231289.364026314,4951251.222772607],[-8231251.210300981,4951330.690292714],[-8231230.428408514,4951405.074617632],[-8231194.872415838,4951438.767673429],[-8231186.0851501115,4951472.199059881],[-8231194.0467373375,4951579.468197395],[-8231195.929370827,4951585.8326636655],[-8231198.6343452,4951591.893514237],[-8231202.114475874,4951597.545021931],[-8231206.309056065,4951602.688599937],[-8231211.14491559,4951607.234521619],[-8231216.537697348,4951611.103485866],[-8231222.393328722,4951614.22800056],[-8231228.60966278,4951616.553560012],[-8231223.402341223,4951627.947037454],[-8231233.616138582,4951633.811560868],[-8231234.105231749,4951635.497436206],[-8231218.480915259,4951627.073470712],[-8231220.264602874,4951623.673783528],[-8231206.423545742,4951615.486270408],[-8231199.127043047,4951628.9230820425],[-8231214.263321751,4951637.840197962],[-8231212.561168441,4951640.43025914],[-8231217.569261397,4951658.895565386],[-8231212.628916619,4951660.349816041],[-8231223.126820776,4951704.082752716],[-8231226.123151636,4951703.517905718],[-8231225.800341897,4951701.898217668],[-8231229.201843682,4951700.928804414],[-8231233.089218835,4951700.2025792],[-8231233.493039514,4951701.741363362],[-8231236.489475474,4951701.014551144],[-8231226.234743586,4951656.957904139],[-8231221.375857423,4951657.764464496],[-8231217.013403297,4951642.376557286],[-8231218.634386354,4951640.110314758],[-8231236.706947923,4951651.168079307],[-8231258.736725622,4951756.2371317865],[-8231297.382182249,4951862.833428138],[-8231290.8726733355,4951866.020285519],[-8231284.882206026,4951870.100004455],[-8231279.532415676,4951874.989748513],[-8231274.931929428,4951880.590233882],[-8231271.174160425,4951886.78774515],[-8231268.33542399,4951893.45643066],[-8231266.473347475,4951900.460896082],[-8231265.62574225,4951907.65891711],[-8231265.8098211605,4951914.9043386895],[-8231267.021848764,4951922.050042694],[-8231201.707779289,4951943.180228012],[-8231200.724797136,4951944.018959066],[-8231199.884358017,4951945.000481403],[-8231199.206951857,4951946.1008655485],[-8231198.709093887,4951947.29328419],[-8231198.402921958,4951948.5486661745],[-8231198.295900588,4951949.836405291],[-8231198.390639068,4951951.12510646],[-8231198.684827735,4951952.383351073],[-8231199.171294357,4951953.58046308],[-8231226.336899589,4952029.13837757],[-8231226.807635706,4952030.487205277],[-8231227.484564859,4952031.745257774],[-8231228.350852642,4952032.88124863],[-8231229.384955408,4952033.8669268675],[-8231230.56115618,4952034.677779668],[-8231231.850204092,4952035.29364192],[-8231233.220041864,4952035.699197724],[-8231234.636603061,4952035.884361299],[-8231236.064659256,4952035.84452779],[-8231237.468696137,4952035.580687841],[-8231345.951086105,4951996.149450842],[-8231363.952270891,4952054.120472543],[-8231376.666487539,4952123.05454713],[-8231394.040200653,4952217.250467211],[-8231395.14469028,4952255.182721447],[-8231369.268911428,4952419.808726951],[-8231372.090454902,4952428.663470658],[-8231381.716337882,4952445.111095318],[-8231389.7885612175,4952449.527544498],[-8231396.543276233,4952453.721675034],[-8231403.373396297,4952457.791953179],[-8231414.482308178,4952464.944202727],[-8231480.41297264,4952502.071869369],[-8231488.871623072,4952506.421306921],[-8231622.367589188,4952580.412809783],[-8231680.6001767265,4952612.687946675],[-8231752.35082013,4952652.483390346],[-8231836.279170831,4952699.180325182],[-8231940.118297608,4952756.953806335],[-8232001.949971286,4952791.354838305],[-8232056.741181901,4952692.123419529],[-8232107.797322908,4952600.575100803],[-8232158.624489911,4952508.491857813],[-8232209.846518437,4952416.474308138],[-8232261.046221926,4952324.838160654],[-8232312.5676658815,4952233.899218292],[-8232362.904781655,4952141.099474748],[-8232414.05174799,4952049.193526951],[-8232465.085462099,4951957.104905457],[-8232489.777256079,4951912.534832557],[-8232494.714654645,4951903.622532099],[-8232519.807717277,4951858.327653063],[-8232575.304793199,4951758.243378325],[-8232626.841219237,4951665.295570254],[-8232678.281706143,4951572.239086543],[-8232729.715577116,4951479.653956257],[-8232781.54444628,4951386.602845597],[-8232809.257550201,4951336.6004866455],[-8232833.037281627,4951293.694610724],[-8232888.319611016,4951193.717031972],[-8232639.043657721,4951055.090703412],[-8232375.517957588,4950909.565887578],[-8232124.406899718,4950769.979349523],[-8231878.720996493,4950633.5619258685],[-8231861.9293297315,4950624.238194162],[-8231830.757117254,4950602.971273631],[-8231822.118301315,4950598.127033234],[-8231816.455791754,4950594.786434887],[-8231810.794734121,4950591.444535209],[-8231800.558757907,4950585.4801188465]]]]},"properties":{"shape_area":13645033.1064,"ntaname":"Upper East Side-Yorkville","cdtaname":"MN08 Upper East Side-Roosevelt Island (CD 8 Equivalent)","shape_leng":18113.2933055,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0803","borocode":1,"countyfips":"061","ntaabbrev":"UES_Yrkvl","cdta2020":"MN08"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8232310.254408663,4956636.545320823],[-8232470.841461961,4956725.621526833],[-8232530.592630487,4956759.216459876],[-8232655.5816309415,4956945.882051846],[-8232711.6333547365,4957032.303703195],[-8232773.081369861,4956921.751039523],[-8232864.0724273445,4957054.40905274],[-8232867.480653579,4957059.829325414],[-8232874.426837963,4957070.2766175745],[-8232947.777410295,4957180.596373461],[-8232989.111800923,4957242.776161908],[-8232992.144510226,4957247.416751844],[-8232999.680661071,4957258.948179955],[-8233006.542975529,4957268.484392206],[-8233010.867512913,4957274.493937394],[-8233133.132079316,4957451.875455639],[-8233266.035303376,4957525.584846684],[-8233303.102213988,4957545.830481292],[-8233344.135073014,4957570.650933055],[-8233364.916214473,4957584.116268433],[-8233371.672752922,4957588.494457528],[-8233377.453031921,4957592.42774218],[-8233391.984858619,4957600.700653959],[-8233401.029238235,4957604.236951413],[-8233415.635359291,4957609.957835781],[-8233492.231673969,4957504.77407364],[-8233532.992754937,4957451.676030725],[-8233537.637794117,4957444.200528251],[-8233568.465165117,4957404.506834299],[-8233650.494033223,4957270.797158823],[-8233670.977379546,4957232.859227219],[-8233756.725130208,4957058.03438169],[-8233758.141516129,4957051.57339447],[-8233784.77168109,4957002.3684850745],[-8233800.607850706,4956974.242873308],[-8233800.655259527,4956974.151827928],[-8233819.42945672,4956937.765167745],[-8233834.29596699,4956912.805763507],[-8233844.365895245,4956894.087277285],[-8233869.2998228185,4956844.649361453],[-8233875.054762875,4956837.689183827],[-8233888.36133028,4956811.771598487],[-8233901.068454685,4956787.5320557775],[-8233916.413402277,4956759.934675478],[-8233927.922099125,4956740.734391849],[-8233940.868586885,4956715.535947894],[-8233955.6119929105,4956685.059543032],[-8233960.165509444,4956672.821176685],[-8233969.033927639,4956651.943071776],[-8233973.352051031,4956647.862880702],[-8233983.301317677,4956628.423443872],[-8234013.509591169,4956574.907189082],[-8234029.330270614,4956538.431959103],[-8234037.839701829,4956517.55404461],[-8234037.836809707,4956510.836434881],[-8234043.113483024,4956505.795762482],[-8234047.187900395,4956496.675961247],[-8234049.103756621,4956488.758633639],[-8234057.257619039,4956478.918277098],[-8234066.846984346,4956460.919389433],[-8234067.084528653,4956454.680544495],[-8234075.714921978,4956439.801500497],[-8234094.774244723,4956402.365061326],[-8234093.091494549,4956396.848501294],[-8234097.168637205,4956390.848115743],[-8234106.759056302,4956375.728688895],[-8234127.376288608,4956336.372677001],[-8234131.690035341,4956325.333724822],[-8234140.675769281,4956299.899115132],[-8234142.153131901,4956289.318813235],[-8234153.860595747,4956273.261284874],[-8234174.959958922,4956242.303022467],[-8234182.15101846,4956224.545507745],[-8234201.548172957,4956195.15372483],[-8234203.852844233,4956191.667652706],[-8234253.680819236,4956102.171224606],[-8234277.452145828,4956062.582609005],[-8234289.69915037,4956039.069368745],[-8234326.557239192,4955976.206828774],[-8234354.511060772,4955930.587227868],[-8234426.037863033,4955808.378136612],[-8234412.6818697285,4955801.457978802],[-8234396.407498496,4955793.025478973],[-8234387.74010271,4955788.927708048],[-8234379.907757334,4955784.956948397],[-8234279.047249072,4955736.645128894],[-8234147.674024922,4955673.716413806],[-8234142.337455309,4955671.172335678],[-8234135.205534119,4955668.049786279],[-8234085.493169448,4955646.284349989],[-8233946.87590889,4955576.530653372],[-8233935.498574936,4955570.804897706],[-8233923.9698302755,4955564.241921519],[-8233876.931410583,4955538.14005933],[-8233861.353812262,4955529.495832367],[-8233809.030447472,4955500.504408394],[-8233793.603712676,4955491.966137473],[-8233672.974701867,4955425.199379497],[-8233619.372793449,4955395.53075327],[-8233309.534657499,4955223.886553197],[-8233150.226965135,4955129.555382049],[-8233094.629240543,4955230.244693053],[-8233043.531550426,4955321.9547148105],[-8232994.5943471445,4955415.22679308],[-8232989.498745826,4955530.634537294],[-8232993.034992436,4955604.2439266415],[-8232993.919912022,4955622.66602542],[-8232995.267093708,4955650.705544853],[-8232998.692008509,4955715.907432217],[-8232999.3661389705,4955727.142125695],[-8232999.0474291975,4955738.392525921],[-8232997.738356456,4955749.571063442],[-8232995.449032075,4955760.590740549],[-8232992.197347481,4955771.365763526],[-8232988.008609721,4955781.812261704],[-8232935.926473303,4955881.753679559],[-8232933.1049033515,4955886.845518172],[-8232887.316183313,4955969.47384428],[-8232885.170951054,4955973.345076905],[-8232833.922067719,4956065.575468297],[-8232797.660750779,4956130.619892121],[-8232794.453326369,4956136.37327151],[-8232785.4390008105,4956152.542866038],[-8232782.659222306,4956157.529237207],[-8232731.367558714,4956249.667029684],[-8232680.780836067,4956341.514248785],[-8232629.220324238,4956434.607213671],[-8232474.397861989,4956348.4800183615],[-8232441.087885583,4956407.509791081],[-8232419.063375534,4956438.933717086],[-8232364.801485507,4956537.279565896],[-8232310.254408663,4956636.545320823]]]},"properties":{"shape_area":15922767.0212,"ntaname":"Morningside Heights","cdtaname":"MN09 Morningside Heights-Hamilton Heights (CD 9 Equivalent)","shape_leng":17757.8071398,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0901","borocode":1,"countyfips":"061","ntaabbrev":"MrngsdHts","cdta2020":"MN09"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8231639.780965986,4958075.474832738],[-8231768.92944204,4958147.390588771],[-8231862.933287839,4958199.270128915],[-8231913.850770205,4958108.358634576],[-8232067.654920934,4958194.037488156],[-8232118.323295988,4958102.203310838],[-8232168.605648763,4958011.690972054],[-8232400.316092519,4958139.032236682],[-8232474.047382688,4958179.908038683],[-8232485.001719677,4958186.010191765],[-8232495.937696424,4958192.041258359],[-8232710.487639068,4958311.87854792],[-8232714.28821735,4958314.04450749],[-8232718.587759012,4958315.958796705],[-8232782.12128423,4958343.509268133],[-8232801.555454489,4958347.583197414],[-8232836.460948276,4958354.170349189],[-8232864.780290912,4958359.514452957],[-8232872.487156013,4958361.304930851],[-8232880.296837241,4958363.091672646],[-8232901.29871245,4958370.05462141],[-8233093.846836652,4958523.0610358035],[-8233140.085936501,4958431.64908024],[-8233137.60975822,4958430.36253866],[-8233138.913393261,4958424.8406693125],[-8233138.858147757,4958424.489663594],[-8233138.648740877,4958423.643625355],[-8233138.109716574,4958422.202484723],[-8233137.331800986,4958420.714811348],[-8233134.878906445,4958417.286731434],[-8233134.281371592,4958416.612186858],[-8233133.5981442705,4958415.971873352],[-8233131.953238504,4958414.77556678],[-8233129.93214655,4958413.674017725],[-8233127.0295523815,4958412.392679302],[-8233098.475316199,4958397.259483428],[-8233096.541245421,4958396.18578416],[-8232982.832990752,4958333.016064166],[-8232989.875918789,4958318.5686503975],[-8232992.323437977,4958313.547569718],[-8232960.115064792,4958296.913349132],[-8232975.213366016,4958273.028179702],[-8232981.442094201,4958261.265791378],[-8232985.607734088,4958250.570005651],[-8232989.185531882,4958224.931035395],[-8233025.6005532015,4958160.54521722],[-8233038.795872764,4958169.427284404],[-8233039.398033771,4958171.488618369],[-8233060.241329721,4958242.794825648],[-8233063.464663566,4958249.417953324],[-8233069.004285588,4958254.665818344],[-8233083.733603361,4958262.767069105],[-8233069.998321728,4958287.740265595],[-8233145.3813672755,4958329.200899901],[-8233179.519624195,4958267.131650122],[-8233103.796583554,4958225.483955772],[-8233090.803161794,4958250.110409709],[-8233078.239939923,4958243.481548824],[-8233074.167655607,4958238.215799177],[-8233049.8594483035,4958154.940946449],[-8233061.971370589,4958135.779504666],[-8233071.732928098,4958120.336540657],[-8233058.062106811,4958112.886740318],[-8233088.19407336,4958067.126726797],[-8233097.93737209,4958073.454577972],[-8233104.34314062,4958077.615436442],[-8233081.886617153,4958112.186497926],[-8233094.567643097,4958120.624418084],[-8233125.668788702,4958073.887116006],[-8233111.908863657,4958064.730933699],[-8233107.446961459,4958071.43609966],[-8233101.33808789,4958067.370312477],[-8233092.759664754,4958061.661924085],[-8233109.723716173,4958036.167751175],[-8233114.472180311,4958029.031748315],[-8233121.710692981,4958015.682698458],[-8233168.903414256,4957934.018417412],[-8233189.419100765,4957946.484150291],[-8233239.330600076,4957872.369264124],[-8233295.169961632,4957789.451334049],[-8233336.295095831,4957728.382207782],[-8233398.7273573475,4957635.195042178],[-8233415.635310187,4957609.957872582],[-8233401.029238235,4957604.236951413],[-8233391.984858619,4957600.700653959],[-8233377.453031921,4957592.42774218],[-8233371.672752922,4957588.494457528],[-8233364.916214473,4957584.116268433],[-8233344.135073014,4957570.650933055],[-8233303.102213988,4957545.830481292],[-8233266.035303376,4957525.584846684],[-8233133.132079316,4957451.875455639],[-8233010.867512913,4957274.493937394],[-8233006.542975529,4957268.484392206],[-8232999.680661071,4957258.948179955],[-8232992.144510226,4957247.416751844],[-8232989.111800923,4957242.776161908],[-8232947.777410295,4957180.596373461],[-8232874.426837963,4957070.2766175745],[-8232867.480653579,4957059.829325414],[-8232864.0724273445,4957054.40905274],[-8232773.081369861,4956921.751039523],[-8232711.6333547365,4957032.303703195],[-8232655.5816309415,4956945.882051846],[-8232530.592630487,4956759.216459876],[-8232470.841461961,4956725.621526833],[-8232310.254408663,4956636.545320823],[-8232243.92140533,4956719.301412085],[-8232185.663606981,4956794.602620221],[-8232179.72175656,4956802.432723733],[-8232115.327367794,4956888.0453047445],[-8232061.003109267,4956981.903981877],[-8232035.937456776,4957023.855042329],[-8232035.922247781,4957023.8814938385],[-8232035.907185419,4957023.909074547],[-8232012.394188708,4957066.82736883],[-8231988.858589811,4957113.741661194],[-8231967.094813363,4957161.505391474],[-8231947.146117049,4957210.014680816],[-8231929.048150872,4957259.16736163],[-8231893.336537221,4957359.099933371],[-8231854.520931445,4957467.852008518],[-8231816.570393179,4957576.135975455],[-8231781.296184151,4957675.7009340795],[-8231746.418951876,4957774.370863457],[-8231711.029081722,4957874.876723165],[-8231675.916579043,4957974.193470928],[-8231639.780965986,4958075.474832738]]]},"properties":{"shape_area":10734805.3205,"ntaname":"Manhattanville-West Harlem","cdtaname":"MN09 Morningside Heights-Hamilton Heights (CD 9 Equivalent)","shape_leng":17357.2462026,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0902","borocode":1,"countyfips":"061","ntaabbrev":"Mnhttnvl","cdta2020":"MN09"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8231001.713941371,4959422.912867843],[-8231082.869932147,4959468.200253735],[-8231115.433704664,4959486.371511813],[-8231294.843612019,4959585.612200347],[-8231599.822763671,4959754.6596903615],[-8231611.468499395,4959761.115364601],[-8231622.78190957,4959767.128558024],[-8231878.860141555,4959919.967036216],[-8231954.497432433,4959951.623641387],[-8231962.119770994,4959949.707936634],[-8232003.009801983,4959939.431764095],[-8232021.497018121,4959948.320205823],[-8232039.689201097,4959957.444037893],[-8232053.187221922,4959964.0597567335],[-8232060.328569376,4959968.33950008],[-8232068.7674158625,4959971.145149687],[-8232085.002295366,4959978.958135298],[-8232099.964506397,4959986.479716613],[-8232108.049891416,4959979.761878552],[-8232114.203204047,4959966.635489587],[-8232122.870076734,4959954.627898689],[-8232126.226137857,4959948.204530576],[-8232130.145124009,4959933.959477267],[-8232133.502411223,4959925.30123302],[-8232139.656285848,4959911.057373449],[-8232152.238723619,4959890.950698435],[-8232157.2720298385,4959882.293525902],[-8232166.217945939,4959870.844833171],[-8232172.649665522,4959859.115496379],[-8232182.439522065,4959837.051865712],[-8232188.597141986,4959816.103614275],[-8232192.238390144,4959798.785608787],[-8232193.4796195505,4959784.122003752],[-8232204.130218172,4959763.77260095],[-8232209.018084874,4959742.603083098],[-8232222.998728021,4959719.2150215935],[-8232249.216068593,4959668.597022506],[-8232260.751989059,4959645.556914816],[-8232277.71260099,4959623.181123799],[-8232289.414184355,4959593.194634697],[-8232319.685559986,4959542.983697301],[-8232407.652521543,4959369.033050627],[-8232476.1243212065,4959223.580186572],[-8232516.508561927,4959148.491981491],[-8232520.080252602,4959142.359953937],[-8232523.967196067,4959136.419322252],[-8232525.264111523,4959134.57909448],[-8232528.425755122,4959130.432983861],[-8232536.816274286,4959120.361435273],[-8232540.491496551,4959115.592664141],[-8232545.72683168,4959107.767036646],[-8232551.924299751,4959097.372464978],[-8232561.772851048,4959079.982655428],[-8232570.73633414,4959063.679173663],[-8232573.338973422,4959058.945114955],[-8232573.352267782,4959058.921307572],[-8232581.944000579,4959042.406005227],[-8232599.252575484,4959050.746085079],[-8232637.297524159,4958976.744995198],[-8232769.190395153,4959043.667883637],[-8232766.9410950085,4959047.609983471],[-8232766.585706213,4959048.231431775],[-8232779.643462364,4959054.5314755635],[-8232849.865849882,4958916.23803557],[-8232884.408981225,4958933.348492764],[-8232904.2366908435,4958893.961767437],[-8233077.755158562,4958549.268296079],[-8233080.015098455,4958550.405555287],[-8233093.8467629645,4958523.061121683],[-8232901.29871245,4958370.05462141],[-8232880.296837241,4958363.091672646],[-8232872.487156013,4958361.304930851],[-8232864.780290912,4958359.514452957],[-8232836.460948276,4958354.170349189],[-8232801.555454489,4958347.583197414],[-8232782.12128423,4958343.509268133],[-8232718.587759012,4958315.958796705],[-8232714.28821735,4958314.04450749],[-8232710.487639068,4958311.87854792],[-8232495.937696424,4958192.041258359],[-8232485.001719677,4958186.010191765],[-8232474.047382688,4958179.908038683],[-8232400.316092519,4958139.032236682],[-8232168.605648763,4958011.690972054],[-8232118.323295988,4958102.203310838],[-8232067.654920934,4958194.037488156],[-8231913.850770205,4958108.358634576],[-8231862.933287839,4958199.270128915],[-8231768.92944204,4958147.390588771],[-8231639.780965986,4958075.474832738],[-8231537.563203392,4958019.179924468],[-8231475.624689141,4958104.601127197],[-8231413.97794371,4958191.001012126],[-8231353.429152322,4958275.430660026],[-8231323.358886003,4958318.452023232],[-8231288.610539334,4958368.165999707],[-8231233.604287957,4958463.208270561],[-8231180.8563091615,4958557.769814112],[-8231130.4764615465,4958648.176776565],[-8231079.857436052,4958739.219903764],[-8231029.353812792,4958830.221691669],[-8230978.682579066,4958921.827011205],[-8230928.587595026,4959012.377596035],[-8230879.532305132,4959106.137507703],[-8230847.4157242095,4959207.255680816],[-8230811.784364378,4959317.544049265],[-8230851.544086255,4959339.524124533],[-8230881.454697519,4959355.598182899],[-8230900.174378518,4959366.968582712],[-8230921.482217745,4959378.652205914],[-8230978.879947484,4959410.330517514],[-8231001.713941371,4959422.912867843]]]},"properties":{"shape_area":15984383.2347,"ntaname":"Hamilton Heights-Sugar Hill","cdtaname":"MN09 Morningside Heights-Hamilton Heights (CD 9 Equivalent)","shape_leng":17103.3628796,"boroname":"Manhattan","ntatype":0,"nta2020":"MN0903","borocode":1,"countyfips":"061","ntaabbrev":"HmltnHts","cdta2020":"MN09"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8231269.686577193,4955801.620575735],[-8231215.0776062645,4955900.029853137],[-8231160.34071629,4955998.66516208],[-8231510.397859,4956192.712892625],[-8231519.03493739,4956197.500765209],[-8231526.540423993,4956201.649346753],[-8231824.745846635,4956366.504003663],[-8231835.774019734,4956372.570600563],[-8231845.14108628,4956377.814956767],[-8232152.014160721,4956548.653465007],[-8232310.25440867,4956636.545320823],[-8232364.801485517,4956537.279565896],[-8232419.063375542,4956438.933717078],[-8232441.087885594,4956407.509791075],[-8232474.397862,4956348.480018356],[-8232629.220324238,4956434.607213671],[-8232680.780836067,4956341.514248785],[-8232731.367558726,4956249.667029679],[-8232782.659222311,4956157.529237207],[-8232785.439000824,4956152.542866033],[-8232794.453326388,4956136.373271504],[-8232797.6607507905,4956130.619892116],[-8232833.922067724,4956065.57546829],[-8232885.170951049,4955973.345076905],[-8232887.316183325,4955969.47384428],[-8232933.104903358,4955886.845518172],[-8232935.92647332,4955881.753679559],[-8232988.008609721,4955781.812261704],[-8232992.19734749,4955771.365763521],[-8232995.449032087,4955760.590740547],[-8232997.738356469,4955749.571063442],[-8232999.047429199,4955738.392525917],[-8232999.366138983,4955727.142125686],[-8232998.692008509,4955715.907432217],[-8232995.267093703,4955650.705544853],[-8232993.919912022,4955622.666025412],[-8232993.03499243,4955604.2439266415],[-8232989.498745838,4955530.634537294],[-8232994.5943471575,4955415.226793075],[-8233043.531550438,4955321.954714805],[-8233094.629240548,4955230.244693046],[-8233150.226965148,4955129.555382049],[-8233059.719680265,4955079.906783893],[-8233020.457465528,4955058.3687433405],[-8232986.154989603,4955045.486897292],[-8232951.871074988,4955035.632663621],[-8232673.742013516,4954881.988684503],[-8232666.742578247,4954878.190792732],[-8232661.621432349,4954875.363419119],[-8232657.761942266,4954873.231669385],[-8232384.395053914,4954721.683665762],[-8232358.190270063,4954706.691044204],[-8232350.293397986,4954702.699525672],[-8232340.637780649,4954697.906114968],[-8232181.23853414,4954609.04106511],[-8232048.261516793,4954534.903758419],[-8231990.992187567,4954501.945059545],[-8231972.280245458,4954535.415348475],[-8231941.316602384,4954591.422157806],[-8231888.353731024,4954686.106262713],[-8231876.6831378415,4954707.1462804265],[-8231870.830859443,4954717.696929084],[-8231864.958424165,4954728.28384371],[-8231859.416023163,4954738.275745693],[-8231836.649980425,4954779.318586043],[-8231833.292511083,4954785.371354018],[-8231826.672804147,4954797.305345712],[-8231821.484746333,4954806.658385451],[-8231789.1356117055,4954864.976914889],[-8231784.241572164,4954873.799787068],[-8231778.866675149,4954883.489458335],[-8231773.575442538,4954893.028410823],[-8231739.454261306,4954954.540856731],[-8231682.422021328,4955057.611065783],[-8231627.492466528,4955156.798610837],[-8231576.368089622,4955248.7718914375],[-8231525.4550566375,4955340.8237501765],[-8231493.003848578,4955400.07254459],[-8231475.124522261,4955432.595603259],[-8231487.381731678,4955439.463169872],[-8231588.582672041,4955496.164556144],[-8231645.68354224,4955528.156693231],[-8231596.847809398,4955621.562511704],[-8231546.424515405,4955712.548328983],[-8231495.4880090915,4955804.146799161],[-8231442.039604717,4955896.380001099],[-8231383.354386032,4955864.115434159],[-8231314.6631662855,4955826.348951008],[-8231281.022232714,4955807.852969338],[-8231269.686577193,4955801.620575735]]]},"properties":{"shape_area":14441227.3216,"ntaname":"Harlem (South)","cdtaname":"MN10 Harlem (CD 10 Equivalent)","shape_leng":16624.4627212,"boroname":"Manhattan","ntatype":0,"nta2020":"MN1001","borocode":1,"countyfips":"061","ntaabbrev":"Hrlm_S","cdta2020":"MN10"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8230308.258503607,4957824.508618514],[-8230276.308697421,4957815.798847216],[-8230276.393534642,4957831.266964199],[-8230276.318197471,4957849.302976325],[-8230292.1615963485,4957850.7436611485],[-8230293.745794628,4957885.68588367],[-8230293.335097915,4957890.358163696],[-8230295.309351275,4957939.927100963],[-8230295.666028144,4957948.66675767],[-8230298.196007182,4958010.06035262],[-8230299.195777148,4958029.950601931],[-8230300.35276053,4958056.521851549],[-8230300.183218039,4958062.797547317],[-8230303.030822082,4958118.864861631],[-8230303.841865228,4958138.682624955],[-8230303.50819584,4958141.985179306],[-8230304.076335612,4958154.95029884],[-8230305.37631787,4958182.8600673685],[-8230306.190377874,4958198.71396784],[-8230306.34949612,4958203.943099897],[-8230306.624041189,4958212.926625898],[-8230308.315258465,4958260.275516021],[-8230308.73109509,4958278.288093334],[-8230310.119761512,4958300.767691722],[-8230311.489833719,4958333.001030864],[-8230311.549150145,4958334.23385235],[-8230313.705256905,4958341.494736824],[-8230319.538701715,4958447.072288266],[-8230322.664655208,4958500.9759217715],[-8230325.606323707,4958540.845776086],[-8230327.92870404,4958549.262286278],[-8230328.105771231,4958571.660682373],[-8230331.493842663,4958657.148575578],[-8230334.910419316,4958743.368166303],[-8230336.106550889,4958744.3442388065],[-8230342.9264783645,4958838.171318747],[-8230346.914108383,4958893.032246345],[-8230348.989670592,4958921.592760604],[-8230342.427698629,4958922.442919],[-8230352.662783715,4959022.981371747],[-8230353.203635813,4959028.36840427],[-8230354.435457937,4959040.636504894],[-8230355.23143827,4959048.560595617],[-8230353.31396449,4959049.194782609],[-8230354.519824533,4959058.098960758],[-8230355.601580131,4959066.64463641],[-8230355.997463251,4959069.764878109],[-8230357.359917932,4959079.91227602],[-8230358.142305179,4959086.551222689],[-8230360.559307907,4959085.674130586],[-8230360.774184028,4959091.385885087],[-8230361.7612586785,4959093.252687323],[-8230361.100581236,4959095.998458929],[-8230360.550368664,4959097.644997214],[-8230361.537442941,4959099.51310187],[-8230360.54807248,4959100.719900349],[-8230361.311605072,4959106.87018489],[-8230362.077981091,4959110.824564341],[-8230361.746335117,4959113.240670812],[-8230362.9530271655,4959115.9865655815],[-8230362.072078341,4959118.730804381],[-8230362.838154035,4959121.477648388],[-8230362.506563088,4959125.431214012],[-8230363.380990118,4959129.714321211],[-8230363.156921193,4959136.412871901],[-8230363.0453,4959139.159065645],[-8230363.314813075,4959145.912224517],[-8230362.237869535,4959149.545859018],[-8230361.883341765,4959150.743882021],[-8230362.097229436,4959157.552370078],[-8230362.920118822,4959158.458256041],[-8230363.195649705,4959158.762011178],[-8230363.007671075,4959159.605126581],[-8230362.313795577,4959162.71380627],[-8230363.327776687,4959163.803354698],[-8230363.850026837,4959164.363263776],[-8230366.078095649,4959195.608885936],[-8230368.163871504,4959196.488048089],[-8230368.270166123,4959200.881387943],[-8230369.255286664,4959205.384957256],[-8230369.031817452,4959211.31552513],[-8230368.91824122,4959214.9393462315],[-8230367.544682402,4959216.310881883],[-8230368.0882652,4959223.559054989],[-8230369.8444458,4959225.756399531],[-8230370.612169715,4959226.307239053],[-8230371.049778686,4959228.8322436325],[-8230373.2458781935,4959228.614736512],[-8230374.012448409,4959232.459665921],[-8230371.266530516,4959233.5556002315],[-8230368.958081895,4959235.749890993],[-8230370.481059463,4959255.18904491],[-8230372.400839505,4959257.441856795],[-8230373.717934435,4959259.200664285],[-8230374.59029902,4959267.986691095],[-8230372.502182316,4959270.291954917],[-8230373.267553959,4959273.805603832],[-8230374.799057861,4959281.8240696415],[-8230376.003171967,4959286.5468863975],[-8230375.671916281,4959289.951452057],[-8230377.316352399,4959293.467137645],[-8230378.632307793,4959296.763433565],[-8230377.531734475,4959298.519169746],[-8230378.023426181,4959302.638332985],[-8230380.217269703,4959305.604062235],[-8230379.773111721,4959311.864449434],[-8230380.9797429405,4959314.830820888],[-8230382.185172942,4959317.795768213],[-8230383.501795383,4959320.10365417],[-8230383.170565194,4959321.859943309],[-8230384.377711722,4959322.520712107],[-8230384.156440576,4959325.37507531],[-8230387.7245672885,4959327.73870048],[-8230391.354770085,4959364.363514846],[-8230411.119270871,4959571.879931346],[-8230414.149327091,4959605.556931602],[-8230416.329802612,4959628.29280409],[-8230417.199715778,4959640.484068131],[-8230415.222749846,4959640.373026265],[-8230419.240376415,4959702.537038187],[-8230420.887671233,4959702.208396209],[-8230423.983512814,4959748.942516603],[-8230424.8981889,4959773.927982757],[-8230425.170313552,4959777.168246821],[-8230422.64316042,4959778.594332576],[-8230421.213154676,4959781.228876474],[-8230420.0053704595,4959781.448440156],[-8230420.329558097,4959787.489319114],[-8230419.668599167,4959790.673530481],[-8230419.887169368,4959793.090214912],[-8230419.006094285,4959795.94415429],[-8230419.443577189,4959798.690941159],[-8230420.101445855,4959799.679968621],[-8230420.428871241,4959802.974494695],[-8230420.747684495,4959816.373194481],[-8230422.06511542,4959817.692577993],[-8230421.294511745,4959819.339078397],[-8230419.426819506,4959821.3147061905],[-8230420.303041548,4959823.292440264],[-8230420.082655537,4959825.050204682],[-8230421.068196941,4959829.003731702],[-8230421.8349958975,4959830.871807181],[-8230421.943745276,4959833.617009955],[-8230420.403900425,4959836.69100656],[-8230420.951534405,4959838.668402123],[-8230420.618178134,4959843.280863384],[-8230421.2747481465,4959846.027825755],[-8230419.956564559,4959847.343984717],[-8230419.624102008,4959850.7488204455],[-8230419.06978882,4959857.887143357],[-8230418.111501127,4959891.877802637],[-8230417.339181593,4959895.83135597],[-8230417.335845087,4959900.334508902],[-8230415.83729026,4959922.354291948],[-8230414.297520216,4959925.537748373],[-8230415.061548713,4959930.920432645],[-8230411.24112404,4959973.036623544],[-8230410.029553656,4959978.527400763],[-8230410.573136811,4959985.776211694],[-8230409.958537745,4959999.559931251],[-8230405.872458688,4960029.649875455],[-8230403.4560875455,4960031.185681205],[-8230403.564053707,4960033.162888995],[-8230402.793837512,4960034.260388794],[-8230402.68123931,4960038.323971575],[-8230401.849647682,4960042.121846609],[-8230401.082872747,4960045.626678371],[-8230405.252858893,4960051.780108968],[-8230404.259964662,4960057.710695018],[-8230401.942691706,4960071.437736609],[-8230400.124052249,4960092.557512891],[-8230399.450539402,4960100.375709805],[-8230397.127023546,4960122.449322125],[-8230394.818773094,4960125.852784854],[-8230392.063644429,4960137.383526052],[-8230389.646346322,4960140.126838771],[-8230386.595538417,4960142.477216362],[-8230382.822971459,4960144.010896126],[-8230381.2832357595,4960148.339868016],[-8230381.420663011,4960151.273360552],[-8230382.112977597,4960159.515113915],[-8230379.453626279,4960166.636968112],[-8230379.031675553,4960170.687435626],[-8230376.933301753,4960174.736655901],[-8230373.998932224,4960176.271060022],[-8230370.385116182,4960180.468805983],[-8230370.2211417705,4960184.7888248535],[-8230366.659244575,4960188.272468458],[-8230364.154094339,4960188.981378498],[-8230362.056268796,4960192.611570535],[-8230362.094142189,4960193.384253536],[-8230361.998206984,4960194.151892738],[-8230361.771337338,4960194.891489773],[-8230361.420330204,4960195.580886376],[-8230360.955701689,4960196.19942832],[-8230360.391372005,4960196.728584106],[-8230359.744248413,4960197.152500296],[-8230359.033718618,4960197.4584763795],[-8230358.281070048,4960197.637345329],[-8230356.519698287,4960201.400969989],[-8230355.957976926,4960205.451346511],[-8230355.200047992,4960208.250934517],[-8230351.983528649,4960213.416818585],[-8230350.486215441,4960214.143555438],[-8230349.117734495,4960215.090842989],[-8230347.910315873,4960216.236371081],[-8230346.892396339,4960217.553160706],[-8230346.087949702,4960219.010199311],[-8230345.515922122,4960220.573171214],[-8230345.189785971,4960222.205265831],[-8230345.746015981,4960225.697790706],[-8230347.216185755,4960228.9946931945],[-8230345.592527592,4960232.395513917],[-8230365.931883969,4960242.542894588],[-8230370.253856637,4960244.699094438],[-8230390.516272967,4960214.223167239],[-8230402.404061455,4960200.000178049],[-8230413.6052867295,4960187.941238761],[-8230440.824776126,4960158.63725233],[-8230487.396665469,4960111.533789973],[-8230585.068907388,4960017.243241525],[-8230629.904984334,4959970.356164952],[-8230662.06698907,4959938.523515712],[-8230737.444643756,4959860.390318615],[-8230749.618491876,4959847.884462386],[-8230761.44671313,4959835.051247361],[-8230772.92050834,4959821.900223141],[-8230784.031296948,4959808.441203041],[-8230794.7708598245,4959794.684176316],[-8230805.131208062,4959780.639379729],[-8230826.627248747,4959746.129552473],[-8230846.107608918,4959710.093638459],[-8230858.253842583,4959684.176942888],[-8230863.704229737,4959672.55903426],[-8230868.558737337,4959660.679813735],[-8230872.804887763,4959648.569820936],[-8230877.273055511,4959635.416209814],[-8230886.780920939,4959602.636931277],[-8230890.780507975,4959590.883589857],[-8230895.687112202,4959579.479090741],[-8230901.4706689045,4959568.493305249],[-8230908.095740817,4959557.993538888],[-8230915.521735345,4959548.044119128],[-8230933.145662309,4959523.496407965],[-8230938.5924920915,4959516.610426175],[-8230943.520910647,4959509.880142988],[-8230948.046966534,4959502.872950892],[-8230952.155143045,4959495.612874649],[-8230955.81029115,4959486.998654028],[-8230958.884374013,4959478.160398422],[-8230961.363778009,4959469.137253548],[-8230962.864531176,4959463.479007543],[-8230970.8832891015,4959427.62203583],[-8230978.879947484,4959410.330517514],[-8230921.482217745,4959378.652205914],[-8230900.174378518,4959366.968582712],[-8230881.454697519,4959355.598182899],[-8230851.544086255,4959339.524124533],[-8230811.784364378,4959317.544049265],[-8230847.4157242095,4959207.255680816],[-8230879.532305132,4959106.137507703],[-8230928.587595026,4959012.377596035],[-8230978.682579066,4958921.827011205],[-8231029.353812792,4958830.221691669],[-8231079.857436052,4958739.219903764],[-8231130.4764615465,4958648.176776565],[-8231180.8563091615,4958557.769814112],[-8231233.604287957,4958463.208270561],[-8231288.610539334,4958368.165999707],[-8231323.358886003,4958318.452023232],[-8231353.429152322,4958275.430660026],[-8231413.97794371,4958191.001012126],[-8231475.624689141,4958104.601127197],[-8231537.563203392,4958019.179924468],[-8231639.780965986,4958075.474832738],[-8231675.916579043,4957974.193470928],[-8231711.029081722,4957874.876723165],[-8231746.418951876,4957774.370863457],[-8231781.296184151,4957675.7009340795],[-8231816.570393179,4957576.135975455],[-8231854.520931445,4957467.852008518],[-8231893.336537221,4957359.099933371],[-8231929.048150872,4957259.16736163],[-8231947.146117049,4957210.014680816],[-8231967.094813363,4957161.505391474],[-8231988.858589811,4957113.741661194],[-8232012.394188708,4957066.82736883],[-8232035.907185419,4957023.909074547],[-8232035.922247781,4957023.8814938385],[-8232035.937456776,4957023.855042329],[-8232061.003109267,4956981.903981877],[-8232115.327367794,4956888.0453047445],[-8232179.72175656,4956802.432723733],[-8232185.663606981,4956794.602620221],[-8232243.92140533,4956719.301412085],[-8232310.254408663,4956636.545320823],[-8232152.014160728,4956548.653465007],[-8231845.141086273,4956377.814956767],[-8231835.77401974,4956372.570600563],[-8231824.745846635,4956366.504003663],[-8231526.540423982,4956201.649346753],[-8231519.03493739,4956197.5007652175],[-8231510.397858993,4956192.71289263],[-8231160.340716282,4955998.66516208],[-8231109.804091291,4956090.13515732],[-8231058.954196161,4956181.1042107195],[-8231008.060822404,4956272.500046392],[-8230957.445144688,4956363.822516562],[-8230906.818780043,4956455.742096372],[-8230853.781471022,4956550.64494385],[-8230823.325782067,4956605.47954294],[-8230811.479979849,4956626.807173642],[-8230783.644840925,4956676.923254726],[-8230774.935799766,4956692.60334216],[-8230763.4894612385,4956713.429123952],[-8230755.59657221,4956727.789751862],[-8230697.891380479,4956826.650881751],[-8230645.218936535,4956926.38222484],[-8230638.278492244,4956938.850905339],[-8230594.520122631,4957017.3105037715],[-8230543.4327940205,4957106.066772049],[-8230532.959903655,4957126.6876404835],[-8230523.522438695,4957144.253908784],[-8230517.171384493,4957156.075098739],[-8230490.651585972,4957205.437146136],[-8230392.462948584,4957381.597633746],[-8230366.243218467,4957437.306922028],[-8230345.771439577,4957478.404897386],[-8230321.414078995,4957532.085412135],[-8230309.499691312,4957558.32590383],[-8230303.541024527,4957571.887329686],[-8230304.67437037,4957587.177194619],[-8230321.476975676,4957692.452304524],[-8230328.057889478,4957729.915391204],[-8230332.910459956,4957708.300362169],[-8230343.002983088,4957761.919882537],[-8230343.798327954,4957766.14530954],[-8230345.602328929,4957791.798581371],[-8230346.474906746,4957803.501170718],[-8230347.569770893,4957831.4402379505],[-8230348.135350971,4957846.043233424],[-8230339.804497213,4957841.689147628],[-8230330.04943266,4957836.133309502],[-8230308.258503607,4957824.508618514]]]},"properties":{"shape_area":23792907.5985,"ntaname":"Harlem (North)","cdtaname":"MN10 Harlem (CD 10 Equivalent)","shape_leng":31863.511594,"boroname":"Manhattan","ntatype":0,"nta2020":"MN1002","borocode":1,"countyfips":"061","ntaabbrev":"Hrlm_N","cdta2020":"MN10"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8230415.801832747,4953745.779688849],[-8230418.520272851,4953748.648045447],[-8230421.938427037,4953752.1098925965],[-8230428.103772654,4953758.725120696],[-8230433.336137123,4953763.979889851],[-8230438.876822657,4953769.730667116],[-8230450.991372187,4953784.019123953],[-8230468.125673415,4953765.203026043],[-8230606.046563693,4953847.476263359],[-8230711.135276,4953907.804681233],[-8230762.047063928,4953815.877436268],[-8230812.73786085,4953723.536520444],[-8231078.36136005,4953868.383833658],[-8231327.44346632,4954006.278607374],[-8231275.965074772,4954100.5131493015],[-8231224.736730403,4954191.89561637],[-8231171.989709653,4954287.185934512],[-8231222.201735709,4954314.611784517],[-8231239.135904568,4954323.861271072],[-8231269.665166582,4954340.5359348785],[-8231283.500129259,4954348.092338494],[-8231350.901715178,4954384.905921624],[-8231425.260505629,4954426.846605662],[-8231432.153582678,4954430.734394449],[-8231515.230235018,4954477.591281747],[-8231530.449672044,4954486.049394137],[-8231545.01484521,4954494.347452677],[-8231582.649621988,4954515.5378740085],[-8231593.979694526,4954521.917051831],[-8231709.0241728015,4954586.691543013],[-8231767.455659421,4954619.085258332],[-8231888.353731017,4954686.106262713],[-8231941.316602371,4954591.422157812],[-8231972.280245446,4954535.415348475],[-8231990.992187567,4954501.945059552],[-8232012.099179136,4954463.037278297],[-8232041.482061976,4954405.99776305],[-8232093.95619822,4954313.825007519],[-8232146.347855727,4954221.816173215],[-8232200.022684093,4954123.009468129],[-8232254.750750677,4954023.811980853],[-8232306.916898182,4953932.026572873],[-8232357.117119644,4953839.229045446],[-8232407.787430681,4953747.255553307],[-8232421.242603152,4953722.803893521],[-8232428.163669728,4953710.226280638],[-8232458.931188558,4953654.313108621],[-8232509.28460307,4953564.287454694],[-8232566.059868039,4953462.406666386],[-8232612.596515041,4953378.897382744],[-8232644.537710417,4953322.849512734],[-8232664.3709100215,4953288.04735641],[-8232719.476666497,4953189.839675919],[-8232539.964131714,4953089.314521172],[-8232489.723519503,4953061.490218572],[-8232456.663518574,4953043.180740603],[-8232372.606041411,4952996.672356166],[-8232360.370799427,4952989.870983532],[-8232350.66882215,4952984.283594686],[-8232324.52815186,4952969.801691419],[-8232220.693095882,4952912.466967091],[-8232179.6627385495,4952889.810849291],[-8232113.974601126,4952853.455728814],[-8232001.949971286,4952791.354838305],[-8231940.118297608,4952756.953806335],[-8231836.279170831,4952699.180325182],[-8231752.35082013,4952652.483390346],[-8231680.6001767265,4952612.687946675],[-8231622.367589188,4952580.412809783],[-8231488.871623072,4952506.421306921],[-8231480.41297264,4952502.071869369],[-8231414.482308178,4952464.944202727],[-8231403.373396297,4952457.791953179],[-8231396.543276233,4952453.721675034],[-8231389.7885612175,4952449.527544498],[-8231381.716337882,4952445.111095318],[-8231372.090454902,4952428.663470658],[-8231369.268862311,4952419.808812784],[-8231357.688587091,4952452.666618205],[-8231358.493568357,4952456.032664475],[-8231346.205774439,4952477.366768727],[-8231341.278273804,4952486.20080395],[-8231335.790544749,4952496.822934808],[-8231331.647250748,4952504.313548738],[-8231327.167368891,4952512.364533867],[-8231323.808530372,4952518.178299311],[-8231320.001178022,4952524.327995327],[-8231315.076819651,4952530.477067149],[-8231309.927514848,4952535.506244174],[-8231298.959122583,4952545.566811879],[-8231289.894815265,4952553.949823993],[-8231282.395512319,4952560.767133774],[-8231272.770540493,4952569.48607157],[-8231260.795106833,4952580.551005404],[-8231253.072768323,4952587.593588575],[-8231249.491399168,4952590.946290108],[-8231238.075422575,4952601.452419859],[-8231234.157820758,4952604.58202762],[-8231229.345906518,4952607.935194657],[-8231220.730171342,4952613.857621575],[-8231205.735125562,4952624.138360832],[-8231187.382056489,4952636.765788742],[-8231169.589683179,4952648.945125685],[-8231149.55841266,4952662.577618414],[-8231132.996982813,4952673.864871768],[-8231124.13061775,4952679.806934778],[-8231093.714610537,4952699.520132245],[-8231089.74580166,4952702.015282258],[-8231086.806105272,4952704.070649463],[-8231084.012956477,4952705.83078903],[-8231077.251434236,4952709.646803635],[-8231055.64255187,4952723.150225919],[-8231050.9447174445,4952726.082870428],[-8231048.587883266,4952727.554204188],[-8231046.529394916,4952729.0221327925],[-8231043.442179439,4952730.929619186],[-8231043.0017046975,4952730.929321099],[-8231038.150096044,4952734.306184768],[-8231032.564813699,4952738.121700294],[-8231028.888944723,4952740.617107602],[-8231027.85949829,4952741.645158512],[-8231024.479400943,4952743.553816666],[-8231022.568479892,4952745.315746674],[-8231013.600081138,4952751.774561232],[-8231003.457320158,4952758.967198231],[-8230992.872666882,4952766.307155652],[-8230989.638582758,4952768.508853876],[-8230988.168626564,4952769.68285608],[-8230985.669389455,4952771.738556721],[-8230983.757755267,4952772.765856792],[-8230979.788486479,4952775.84930542],[-8230975.8778857505,4952778.275966672],[-8230970.291211593,4952782.2602883],[-8230968.6959794145,4952783.356378413],[-8230965.9024668345,4952785.547676856],[-8230964.605744083,4952786.045316247],[-8230962.011590635,4952788.037624344],[-8230958.12170947,4952790.826948195],[-8230955.328391891,4952792.6199679775],[-8230953.134154506,4952794.113997016],[-8230950.7394265365,4952795.807112864],[-8230949.8416336905,4952797.102904673],[-8230946.749225744,4952798.795443078],[-8230945.65249228,4952799.890671328],[-8230944.730329435,4952800.265890692],[-8230943.801589302,4952800.64375525],[-8230881.488220113,4952893.233735298],[-8230800.553035989,4953013.625996255],[-8230767.805758171,4953062.0002035005],[-8230762.139233017,4953070.240435499],[-8230755.378319139,4953080.267034743],[-8230753.390038018,4953083.045803624],[-8230737.372995117,4953119.237747511],[-8230735.980432719,4953122.812349888],[-8230734.488836277,4953124.698519788],[-8230734.687051265,4953125.890883436],[-8230720.16406787,4953156.818964882],[-8230715.311163904,4953170.716600621],[-8230711.703211369,4953181.048924729],[-8230706.62798443,4953196.191490646],[-8230705.6331924,4953198.573995803],[-8230699.859637008,4953216.249589385],[-8230690.404438024,4953244.004255296],[-8230675.583934219,4953287.359714489],[-8230673.483259585,4953293.505072261],[-8230654.671999935,4953348.368766487],[-8230645.8502573585,4953373.581304096],[-8230544.925629156,4953318.9977673255],[-8230543.197843983,4953318.974156987],[-8230542.205587383,4953319.689577715],[-8230531.996859393,4953338.198287124],[-8230532.160523245,4953339.906362411],[-8230533.537633089,4953341.450585088],[-8230630.837962002,4953395.9658302115],[-8230637.257284476,4953399.563113159],[-8230638.212539367,4953402.573529867],[-8230635.095738844,4953408.435216968],[-8230636.057800136,4953409.48580482],[-8230595.018158771,4953482.639495714],[-8230553.505629634,4953557.763587298],[-8230520.533857582,4953616.815034618],[-8230499.065854897,4953655.713765598],[-8230466.549328144,4953688.521214786],[-8230458.607119379,4953680.459307371],[-8230427.746248234,4953710.852913823],[-8230436.052343976,4953718.850656193],[-8230436.396309471,4953719.182017793],[-8230425.679218308,4953730.418279921],[-8230418.534777953,4953736.884807335],[-8230412.569747764,4953742.35443011],[-8230415.801832747,4953745.779688849]]]},"properties":{"shape_area":16653940.2784,"ntaname":"East Harlem (South)","cdtaname":"MN11 East Harlem (CD 11 Equivalent)","shape_leng":23396.960257,"boroname":"Manhattan","ntatype":0,"nta2020":"MN1101","borocode":1,"countyfips":"061","ntaabbrev":"EstHrlm_S","cdta2020":"MN11"}}, +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8229538.593401606,4954577.346527618],[-8229556.822959423,4954571.497741968],[-8229558.590746406,4954575.92770988],[-8229594.692216723,4954564.443503493],[-8229590.716434081,4954552.483401876],[-8229554.615113429,4954563.746135033],[-8229555.49808743,4954567.068167604],[-8229536.606382213,4954571.9484359855],[-8229538.593401606,4954577.346527618]]],[[[-8229464.617910585,4955009.063402075],[-8229484.927905744,4955020.828191332],[-8229485.412198881,4955019.82142334],[-8229486.700984975,4955020.426814986],[-8229487.346571068,4955019.258998826],[-8229466.29346428,4955006.320257185],[-8229464.617910585,4955009.063402075]]],[[[-8229478.776737279,4954986.916452922],[-8229534.845628726,4955019.60394133],[-8229523.450527058,4955039.6962471865],[-8229528.757797491,4955042.735049674],[-8229543.570590817,4955017.714763573],[-8229482.124182317,4954981.681291714],[-8229478.776737279,4954986.916452922]]],[[[-8229461.399082359,4955056.398953739],[-8229503.312156047,4955083.297053809],[-8229521.161430345,4955055.624241496],[-8229515.474123354,4955053.343840174],[-8229501.422682964,4955074.951259281],[-8229497.252723073,4955072.292782497],[-8229498.773000964,4955068.501274224],[-8229466.171136142,4955048.751301171],[-8229461.399082359,4955056.398953739]]],[[[-8230415.801832747,4953745.779688849],[-8230412.569698668,4953742.354466882],[-8230377.476913544,4953773.777703904],[-8230352.052615115,4953795.825442664],[-8230333.117434657,4953812.9147326],[-8230325.535383959,4953820.142763108],[-8230303.022143534,4953840.79195348],[-8230282.748124766,4953860.006681231],[-8230264.942837097,4953876.411684847],[-8230237.8912446555,4953901.706445932],[-8230214.115973239,4953920.058389937],[-8230211.283381602,4953922.230975823],[-8230195.8053116305,4953934.100005872],[-8230170.234609306,4953953.862643845],[-8230147.443586425,4953971.143800751],[-8230132.541829503,4953981.169837035],[-8230129.278270186,4953983.399082704],[-8230111.69860648,4953995.401688303],[-8230081.372007626,4954017.762910538],[-8230073.708145336,4954023.472167871],[-8230058.678583129,4954034.668050675],[-8230049.541867405,4954041.411665024],[-8230034.5455572475,4954052.218454611],[-8230008.87244649,4954070.710630521],[-8229973.973215473,4954095.39363287],[-8229896.686803243,4954150.229999185],[-8229871.844648813,4954168.151624893],[-8229861.944600401,4954185.694745395],[-8229857.614280443,4954183.913225742],[-8229841.092759935,4954214.503987158],[-8229842.980553039,4954215.839049105],[-8229840.643718745,4954220.281102454],[-8229838.978009373,4954219.945995015],[-8229837.420497752,4954222.944131749],[-8229831.312832131,4954220.273632655],[-8229828.753939286,4954225.269271133],[-8229833.417163284,4954227.940088893],[-8229832.415686477,4954229.937740254],[-8229836.023578792,4954232.884631698],[-8229821.673213165,4954258.312017716],[-8229814.659668514,4954271.126736334],[-8229797.698814692,4954302.116396138],[-8229772.891509896,4954346.420319896],[-8229769.733893933,4954352.701661105],[-8229744.714045321,4954475.934812102],[-8229742.687011721,4954485.919020787],[-8229735.429970384,4954705.096881444],[-8229732.397492191,4954800.712161247],[-8229729.665305517,4954880.781540245],[-8229727.672495662,4954938.941307325],[-8229731.692603545,4954985.177491326],[-8229733.457005251,4955005.469797013],[-8229737.097269921,4955048.355415525],[-8229738.629141152,4955064.172628819],[-8229739.512187756,4955073.29186337],[-8229740.4656630615,4955081.841988377],[-8229740.998819527,4955091.342933877],[-8229741.608652225,4955102.225404867],[-8229742.520173655,4955118.492282424],[-8229751.813763626,4955123.194556622],[-8229761.808897746,4955128.251050508],[-8229769.2340102615,4955161.309734313],[-8229772.276047787,4955179.2712694695],[-8229775.511632264,4955194.367672456],[-8229778.082029084,4955204.973197693],[-8229787.230398631,4955232.874393195],[-8229789.136427419,4955239.5627427315],[-8229796.569892082,4955260.967039196],[-8229802.859344262,4955279.505136389],[-8229815.724955377,4955317.631731942],[-8229826.207673099,4955349.928288752],[-8229837.651408948,4955374.775089824],[-8229839.945177023,4955379.7842369955],[-8229848.049033208,4955397.393217425],[-8229850.990831912,4955405.5064496035],[-8229854.920129815,4955415.171802966],[-8229847.57453303,4955427.496942922],[-8229847.030672351,4955428.408526154],[-8229851.358422745,4955441.74418983],[-8229856.656552444,4955452.6068364885],[-8229870.35859997,4955483.70788709],[-8229871.300371534,4955485.058290725],[-8229879.08821031,4955503.0230406355],[-8229880.968588564,4955507.21066987],[-8229884.325863451,4955514.775139857],[-8229887.01490457,4955519.638662453],[-8229889.3002059795,4955523.827110077],[-8229895.079889212,4955534.501114363],[-8229899.2468544105,4955542.336377094],[-8229910.337719653,4955561.859867233],[-8229913.70006962,4955567.129987707],[-8229913.697232944,4955568.479576458],[-8229917.863800938,4955576.450747553],[-8229920.285438619,4955579.829554843],[-8229921.090637196,4955582.125685611],[-8229926.4678800125,4955591.583576448],[-8229927.407694272,4955593.879914142],[-8229929.023136781,4955596.041805172],[-8229929.692850296,4955598.203046601],[-8229939.709153225,4955615.564656413],[-8229942.128359272,4955620.832603061],[-8229951.184414235,4955636.863491603],[-8229953.959210594,4955641.77490792],[-8229956.646520908,4955646.774412666],[-8229958.530142189,4955649.477850045],[-8229958.662874881,4955650.962416651],[-8229964.377137255,4955660.757657786],[-8229965.047672814,4955662.51384157],[-8229969.484009718,4955670.215142787],[-8229972.172955621,4955675.214659109],[-8229973.97419445,4955678.609418046],[-8229975.39889978,4955681.294461479],[-8229979.029282177,4955687.778870306],[-8229991.2713684905,4955705.888227467],[-8229995.239106531,4955711.902384446],[-8230004.788740119,4955726.766840674],[-8230006.808196893,4955728.7950138645],[-8230009.362285771,4955733.928912438],[-8230011.246746378,4955736.227266606],[-8230013.268041781,4955737.446669583],[-8230014.205801729,4955740.687720299],[-8230016.492971992,4955744.066209935],[-8230021.851048315,4955750.897480179],[-8230116.02698672,4955882.947847885],[-8230142.822866505,4955920.12934859],[-8230176.441421426,4955966.730430747],[-8230193.343527726,4955989.569341165],[-8230203.297482582,4956006.926640994],[-8230204.390751708,4956008.509820329],[-8230212.755187079,4956020.383721638],[-8230220.483901061,4956032.353282096],[-8230246.38447723,4956073.628429365],[-8230308.672451124,4956228.306574117],[-8230310.296758071,4956240.01821341],[-8230317.094562611,4956283.305762316],[-8230319.044681066,4956287.308707329],[-8230332.930193168,4956361.00881092],[-8230333.17107379,4956414.53915166],[-8230330.231722521,4956472.3645207165],[-8230329.092802552,4956495.718611813],[-8230326.655354719,4956545.7356599085],[-8230317.005009852,4956629.267015926],[-8230310.49578848,4956676.033349434],[-8230305.592471927,4956713.5459059635],[-8230299.511339692,4956768.119730844],[-8230296.089698093,4956807.33026426],[-8230293.2051672945,4956837.532813902],[-8230287.422802536,4956915.634877204],[-8230281.019648401,4957020.480150889],[-8230280.68045544,4957026.034733268],[-8230280.21560211,4957033.651383513],[-8230280.08334052,4957039.450556053],[-8230279.904698978,4957047.737532026],[-8230279.260826828,4957081.756495328],[-8230296.876194836,4957082.09030017],[-8230294.254900648,4957088.630089628],[-8230291.47076907,4957094.025627625],[-8230292.610809346,4957099.259374324],[-8230293.589194124,4957103.512564991],[-8230293.256204186,4957111.199282524],[-8230293.090140607,4957114.469999687],[-8230294.724622237,4957116.596810664],[-8230294.06813316,4957119.376290405],[-8230292.757027656,4957122.318949897],[-8230294.227664235,4957124.774211298],[-8230292.2630263725,4957126.571114542],[-8230293.240427255,4957132.133254756],[-8230292.419844642,4957135.567010943],[-8230293.398603843,4957139.3281197455],[-8230293.232662087,4957142.436621933],[-8230294.048906804,4957144.890020637],[-8230293.63594147,4957150.040763124],[-8230291.827445233,4957163.449977736],[-8230291.169718698,4957167.86570998],[-8230291.657064659,4957172.44575971],[-8230291.163060397,4957176.697921604],[-8230289.360556186,4957180.456949586],[-8230289.521935195,4957183.402038244],[-8230289.027782545,4957187.816454485],[-8230290.169675552,4957192.233554602],[-8230290.002622738,4957196.813097886],[-8230289.50874088,4957200.900320338],[-8230289.505410931,4957205.316489198],[-8230289.502821196,4957208.750985861],[-8230290.400031966,4957212.2679254655],[-8230289.252710744,4957214.883461796],[-8230289.578677402,4957216.519882379],[-8230288.433222911,4957218.644620122],[-8230289.246914228,4957222.570513711],[-8230287.936107175,4957226.822105895],[-8230290.060106824,4957228.9493113365],[-8230289.895272777,4957230.585290563],[-8230288.747973168,4957233.36443236],[-8230288.746246846,4957235.653636871],[-8230289.726616393,4957237.290529869],[-8230289.56153641,4957239.252410736],[-8230288.742546364,4957240.560696972],[-8230289.2321155425,4957242.195772479],[-8230289.230881704,4957243.832024559],[-8230276.19766171,4957243.500214789],[-8230273.625065212,4957379.347842213],[-8230271.852321632,4957472.960539265],[-8230272.227720522,4957564.5746820485],[-8230272.990089032,4957648.55776214],[-8230275.878456517,4957796.065904705],[-8230276.060223411,4957804.72083257],[-8230276.308734295,4957815.7987613315],[-8230308.258503607,4957824.508618514],[-8230330.04943266,4957836.133309502],[-8230339.804497213,4957841.689147628],[-8230348.135350971,4957846.043233424],[-8230347.569770893,4957831.4402379505],[-8230346.474906746,4957803.501170718],[-8230345.602328929,4957791.798581371],[-8230343.798327954,4957766.14530954],[-8230343.002983088,4957761.919882537],[-8230332.910459956,4957708.300362169],[-8230328.057889478,4957729.915391204],[-8230321.476975676,4957692.452304524],[-8230304.67437037,4957587.177194619],[-8230303.541024527,4957571.887329686],[-8230309.499691312,4957558.32590383],[-8230321.414078995,4957532.085412135],[-8230345.771439577,4957478.404897386],[-8230366.243218467,4957437.306922028],[-8230392.462948584,4957381.597633746],[-8230490.651585972,4957205.437146136],[-8230517.171384493,4957156.075098739],[-8230523.522438695,4957144.253908784],[-8230532.959903655,4957126.6876404835],[-8230543.4327940205,4957106.066772049],[-8230594.520122631,4957017.3105037715],[-8230638.278492244,4956938.850905339],[-8230645.218936535,4956926.38222484],[-8230697.891380479,4956826.650881751],[-8230755.59657221,4956727.789751862],[-8230763.4894612385,4956713.429123952],[-8230774.935799766,4956692.60334216],[-8230783.644840925,4956676.923254726],[-8230811.479979849,4956626.807173642],[-8230823.325782067,4956605.47954294],[-8230853.781471022,4956550.64494385],[-8230906.818780043,4956455.742096372],[-8230957.445144688,4956363.822516562],[-8231008.060822404,4956272.500046392],[-8231058.954196161,4956181.1042107195],[-8231109.804091291,4956090.13515732],[-8231160.340716282,4955998.66516208],[-8231215.077606247,4955900.029853137],[-8231269.686577193,4955801.620575735],[-8231281.0222327085,4955807.852969341],[-8231314.663166292,4955826.348951012],[-8231383.3543860195,4955864.115434165],[-8231442.039604704,4955896.380001099],[-8231495.4880090915,4955804.146799161],[-8231546.424515385,4955712.548328987],[-8231596.847809386,4955621.562511704],[-8231645.683542224,4955528.156693231],[-8231588.582672033,4955496.1645561475],[-8231487.381731672,4955439.463169872],[-8231475.124522267,4955432.595603264],[-8231493.003848573,4955400.07254459],[-8231525.4550566245,4955340.823750181],[-8231576.36808961,4955248.7718914375],[-8231627.4924665205,4955156.798610844],[-8231682.422021328,4955057.611065783],[-8231739.454261311,4954954.540856731],[-8231773.57544253,4954893.028410823],[-8231778.86667513,4954883.489458341],[-8231784.241572155,4954873.799787068],[-8231789.135611711,4954864.976914889],[-8231821.484746329,4954806.658385456],[-8231826.672804143,4954797.305345718],[-8231833.292511076,4954785.371354023],[-8231836.649980429,4954779.31858605],[-8231859.416023151,4954738.275745693],[-8231864.958424159,4954728.28384371],[-8231870.830859449,4954717.696929084],[-8231876.6831378415,4954707.146280432],[-8231888.353731017,4954686.106262713],[-8231767.455659421,4954619.085258332],[-8231709.0241728015,4954586.691543013],[-8231593.979694526,4954521.917051831],[-8231582.649621988,4954515.5378740085],[-8231545.01484521,4954494.347452677],[-8231530.449672044,4954486.049394137],[-8231515.230235018,4954477.591281747],[-8231432.153582678,4954430.734394449],[-8231425.260505629,4954426.846605662],[-8231350.901715178,4954384.905921624],[-8231283.500129259,4954348.092338494],[-8231269.665166582,4954340.5359348785],[-8231239.135904568,4954323.861271072],[-8231222.201735709,4954314.611784517],[-8231171.989709653,4954287.185934512],[-8231224.736730403,4954191.89561637],[-8231275.965074772,4954100.5131493015],[-8231327.44346632,4954006.278607374],[-8231078.36136005,4953868.383833658],[-8230812.73786085,4953723.536520444],[-8230762.047063928,4953815.877436268],[-8230711.135276,4953907.804681233],[-8230606.046563693,4953847.476263359],[-8230468.125673415,4953765.203026043],[-8230450.991372187,4953784.019123953],[-8230438.876822657,4953769.730667116],[-8230433.336137123,4953763.979889851],[-8230428.103772654,4953758.725120696],[-8230421.938427037,4953752.1098925965],[-8230418.520272851,4953748.648045447],[-8230415.801832747,4953745.779688849]]]]},"properties":{"shape_area":26104503.6338,"ntaname":"East Harlem (North)","cdtaname":"MN11 East Harlem (CD 11 Equivalent)","shape_leng":38698.0510642,"boroname":"Manhattan","ntatype":0,"nta2020":"MN1102","borocode":1,"countyfips":"061","ntaabbrev":"EstHrlm_N","cdta2020":"MN11"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8229538.593401606,4954577.346527618],[-8229536.6063331,4954571.948485016],[-8229529.139157885,4954550.556786491],[-8229520.27675822,4954519.053461019],[-8229510.643882206,4954477.985417477],[-8229503.714916607,4954411.128380334],[-8229503.82512477,4954408.5668580355],[-8229503.8914589435,4954407.032040799],[-8229505.41225606,4954371.84840704],[-8229507.448676392,4954324.754849267],[-8229507.747257951,4954312.624323708],[-8229508.887764488,4954300.543856809],[-8229510.8646829035,4954288.571811616],[-8229513.668459229,4954276.766026942],[-8229517.285544908,4954265.183537943],[-8229521.698462421,4954253.880300626],[-8229526.885889534,4954242.910921501],[-8229543.131674747,4954195.389424983],[-8229545.002584056,4954188.161817317],[-8229555.327341879,4954148.915070379],[-8229656.838246042,4953965.964828127],[-8229660.189098077,4953958.391294252],[-8229663.239956205,4953949.722979216],[-8229664.6500487635,4953940.805954813],[-8229665.400778619,4953931.809403236],[-8229665.488084708,4953922.78201366],[-8229664.911496337,4953913.772641879],[-8229657.060164452,4953836.431762413],[-8229627.374854434,4953807.886890198],[-8229605.208806016,4953787.943148942],[-8229531.094947298,4953777.957817918],[-8229541.91778925,4953757.753946481],[-8229542.117718047,4953757.609365968],[-8229546.791770545,4953754.23249871],[-8229551.691159653,4953750.692888611],[-8229580.372191299,4953709.2879257575],[-8229589.447524525,4953692.862770063],[-8229615.487012836,4953655.187535078],[-8229617.35624063,4953647.576305441],[-8229616.409082349,4953643.280608576],[-8229615.436064535,4953638.744231573],[-8229612.720436982,4953635.2276588045],[-8229603.281407397,4953633.289321848],[-8229588.708592382,4953635.749858507],[-8229578.46890451,4953636.499840173],[-8229572.780695338,4953636.116035293],[-8229564.817062985,4953636.109441847],[-8229558.365842766,4953641.413131889],[-8229551.159317755,4953642.924043932],[-8229542.8173497,4953641.400229563],[-8229534.474540176,4953641.39329638],[-8229528.783854762,4953644.043168123],[-8229518.167360887,4953641.75897067],[-8229509.824305683,4953641.752016192],[-8229496.931927406,4953640.224464828],[-8229486.925345891,4953638.052044345],[-8229482.903009869,4953637.178787453],[-8229467.66631242,4953638.840271491],[-8229459.0035218755,4953645.11226833],[-8229452.158901577,4953657.988460375],[-8229443.027545955,4953678.062713346],[-8229441.447735322,4953715.586220241],[-8229445.578029303,4953738.715630835],[-8229461.09174187,4953755.0413292255],[-8229469.046593162,4953758.08728552],[-8229475.870259061,4953757.719762943],[-8229482.683060429,4953763.796163186],[-8229486.828835086,4953777.828328555],[-8229478.884495342,4953779.55203105],[-8229474.694514163,4953780.461196081],[-8229452.327602038,4953781.93947125],[-8229423.533659697,4953773.551480621],[-8229360.9900935665,4953773.065846563],[-8229357.967958571,4953766.995864654],[-8229358.734621996,4953762.069337447],[-8229364.108118541,4953762.263575487],[-8229378.434073686,4953767.404726353],[-8229396.620282646,4953770.083520843],[-8229420.8796371985,4953765.949902842],[-8229435.652618406,4953770.518790115],[-8229461.796773695,4953773.208967395],[-8229467.868285909,4953767.5332570085],[-8229465.601155822,4953762.982472009],[-8229453.859921139,4953758.4183392655],[-8229434.920246957,4953752.706581769],[-8229422.423214682,4953746.625266992],[-8229422.443401326,4953732.603906993],[-8229421.700790993,4953721.99195823],[-8229417.181656959,4953702.279833875],[-8229414.317165951,4953693.6453319695],[-8229410.012595473,4953680.669023445],[-8229409.652102297,4953667.783683441],[-8229414.596525099,4953655.285495945],[-8229414.236026658,4953642.400385953],[-8229411.976569252,4953632.544373312],[-8229411.620449641,4953616.627759257],[-8229416.264950352,4953602.531380731],[-8229432.98654255,4953597.101380942],[-8229451.24582724,4953596.931726281],[-8229478.942453788,4953602.957188772],[-8229494.648908889,4953607.3403326245],[-8229507.377805533,4953610.892719389],[-8229557.547913612,4953614.134501245],[-8229635.412272576,4953615.974768933],[-8229693.505691445,4953636.998001807],[-8229707.408543973,4953633.57183896],[-8229812.785372781,4953693.954175706],[-8229819.230939471,4953695.476344181],[-8229825.95713977,4953691.858017863],[-8229931.151295024,4953635.268177051],[-8229943.661592428,4953640.587293733],[-8230014.256642603,4953565.555991086],[-8230084.314109874,4953471.054623443],[-8230099.391537296,4953450.680739762],[-8230117.283036047,4953454.406918233],[-8230150.078371066,4953375.012864439],[-8230198.210005182,4953306.461329041],[-8230485.884599138,4952862.732226517],[-8230491.58283823,4952828.395419543],[-8230494.878491672,4952800.384537103],[-8230508.769631784,4952734.786953373],[-8230518.173063498,4952715.526783276],[-8230523.80757396,4952691.6543422155],[-8230525.735587071,4952648.428087695],[-8230515.896392819,4952608.175757394],[-8230497.054175358,4952574.156577868],[-8230451.709883558,4952527.939049231],[-8230409.162318742,4952471.098823933],[-8230147.267122181,4952425.297442381],[-8229967.232384202,4952381.795049964],[-8229920.997930078,4952361.998038713],[-8229884.450174549,4952335.040021473],[-8229757.951820435,4952237.394670948],[-8229682.16181808,4952178.490115099],[-8229638.579672101,4952165.597837257],[-8229603.656071682,4952175.781541481],[-8229593.530487847,4952179.431894936],[-8229583.227808975,4952183.087711068],[-8229576.565664901,4952184.681837312],[-8229552.088519562,4952190.538959751],[-8229531.306833491,4952205.716602551],[-8229373.2939480245,4952349.570906411],[-8229336.703687231,4952363.862364286],[-8229320.290986504,4952363.848371015],[-8229310.446483247,4952360.088426314],[-8229308.965576463,4952359.413703582],[-8229300.133921834,4952355.390213288],[-8229280.438011701,4952356.311203033],[-8229256.988449837,4952359.573685508],[-8229235.528955831,4952370.649742341],[-8229216.633227313,4952390.488664879],[-8229209.594432426,4952396.109783072],[-8229208.333153962,4952396.486958316],[-8229195.522641493,4952400.318001887],[-8229188.949039044,4952410.160014208],[-8229183.556622194,4952420.025775327],[-8229183.312881423,4952420.471765843],[-8229077.923776969,4952520.379918881],[-8229054.246277957,4952551.546152553],[-8229007.835885149,4952731.265728027],[-8228980.658280491,4952837.8611589],[-8228958.885541498,4952873.802563556],[-8228947.429492195,4952866.0684387265],[-8228570.756428989,4953411.170163004],[-8228579.800478327,4953418.348957619],[-8228576.793080022,4953450.751593116],[-8228562.364616487,4953466.662260646],[-8228527.8471201435,4953501.26867357],[-8228496.389401907,4953520.467530378],[-8228414.625597465,4953564.011755912],[-8228411.6213978175,4953562.26256572],[-8228387.818014101,4953548.40510178],[-8228310.503665571,4953643.9359887],[-8228325.517055555,4953659.049663524],[-8228238.179305902,4953756.564782386],[-8228219.901830046,4953765.32360997],[-8228195.896356124,4953790.140514369],[-8228134.752985225,4953902.062539769],[-8228044.781198631,4954057.278438537],[-8228047.055351432,4954156.23174134],[-8228117.630746958,4954325.218976241],[-8228230.780033092,4954529.991295751],[-8228335.845536589,4954621.012235986],[-8228361.5962591125,4954646.397489042],[-8228503.299992304,4954720.30086938],[-8228511.885226598,4954722.871565241],[-8228615.505908648,4954775.596963295],[-8228624.020020964,4954779.929190335],[-8228624.879836729,4954780.241276079],[-8228632.742820647,4954784.775694511],[-8228641.663370315,4954790.077029855],[-8228651.918139742,4954796.241217353],[-8228771.786696779,4954868.66304668],[-8228776.255475818,4954875.605533021],[-8228801.189063525,4954911.538393797],[-8228830.155059013,4954961.996240515],[-8228851.772041621,4954999.011649433],[-8228863.21241695,4955029.800487515],[-8228885.651950973,4955084.97556176],[-8228949.482335748,4955180.276849746],[-8228976.696243388,4955198.413201315],[-8229029.951149464,4955233.903693555],[-8229222.161518948,4955251.016313341],[-8229323.99315227,4955252.916156749],[-8229348.230158118,4955246.711745976],[-8229371.087039319,4955225.426625416],[-8229379.68707651,4955213.6676087435],[-8229385.189406292,4955200.760053388],[-8229391.369480278,4955187.46621995],[-8229461.399082359,4955056.398953739],[-8229466.171136142,4955048.751301171],[-8229480.897530201,4955026.767979182],[-8229460.614636137,4955015.072156333],[-8229464.617910585,4955009.063402075],[-8229466.29346428,4955006.320257185],[-8229478.776737279,4954986.916452922],[-8229482.124182317,4954981.681291714],[-8229494.408401789,4954965.409873053],[-8229499.754552765,4954958.32838915],[-8229502.928820898,4954951.474033013],[-8229504.981158124,4954943.778748885],[-8229507.085434615,4954936.359716455],[-8229545.265334068,4954801.745869476],[-8229568.971191716,4954685.180787791],[-8229564.889655465,4954624.776475821],[-8229546.853936433,4954580.379507624],[-8229540.140146777,4954581.548492643],[-8229538.593401606,4954577.346527618]]]},"properties":{"shape_area":23466536.5079,"ntaname":"Randall's Island","cdtaname":"MN11 East Harlem (CD 11 Equivalent)","shape_leng":32050.1607805,"boroname":"Manhattan","ntatype":9,"nta2020":"MN1191","borocode":1,"countyfips":"061","ntaabbrev":"RndllsIsl","cdta2020":"MN11"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8230005.083974538,4961910.354430891],[-8230167.198005607,4961999.759504883],[-8230321.889184387,4962085.973128804],[-8230458.9923300445,4962161.935779435],[-8230562.046282676,4962220.107794331],[-8230636.95875354,4962131.827041091],[-8230647.420942297,4962118.376176045],[-8230661.03362291,4962103.813429416],[-8230672.444462873,4962089.233173578],[-8230683.360579211,4962077.591110695],[-8230692.2847646475,4962066.222143722],[-8230704.038496066,4962051.745222987],[-8230737.734621672,4961951.756997804],[-8230744.535636406,4961929.341760516],[-8230910.111705779,4961970.482668994],[-8231011.741247055,4961996.917111474],[-8231115.043956041,4962021.0742984675],[-8231191.793978289,4962040.604501566],[-8231239.188804212,4962041.0476060575],[-8231268.415477253,4962038.940100992],[-8231283.210917087,4962035.16247509],[-8231297.5950446315,4962026.324593408],[-8231313.908478019,4962028.748415026],[-8231334.6130305305,4962032.13982439],[-8231352.932365981,4962029.408912279],[-8231381.347174317,4962025.054151601],[-8231396.18236023,4962023.918782719],[-8231359.448505504,4962119.571590106],[-8231356.705840914,4962126.761347664],[-8231342.00425266,4962166.951480705],[-8231323.325329373,4962229.158283519],[-8231321.249681746,4962236.070980368],[-8231317.945990811,4962251.428255608],[-8231390.533923002,4962267.93565258],[-8231408.176942311,4962272.577947207],[-8231421.752242953,4962275.937017648],[-8231465.445466356,4962286.743733264],[-8231491.742032139,4962293.247259435],[-8231497.452053271,4962294.668672973],[-8231514.59873201,4962299.396333115],[-8231624.224141732,4962326.69974113],[-8231677.322126102,4962340.073709903],[-8231728.798349907,4962353.038644908],[-8231738.424527745,4962355.614687652],[-8231742.172293212,4962345.719856562],[-8231742.572080212,4962344.032372998],[-8231743.621641241,4962339.600928009],[-8231742.897323879,4962337.80885599],[-8231742.8382895,4962335.648063932],[-8231747.2829444595,4962324.714186645],[-8231747.929689375,4962320.11140368],[-8231744.524138081,4962315.968987489],[-8231745.6108425325,4962308.590103887],[-8231744.368995343,4962305.9859762],[-8231740.87735704,4962304.788823058],[-8231738.383193131,4962303.737097144],[-8231737.201446985,4962301.696340197],[-8231739.543792795,4962300.546973549],[-8231742.972742153,4962299.370094758],[-8231745.208869245,4962296.474347826],[-8231745.483075479,4962293.500889287],[-8231740.734351216,4962286.566816218],[-8231738.719410099,4962286.732965763],[-8231737.726384177,4962285.207376832],[-8231741.916654559,4962280.906843527],[-8231737.066976687,4962271.256623737],[-8231736.940886907,4962266.721868882],[-8231737.847132066,4962265.225602155],[-8231738.658977694,4962260.272885566],[-8231731.324106986,4962239.59858069],[-8231730.7035927335,4962234.51229357],[-8231729.2994355755,4962227.679083934],[-8231726.221930454,4962219.855046752],[-8231726.272198757,4962203.466600655],[-8231724.432123315,4962196.890351067],[-8231721.418300514,4962192.3622767255],[-8231715.28654955,4962185.13993924],[-8231712.678310081,4962179.98400139],[-8231711.273201745,4962174.7483432405],[-8231709.659722474,4962154.604930451],[-8231705.093243159,4962147.468072815],[-8231703.495458565,4962141.5039306935],[-8231701.809749909,4962127.931129191],[-8231706.600665529,4962116.9413034],[-8231703.469245783,4962092.041043901],[-8231702.781078823,4962052.7134793075],[-8231700.028622762,4962034.988739483],[-8231700.115173292,4962025.5468376735],[-8231700.40236177,4962015.505182532],[-8231702.375467904,4961985.2732194895],[-8231704.098948349,4961977.068031431],[-8231707.405603894,4961954.356639764],[-8231708.832553995,4961920.493161387],[-8231714.48217161,4961907.089374183],[-8231718.140910685,4961899.382822836],[-8231721.162926124,4961889.862628833],[-8231722.843935067,4961867.260123359],[-8231718.960562019,4961847.101725527],[-8231710.957837058,4961821.889007892],[-8231702.375546788,4961801.385920698],[-8231688.599680171,4961787.935440691],[-8231683.779382402,4961778.476979673],[-8231679.6502748495,4961765.474519565],[-8231675.9148912085,4961753.948227908],[-8231670.11490602,4961736.216435567],[-8231669.849234485,4961723.920015361],[-8231678.468213438,4961710.270822483],[-8231681.825563812,4961709.512427886],[-8231687.159629098,4961705.1197738005],[-8231688.651995322,4961700.280013626],[-8231684.931126459,4961695.809485926],[-8231680.652889457,4961689.476795416],[-8231684.444466208,4961689.22612197],[-8231682.964450876,4961683.579856192],[-8231679.911250024,4961684.263264848],[-8231665.324566315,4961663.0093573],[-8231661.86268975,4961655.639607203],[-8231656.739129466,4961638.9752684105],[-8231654.08982287,4961622.08303653],[-8231650.185304004,4961611.989715888],[-8231650.775069507,4961608.035183455],[-8231654.515857732,4961603.301864636],[-8231655.858348848,4961592.386983581],[-8231653.969687753,4961588.374357282],[-8231652.37133438,4961583.416582077],[-8231648.9740625415,4961572.840868089],[-8231648.898233747,4961558.770935833],[-8231647.421703061,4961545.56503863],[-8231645.356099162,4961541.46784482],[-8231644.72749744,4961536.740002349],[-8231643.233397976,4961533.664687414],[-8231639.838795475,4961495.419045566],[-8231641.223075993,4961480.926123874],[-8231640.445763557,4961476.437697906],[-8231642.796794155,4961459.072957987],[-8231641.437183034,4961450.486824977],[-8231644.090378156,4961440.785243951],[-8231643.718228077,4961436.579931223],[-8231641.54712113,4961422.07921199],[-8231641.915212064,4961415.878119483],[-8231644.350597175,4961408.50267349],[-8231644.888096899,4961392.049661546],[-8231645.120929222,4961391.473901177],[-8231653.095101496,4961371.762401126],[-8231658.775755992,4961359.611315656],[-8231665.964377081,4961343.276578411],[-8231672.165345985,4961327.886562277],[-8231682.115165694,4961298.511367184],[-8231692.0784957055,4961278.810054629],[-8231699.686423608,4961264.168771814],[-8231710.086068997,4961253.03221472],[-8231715.12363022,4961244.872801946],[-8231718.750572578,4961234.1232712865],[-8231727.823592801,4961217.95432109],[-8231735.153110205,4961205.403109594],[-8231737.946695091,4961193.7310498785],[-8231754.313767797,4961159.609050203],[-8231755.8033747645,4961156.502791983],[-8231757.266848896,4961152.5490389215],[-8231761.074253844,4961137.960461716],[-8231765.815722012,4961129.462730273],[-8231772.921115359,4961122.229809386],[-8231785.04571723,4961105.965389798],[-8231796.817502967,4961070.292959974],[-8231804.290257065,4961045.550613695],[-8231809.845748893,4961028.089927551],[-8231821.349959448,4961006.697816069],[-8231827.206787068,4960986.49162752],[-8231838.124295333,4960952.8157884255],[-8231850.03276668,4960917.357495366],[-8231862.036616112,4960889.822817602],[-8231879.7012009965,4960838.716395123],[-8231884.262262981,4960830.792961047],[-8231890.905843581,4960821.088809284],[-8231901.417595897,4960801.083718511],[-8231908.964498006,4960772.756060265],[-8231916.1047878545,4960757.701365058],[-8231926.925606009,4960719.072605397],[-8231938.044909942,4960677.868991023],[-8231947.171157156,4960656.674020482],[-8231957.111156967,4960622.637907308],[-8231965.536483063,4960590.708450523],[-8231981.983226983,4960518.588845339],[-8231985.12774493,4960508.3958706325],[-8232025.371455674,4960364.113691823],[-8232020.394915514,4960354.637830396],[-8232014.261297168,4960351.292433294],[-8232013.999343936,4960351.2253477],[-8232007.006987589,4960349.559588035],[-8231998.707277762,4960347.892600561],[-8231998.145419901,4960347.601195963],[-8231997.641907147,4960347.217691366],[-8231997.211679637,4960346.753466076],[-8231996.867502998,4960346.222294511],[-8231996.619589564,4960345.639937512],[-8231996.475295363,4960345.023674681],[-8231996.438901843,4960344.391791656],[-8231996.511488846,4960343.763037554],[-8231996.690902565,4960343.156068642],[-8231996.971819452,4960342.588894774],[-8231997.345904183,4960342.07834501],[-8232001.588762221,4960340.606550695],[-8232008.275529102,4960340.074499851],[-8232015.899816363,4960338.608581377],[-8232021.788368132,4960332.726743824],[-8232025.805925694,4960323.7686358495],[-8232029.961856864,4960306.116667292],[-8232031.983021036,4960279.904206618],[-8232034.195979753,4960269.205414647],[-8232036.8722936185,4960267.468736361],[-8232038.082444693,4960263.172016065],[-8232041.428520159,4960251.288752641],[-8232041.966076918,4960246.072971898],[-8232041.780023203,4960241.21587759],[-8232041.110438861,4960223.736742187],[-8232042.184476359,4960215.044868472],[-8232044.33266149,4960202.206842536],[-8232047.280344541,4960192.044413933],[-8232047.688172407,4960180.675617178],[-8232052.378565964,4960164.897006115],[-8232053.88713092,4960160.413107111],[-8232064.005087712,4960130.349120533],[-8232099.964457275,4959986.479765678],[-8232085.002295366,4959978.958135288],[-8232068.76741587,4959971.145149687],[-8232060.328569388,4959968.33950008],[-8232053.187221932,4959964.0597567335],[-8232039.689201108,4959957.444037887],[-8232021.497018127,4959948.320205823],[-8232003.0098019885,4959939.4317640895],[-8231962.119770994,4959949.707936634],[-8231954.49743244,4959951.62364138],[-8231878.86014156,4959919.967036208],[-8231622.78190957,4959767.128558024],[-8231611.468499407,4959761.115364601],[-8231599.82276369,4959754.6596903615],[-8231294.843612029,4959585.612200347],[-8231115.433704671,4959486.371511813],[-8231082.869932154,4959468.20025373],[-8231001.713941388,4959422.912867838],[-8231004.198995624,4959583.875354133],[-8231001.077932227,4959596.221693426],[-8230997.252994257,4959608.368437328],[-8230992.736803999,4959620.275492532],[-8230987.544334594,4959631.903496479],[-8230981.692657899,4959643.214131989],[-8230975.201086314,4959654.170064835],[-8230936.548486622,4959723.622794834],[-8230927.08381053,4959749.377258524],[-8230923.969424749,4959759.53846102],[-8230920.203886911,4959769.476790486],[-8230915.80298652,4959779.1505679665],[-8230910.78517776,4959788.519223728],[-8230905.17150211,4959797.543467422],[-8230898.985504037,4959806.185446458],[-8230892.253121,4959814.408924302],[-8230885.002585534,4959822.1794127],[-8230877.264303392,4959829.464323018],[-8230869.07072604,4959836.233102953],[-8230795.275842072,4959919.159675034],[-8230785.289201045,4959928.655972141],[-8230775.885339399,4959938.729721442],[-8230767.097587344,4959949.345222164],[-8230758.957091827,4959960.464853355],[-8230751.492706137,4959972.04920718],[-8230744.730897737,4959984.057194911],[-8230738.69562141,4959996.446292415],[-8230733.408270002,4960009.172592017],[-8230728.887585805,4960022.19099062],[-8230725.149594111,4960035.455349549],[-8230712.809717797,4960072.58071943],[-8230671.805062601,4960189.856969559],[-8230631.811096701,4960303.919574014],[-8230594.7692012265,4960410.081082938],[-8230560.5253256215,4960506.360792953],[-8230543.559034342,4960539.740082014],[-8230529.2263681255,4960561.6254203785],[-8230513.363163566,4960582.545824012],[-8230496.071162088,4960602.344866644],[-8230477.474861412,4960620.878169706],[-8230457.7166207535,4960638.025038363],[-8230443.368786364,4960656.551037778],[-8230443.355489566,4960656.568142685],[-8230443.344941748,4960656.586821136],[-8230430.793328334,4960677.222004404],[-8230430.789296447,4960677.228606688],[-8230430.7854355015,4960677.236485954],[-8230420.452410918,4960699.722671664],[-8230420.441835619,4960699.745180721],[-8230420.435238254,4960699.767643619],[-8230412.684122942,4960723.74723175],[-8230407.797696666,4960748.77315391],[-8230405.954687301,4960774.28955302],[-8230405.953303376,4960774.301461237],[-8230405.953294594,4960774.313296808],[-8230407.19100099,4960799.7569834255],[-8230407.19219237,4960799.772920635],[-8230407.194907191,4960799.787483886],[-8230411.42008948,4960824.632781621],[-8230411.421286825,4960824.640689303],[-8230411.423932947,4960824.648573508],[-8230418.425748082,4960848.409760598],[-8230427.882352093,4960870.641764214],[-8230427.891667472,4960870.662937847],[-8230427.903487628,4960870.683941444],[-8230439.439054898,4960891.077485398],[-8230455.128318235,4960931.60851016],[-8230472.793831174,4960993.475437017],[-8230476.480901166,4961008.968945986],[-8230476.490105912,4961009.007333357],[-8230476.493958577,4961009.044268006],[-8230478.0661810385,4961025.097247902],[-8230478.0688166255,4961025.119620145],[-8230478.067350282,4961025.143511834],[-8230477.424590532,4961041.369089669],[-8230477.423283155,4961041.377143016],[-8230474.58029551,4961057.297620432],[-8230474.577461948,4961057.310731176],[-8230474.573595978,4961057.325289954],[-8230469.666806472,4961072.501188901],[-8230422.1558190845,4961160.3253884055],[-8230369.934594766,4961254.9552505575],[-8230340.697333764,4961306.250809459],[-8230318.830271272,4961344.615479161],[-8230263.566110236,4961443.923676258],[-8230209.792821866,4961541.1916162055],[-8230200.659818253,4961557.612658005],[-8230158.332060896,4961633.716551017],[-8230108.789121577,4961723.546690859],[-8230099.343456056,4961739.774631921],[-8230093.369296301,4961751.276859034],[-8230083.82946534,4961768.085472938],[-8230080.672956893,4961773.648526073],[-8230077.049101623,4961779.193758298],[-8230073.709805409,4961785.41796309],[-8230070.3717830945,4961791.640673686],[-8230063.139285394,4961805.14310242],[-8230055.906767513,4961818.6457409505],[-8230005.083974538,4961910.354430891]]]},"properties":{"shape_area":21026876.3569,"ntaname":"Washington Heights (South)","cdtaname":"MN12 Washington Heights-Inwood (CD 12 Equivalent)","shape_leng":22198.4098239,"boroname":"Manhattan","ntatype":0,"nta2020":"MN1201","borocode":1,"countyfips":"061","ntaabbrev":"WshHts_S","cdta2020":"MN12"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8229267.123428889,4963992.608617591],[-8229334.899207961,4964131.892765468],[-8229405.892848658,4964277.473623935],[-8229473.637497916,4964416.865761053],[-8229506.022005681,4964483.20185421],[-8229545.957543877,4964565.004632976],[-8229633.738792736,4964662.488343552],[-8229708.181309753,4964741.975005794],[-8229776.599296112,4964817.612684992],[-8229848.798550168,4964896.089434637],[-8229952.304048136,4965010.262574026],[-8229981.174943867,4965039.27805913],[-8229992.077529585,4965050.236130369],[-8230007.40162033,4965067.588864086],[-8230017.361589266,4965078.336579219],[-8230017.343943325,4965121.386897363],[-8230024.210470909,4965158.541274677],[-8230094.695867595,4965197.066461708],[-8230092.604073606,4965193.7662302],[-8230083.242265351,4965184.569402684],[-8230080.065328335,4965175.73800546],[-8230078.161718249,4965167.279363389],[-8230078.54199872,4965161.267548164],[-8230079.170402577,4965158.637733919],[-8230081.178976983,4965152.752523286],[-8230084.835251544,4965143.156819721],[-8230087.439566907,4965134.1715143705],[-8230112.595392875,4965147.572933644],[-8230102.183871227,4965180.920970308],[-8230111.9679308515,4965183.924777365],[-8230124.783322992,4965142.98829525],[-8230093.6369712325,4965126.986131941],[-8230105.240689682,4965102.427093909],[-8230116.026636527,4965100.638296101],[-8230128.776056226,4965092.170416768],[-8230135.817422205,4965079.68135076],[-8230135.929090029,4965070.151313235],[-8230132.40457264,4965068.479465412],[-8230123.43383136,4965067.639324421],[-8230112.707411198,4965065.782337005],[-8230107.531449455,4965061.98584833],[-8230104.577693745,4965054.862196828],[-8230103.485659506,4965030.815127458],[-8230100.899369183,4965026.744628709],[-8230100.45855056,4965020.308454428],[-8230105.801956922,4965021.461431599],[-8230106.6540634325,4965018.802470022],[-8230111.811045415,4965021.41116937],[-8230127.7330826055,4965029.465058137],[-8230128.94321638,4965030.077287459],[-8230128.392404517,4965031.283759246],[-8230130.874157942,4965032.416659095],[-8230135.827545887,4965021.565904317],[-8230133.875444521,4965020.674651343],[-8230133.314238729,4965020.418488847],[-8230145.043963349,4964994.723209032],[-8230142.551339021,4964993.585395651],[-8230142.195923444,4964993.423160185],[-8230141.846869645,4964994.188115395],[-8230140.97892353,4964996.089010447],[-8230137.125795033,4965004.530042822],[-8230135.925008526,4965007.16041039],[-8230132.361500178,4965014.966302978],[-8230131.679261446,4965016.461099212],[-8230127.559865225,4965025.484986192],[-8230127.378434995,4965025.88234637],[-8230122.290521518,4965023.278988985],[-8230107.760281244,4965015.1224059025],[-8230108.532140649,4965011.305179453],[-8230102.393362575,4965008.989283356],[-8230103.900829301,4965004.622182635],[-8230103.09167175,4964996.500280324],[-8230114.16146065,4964935.840277238],[-8230113.4996895725,4964931.987508992],[-8230112.035563784,4964927.585548177],[-8230111.642131591,4964923.468566255],[-8230113.148612032,4964906.484750595],[-8230115.631932246,4964881.642921425],[-8230119.946653068,4964863.218370738],[-8230130.355235904,4964840.086858931],[-8230133.630235914,4964840.575112593],[-8230139.156653965,4964825.547083593],[-8230146.103216798,4964828.232284557],[-8230144.471776223,4964832.457868354],[-8230166.055174713,4964840.786772608],[-8230172.972219058,4964822.862750405],[-8230157.4864437515,4964816.887765124],[-8230156.80499495,4964818.656608697],[-8230146.43457612,4964814.654222746],[-8230165.377487483,4964777.258881237],[-8230192.26212361,4964714.992299279],[-8230198.551538826,4964695.752824163],[-8230212.50954941,4964668.708775601],[-8230219.888559616,4964668.16775131],[-8230228.643997016,4964653.14514606],[-8230232.480304476,4964640.304086093],[-8230232.487156492,4964631.286893846],[-8230238.777102313,4964625.279646111],[-8230247.805889879,4964609.982736524],[-8230247.540822083,4964599.051868301],[-8230254.105956991,4964590.586398509],[-8230271.762194199,4964549.472187179],[-8230279.962752793,4964546.746523389],[-8230289.668240669,4964537.464584021],[-8230297.067438936,4964515.333312736],[-8230297.085141213,4964491.832412597],[-8230304.331432978,4964483.3660659185],[-8230316.639384014,4964468.892971851],[-8230321.018444937,4964459.331066676],[-8230334.826998026,4964447.455150176],[-8230343.597123307,4964413.8505648235],[-8230364.10576371,4964379.559074385],[-8230379.323778439,4964354.113741777],[-8230389.953144124,4964334.9399642],[-8230472.743258041,4964188.232899233],[-8230514.323546979,4964126.508177683],[-8230570.816694651,4964035.965696605],[-8230608.090672896,4963953.396907801],[-8230608.98801742,4963946.410521377],[-8230607.238538299,4963941.023942183],[-8230608.148028758,4963937.936401087],[-8230625.686164293,4963921.103156045],[-8230633.891962642,4963908.539456985],[-8230639.644136149,4963889.142496665],[-8230644.30519895,4963868.379580695],[-8230668.638106668,4963829.0088336365],[-8230693.816429327,4963783.27492898],[-8230694.553488943,4963775.406225684],[-8230690.659910048,4963771.6137219],[-8230688.2639232855,4963769.88420046],[-8230688.127965567,4963767.650029373],[-8230691.233941829,4963765.408910183],[-8230711.073367712,4963743.731587967],[-8230724.187035797,4963728.5942507405],[-8230774.97193133,4963662.651523002],[-8230786.867489803,4963631.233980798],[-8230789.188867816,4963617.615389801],[-8230796.364521522,4963608.5208880445],[-8230813.208763063,4963572.962508656],[-8230813.625544144,4963567.862610892],[-8230842.335818196,4963519.904512825],[-8230856.419067685,4963489.8592822235],[-8230864.179731168,4963466.238344655],[-8230876.578286525,4963444.653711629],[-8230883.337466318,4963439.83410611],[-8230892.449008421,4963422.193601968],[-8230896.593016918,4963411.166848517],[-8230919.780375745,4963371.753080102],[-8230928.06403424,4963356.042345238],[-8230938.821728144,4963348.054100422],[-8230957.450450508,4963323.389957457],[-8230965.183079508,4963306.299098901],[-8230972.637518479,4963292.518150507],[-8230979.532832855,4963289.214285981],[-8230995.818739053,4963261.101301412],[-8231002.17392899,4963242.632674115],[-8231008.253437267,4963221.681871903],[-8231017.507974996,4963197.010029303],[-8231032.139658969,4963168.621507659],[-8231040.426555131,4963145.466187654],[-8231046.217762653,4963144.642421908],[-8231048.703674406,4963138.854805921],[-8231058.371786209,4963111.564201201],[-8231069.686986055,4963096.683492616],[-8231075.486554355,4963081.246066442],[-8231074.389707698,4963072.974112358],[-8231078.116688679,4963065.11852805],[-8231100.48005815,4963021.846107729],[-8231104.070881383,4963012.197673995],[-8231109.324137434,4962990.420232671],[-8231120.088187931,4962974.159757024],[-8231129.863697569,4962948.320336111],[-8231130.365812741,4962947.109685875],[-8231134.288213482,4962937.655810661],[-8231139.210825949,4962924.680871578],[-8231155.849521866,4962889.677089995],[-8231166.612813959,4962872.314335971],[-8231171.312650989,4962854.395707448],[-8231191.926680701,4962823.075531581],[-8231238.515743666,4962752.288937828],[-8231253.689433785,4962737.135263324],[-8231274.664053851,4962705.368410907],[-8231300.05142756,4962672.373814969],[-8231303.678854562,4962668.745653379],[-8231350.517199803,4962621.898133703],[-8231363.980668231,4962614.441620016],[-8231385.729836649,4962609.593649259],[-8231395.239837783,4962605.844191336],[-8231403.96514239,4962597.995571985],[-8231407.456657582,4962586.6555873975],[-8231409.901033026,4962580.549964303],[-8231416.356596391,4962582.818810878],[-8231423.334045938,4962584.564864994],[-8231430.313425824,4962582.822063272],[-8231433.1063605035,4962577.065136083],[-8231431.886468222,4962570.260102653],[-8231435.725539172,4962568.866428525],[-8231440.785799909,4962563.284464132],[-8231443.752613322,4962559.446229891],[-8231444.626553248,4962554.561826251],[-8231449.336891191,4962554.911527087],[-8231453.6975814765,4962559.274219006],[-8231452.649197148,4962564.857212484],[-8231455.440399177,4962566.60217865],[-8231458.9307927545,4962565.555867397],[-8231459.854434512,4962565.823146283],[-8231462.7087342795,4962564.270157883],[-8231464.660755538,4962564.431523622],[-8231464.439131149,4962565.824136319],[-8231462.593224036,4962569.395487731],[-8231464.51214354,4962574.280540449],[-8231472.711853348,4962572.189392173],[-8231473.933879153,4962569.92095061],[-8231478.819123078,4962567.304678534],[-8231482.700893584,4962569.024268792],[-8231484.485208742,4962567.379947887],[-8231485.213521299,4962562.563131056],[-8231485.53815575,4962560.415644696],[-8231484.976572858,4962556.49083289],[-8231485.661464014,4962554.784273194],[-8231487.959968995,4962553.508869442],[-8231493.672806062,4962551.406387978],[-8231494.925000682,4962550.708088082],[-8231506.089223683,4962544.482260669],[-8231512.235624618,4962541.054586623],[-8231517.295893842,4962541.2313535875],[-8231520.4371506255,4962536.870291754],[-8231524.625513483,4962532.333869441],[-8231527.0696388865,4962527.623970232],[-8231536.840550418,4962519.949856785],[-8231541.550694286,4962520.823808701],[-8231549.403333134,4962517.68394395],[-8231550.101343926,4962514.021007754],[-8231546.874737395,4962510.09430724],[-8231547.922891025,4962506.081497241],[-8231550.888550942,4962506.431972386],[-8231554.552078347,4962505.037045786],[-8231555.599753497,4962502.76850078],[-8231563.102186139,4962500.327252344],[-8231561.881418621,4962496.489440807],[-8231563.8018429065,4962494.919740502],[-8231566.767550115,4962496.490570705],[-8231569.7333170995,4962496.665513268],[-8231573.320825617,4962488.484901181],[-8231577.585987853,4962487.070842731],[-8231578.459975328,4962480.964926105],[-8231581.600242686,4962480.965576733],[-8231587.883231487,4962469.103251516],[-8231592.420109154,4962464.916970293],[-8231595.735522135,4962467.186203013],[-8231600.794380024,4962467.3616228085],[-8231600.446654186,4962462.82560614],[-8231605.157885533,4962458.813506605],[-8231613.358384429,4962458.815191012],[-8231619.463874511,4962461.433801556],[-8231623.3025366515,4962461.783171327],[-8231627.140449841,4962459.166698677],[-8231636.387589532,4962461.611538309],[-8231644.139677506,4962459.270520221],[-8231646.265378984,4962456.652717212],[-8231646.828353058,4962447.7059889315],[-8231641.990156785,4962438.891686199],[-8231641.911384519,4962438.749095377],[-8231640.802067995,4962435.151478467],[-8231640.18382349,4962431.142256266],[-8231641.83101154,4962429.03497621],[-8231644.398542974,4962429.388785375],[-8231645.97442252,4962431.613796475],[-8231647.254837722,4962435.064013399],[-8231647.429984177,4962438.556776362],[-8231648.676083055,4962440.90180431],[-8231650.224973305,4962441.6103843665],[-8231651.773015304,4962438.583022358],[-8231653.417285309,4962438.5857269475],[-8231652.882386743,4962430.183066005],[-8231650.329458602,4962427.805532174],[-8231653.081763305,4962420.13995922],[-8231658.199927231,4962417.043180126],[-8231661.680725501,4962410.656418713],[-8231664.147351826,4962409.506789703],[-8231666.231834875,4962410.321936628],[-8231667.5008403575,4962411.397550255],[-8231668.688130909,4962412.403660182],[-8231670.27140763,4962414.906040393],[-8231670.221933424,4962418.293814591],[-8231667.67699437,4962419.950940312],[-8231666.77461118,4962422.718685535],[-8231668.776098797,4962426.320721841],[-8231672.478182975,4962429.544751995],[-8231673.865331661,4962430.487194466],[-8231675.363751947,4962431.240265793],[-8231676.9477403285,4962431.791047929],[-8231678.590125468,4962432.130092858],[-8231680.257391942,4962432.2515028715],[-8231681.92626628,4962432.154671335],[-8231683.5683028335,4962431.841248732],[-8231685.327162405,4962431.245682923],[-8231686.981380612,4962430.401972523],[-8231688.496165347,4962429.327862827],[-8231720.366280881,4962384.325916752],[-8231721.643891763,4962382.517981861],[-8231727.8024082435,4962373.803653349],[-8231734.177441643,4962364.781635622],[-8231738.424527745,4962355.614687652],[-8231728.798349907,4962353.038644908],[-8231677.322126102,4962340.073709903],[-8231624.224141732,4962326.69974113],[-8231514.59873201,4962299.396333115],[-8231497.452053271,4962294.668672973],[-8231491.742032139,4962293.247259435],[-8231465.445466356,4962286.743733264],[-8231421.752242953,4962275.937017648],[-8231408.176942311,4962272.577947207],[-8231390.533923002,4962267.93565258],[-8231317.945990811,4962251.428255608],[-8231321.249681746,4962236.070980368],[-8231323.325329373,4962229.158283519],[-8231342.00425266,4962166.951480705],[-8231356.705840914,4962126.761347664],[-8231359.448505504,4962119.571590106],[-8231396.18236023,4962023.918782719],[-8231381.347174317,4962025.054151601],[-8231352.932365981,4962029.408912279],[-8231334.6130305305,4962032.13982439],[-8231313.908478019,4962028.748415026],[-8231297.5950446315,4962026.324593408],[-8231283.210917087,4962035.16247509],[-8231268.415477253,4962038.940100992],[-8231239.188804212,4962041.0476060575],[-8231191.793978289,4962040.604501566],[-8231115.043956041,4962021.0742984675],[-8231011.741247055,4961996.917111474],[-8230910.111705779,4961970.482668994],[-8230744.535636406,4961929.341760516],[-8230737.734621672,4961951.756997804],[-8230704.038496066,4962051.745222987],[-8230692.2847646475,4962066.222143722],[-8230683.360579211,4962077.591110695],[-8230672.444462873,4962089.233173578],[-8230661.03362291,4962103.813429416],[-8230647.420942297,4962118.376176045],[-8230636.95875354,4962131.827041091],[-8230562.046282676,4962220.107794331],[-8230458.9923300445,4962161.935779435],[-8230321.889184387,4962085.973128804],[-8230167.198005607,4961999.759504883],[-8230005.083974538,4961910.354430891],[-8229979.253835473,4961956.146146225],[-8229948.394180626,4962011.608663644],[-8229919.092357246,4962064.682143598],[-8229817.372793722,4962012.14724226],[-8229736.678564205,4962178.316583473],[-8229701.515777911,4962225.1412190115],[-8229655.054898387,4962263.891143837],[-8229617.4508634955,4962318.679385274],[-8229569.073449772,4962389.163593595],[-8229524.881705223,4962497.352326639],[-8229522.304008141,4962553.2514778655],[-8229519.686788121,4962610.007535967],[-8229593.4360260125,4962652.08429923],[-8229542.640005003,4962744.0929222405],[-8229493.662768615,4962832.31752287],[-8229325.059377774,4963140.51407611],[-8229312.473746626,4963159.704450131],[-8229300.507450652,4963179.287055201],[-8229289.172703755,4963199.241911976],[-8229278.481138878,4963219.548683594],[-8229268.443614443,4963240.186630385],[-8229259.070377114,4963261.134695041],[-8229256.057693524,4963272.84357731],[-8229253.916989318,4963284.742817869],[-8229252.659933554,4963296.767571915],[-8229252.293379909,4963308.852310178],[-8229252.819352303,4963320.931203098],[-8229254.234966713,4963332.938396697],[-8229256.532511847,4963344.808455944],[-8229259.699470102,4963356.476692677],[-8229293.552203793,4963442.274175946],[-8229299.19599989,4963450.85181726],[-8229305.6121189585,4963458.868173065],[-8229312.745089251,4963466.253932715],[-8229320.533205275,4963472.945240897],[-8229328.909167786,4963478.8842429165],[-8229337.800558992,4963484.01958858],[-8229347.169281879,4963487.882185859],[-8229356.871577198,4963490.807619732],[-8229366.813952978,4963492.767699595],[-8229376.900603605,4963493.74353733],[-8229387.034333083,4963493.725729377],[-8229397.117491715,4963492.714447329],[-8229407.05291716,4963490.719436318],[-8229416.744852933,4963487.75990901],[-8229426.099923482,4963483.864398299],[-8229435.027983029,4963479.070442737],[-8229443.44300086,4963473.424238509],[-8229451.263890356,4963466.980194173],[-8229458.415290393,4963459.800406295],[-8229464.828291505,4963451.954060973],[-8229470.661538521,4963440.69088909],[-8229476.926614799,4963429.662096639],[-8229483.614118258,4963418.884231293],[-8229490.714039707,4963408.3734536385],[-8229498.215698389,4963398.1455454305],[-8229506.107837063,4963388.215852393],[-8229529.601324081,4963476.311100572],[-8229527.074416518,4963500.599663985],[-8229521.84885517,4963524.984730695],[-8229513.870934605,4963549.014972305],[-8229503.188497638,4963572.230811693],[-8229489.949915819,4963594.1823779],[-8229474.399151252,4963614.462785406],[-8229456.866012143,4963632.730995851],[-8229437.7360993475,4963648.728645231],[-8229417.43131367,4963662.296781574],[-8229395.923252279,4963680.109030775],[-8229375.645795262,4963700.119523088],[-8229356.918993855,4963722.204265087],[-8229340.045814437,4963746.175682772],[-8229325.294567743,4963771.793363273],[-8229312.894462867,4963798.764245617],[-8229303.0147894295,4963826.757436943],[-8229295.764414613,4963855.416491783],[-8229294.347892216,4963864.373496679],[-8229289.848248138,4963885.578278599],[-8229286.164248106,4963916.358035955],[-8229285.378543136,4963942.160862539],[-8229270.261749369,4963978.650432007],[-8229268.773841165,4963985.133893934],[-8229264.584499331,4963987.1461509075],[-8229267.123428889,4963992.608617591]]]},"properties":{"shape_area":24773511.3589,"ntaname":"Washington Heights (North)","cdtaname":"MN12 Washington Heights-Inwood (CD 12 Equivalent)","shape_leng":26731.5961172,"boroname":"Manhattan","ntatype":0,"nta2020":"MN1202","borocode":1,"countyfips":"061","ntaabbrev":"WshHts_N","cdta2020":"MN12"}}, +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8228997.7582727205,4963120.435102699],[-8228992.361152536,4963117.722755777],[-8228991.4996861145,4963119.453152416],[-8228991.494371228,4963119.464077372],[-8228990.6945304945,4963121.108810847],[-8228989.699489989,4963123.155484962],[-8228988.265468565,4963126.780309545],[-8228985.6220715605,4963133.461572415],[-8228983.771917154,4963137.021312248],[-8228979.998854859,4963144.6378224995],[-8228976.99526711,4963150.701234842],[-8228976.473537566,4963150.6759153325],[-8228975.954459504,4963150.734769144],[-8228975.451321978,4963150.876113372],[-8228974.97692448,4963151.096373455],[-8228974.543257874,4963151.389911023],[-8228974.161553235,4963151.749244921],[-8228973.841176406,4963152.165295852],[-8228973.590708823,4963152.627289145],[-8228973.416350109,4963153.123711149],[-8228973.322679926,4963153.641916942],[-8228973.311749022,4963154.168178688],[-8228973.3841336,4963154.68984791],[-8228973.537954797,4963155.1929967785],[-8228973.769244863,4963155.665144699],[-8228964.042336143,4963173.614289216],[-8228962.689839142,4963172.881334062],[-8228939.722750019,4963215.263114044],[-8228932.64348181,4963211.679270256],[-8228924.856398738,4963225.748613665],[-8228922.865246528,4963224.7769316025],[-8228914.532783745,4963240.185275882],[-8228913.844969712,4963239.8261872865],[-8228905.847131912,4963255.148808931],[-8228925.826582244,4963265.5777031],[-8228933.816917238,4963250.269950769],[-8228933.247677758,4963249.973011105],[-8228941.08561896,4963234.7650813265],[-8228939.082719787,4963233.604342583],[-8228946.886236191,4963219.076235167],[-8228945.285505769,4963218.274011019],[-8228954.056720062,4963201.754767388],[-8228967.975103394,4963175.67876688],[-8228966.450657631,4963174.912919076],[-8228976.112259375,4963157.014258562],[-8228976.624486056,4963157.065015005],[-8228977.138062513,4963157.034082248],[-8228977.640044226,4963156.922333001],[-8228978.117877872,4963156.732654332],[-8228978.559720708,4963156.469874255],[-8228978.954293485,4963156.140344115],[-8228979.29161694,4963155.752479521],[-8228979.56352795,4963155.316171377],[-8228979.762918909,4963154.842072909],[-8228979.884793205,4963154.342239213],[-8228979.9260694245,4963153.829365673],[-8228979.8858028175,4963153.31632146],[-8228979.765038274,4963152.815781078],[-8228979.566834585,4963152.340519011],[-8228979.295921502,4963151.902451596],[-8228981.18608498,4963148.680679195],[-8228995.89488287,4963123.611503875],[-8228996.009264816,4963123.41636944],[-8228996.101877351,4963123.258302891],[-8228996.842136429,4963121.996668252],[-8228997.4896108005,4963120.893198323],[-8228997.7582727205,4963120.435102699]]],[[[-8228997.7582727205,4963120.435102699],[-8229001.233248458,4963163.834391191],[-8229001.108327565,4963198.538501523],[-8228997.764891866,4963204.119957123],[-8229000.745401358,4963208.11066105],[-8229001.361128813,4963252.568243098],[-8228996.448356312,4963256.14665831],[-8228992.014059533,4963258.9790267125],[-8228986.874945879,4963257.024463374],[-8228983.857816679,4963261.098952854],[-8228978.0051714275,4963264.284649946],[-8228971.616024943,4963272.610648259],[-8228975.692432256,4963273.500546961],[-8228978.172542852,4963275.275472798],[-8228976.928125767,4963279.351542675],[-8228967.70836261,4963281.293279136],[-8228964.399132252,4963292.240796327],[-8228956.034893743,4963300.591822482],[-8228952.867190941,4963303.988141276],[-8228950.286064168,4963306.916054523],[-8228948.290746097,4963309.961796131],[-8228946.7657694165,4963311.367026146],[-8228944.18616427,4963312.419618811],[-8228941.255748063,4963312.534187522],[-8228937.269780391,4963313.46834039],[-8228930.938539622,4963315.572473613],[-8228926.366371337,4963316.740464889],[-8228921.910374512,4963318.846268921],[-8228920.034077716,4963320.016668118],[-8228916.867929312,4963321.654744046],[-8228913.936324648,4963323.175895074],[-8228910.25883093,4963326.647229289],[-8228910.250445092,4963336.011865736],[-8228912.627764065,4963344.624286275],[-8228913.328234583,4963347.6724209655],[-8228914.027489097,4963352.244187316],[-8228913.086239404,4963356.111385632],[-8228909.917422738,4963360.79697472],[-8228905.928628406,4963364.7785316175],[-8228899.24527112,4963367.468430556],[-8228892.79572717,4963370.744528054],[-8228888.574763919,4963372.26447302],[-8228882.003777471,4963375.297540813],[-8228872.929512704,4963390.205368822],[-8228864.105139198,4963394.441672586],[-8228841.253950454,4963477.933075621],[-8228838.572086741,4963492.344503417],[-8228840.230997747,4963497.629728354],[-8228841.584256123,4963501.940427734],[-8228846.6852268735,4963502.723965283],[-8228847.759312248,4963504.779301209],[-8228855.197909474,4963503.319187969],[-8228857.839029064,4963504.202214173],[-8228856.858990458,4963505.962600098],[-8228854.607201924,4963506.743436473],[-8228856.856610299,4963508.604415837],[-8228856.2670341255,4963510.855044055],[-8228854.599617915,4963515.159021712],[-8228855.379657191,4963518.486875449],[-8228855.670062198,4963521.422106616],[-8228857.233864276,4963523.674677975],[-8228858.308806946,4963524.946979067],[-8228861.635355062,4963525.53752439],[-8228860.751143021,4963529.54906633],[-8228859.474448789,4963535.41921395],[-8228856.044109828,4963541.38515729],[-8228857.6097629815,4963541.777473905],[-8228857.999086203,4963543.343659491],[-8228859.564239344,4963544.127962276],[-8228861.518790275,4963546.478524571],[-8228860.246198109,4963547.846496059],[-8228859.656204949,4963550.585600353],[-8228862.882513605,4963553.915619416],[-8228867.284883947,4963554.899316894],[-8228869.0434317505,4963557.93366161],[-8228871.486926078,4963561.2628777465],[-8228874.715715831,4963561.9511148445],[-8228876.66964391,4963565.084547915],[-8228877.449636402,4963568.313357599],[-8228876.4678206565,4963572.1294140415],[-8228875.877551679,4963575.2607350405],[-8228874.702518814,4963576.628751046],[-8228874.699790828,4963579.662794964],[-8228876.55453391,4963584.360708764],[-8228878.998061813,4963587.690104004],[-8228882.521378912,4963586.910373264],[-8228885.162962393,4963587.304808175],[-8228889.271449356,4963589.069829916],[-8228891.323388211,4963591.713610229],[-8228889.269077874,4963591.711766434],[-8228886.530926261,4963590.04568333],[-8228883.009630508,4963588.574288834],[-8228881.051136317,4963589.257864774],[-8228882.223594524,4963592.096274003],[-8228883.983797725,4963593.272863645],[-8228886.03740538,4963594.057604885],[-8228888.385412689,4963595.0380764175],[-8228890.927899844,4963596.899352412],[-8228892.100724848,4963598.074112204],[-8228893.373529319,4963597.977545605],[-8228894.2525159735,4963599.837304085],[-8228896.697369378,4963601.699842818],[-8228898.945081829,4963603.951614763],[-8228901.781474471,4963606.400658202],[-8228903.346325,4963607.674877966],[-8228905.595267085,4963608.55747549],[-8228907.844280585,4963611.0060903],[-8228910.777937164,4963612.475503856],[-8228912.243882954,4963614.923343725],[-8228917.429973123,4963615.025694146],[-8228919.483926037,4963616.887880297],[-8228921.146518,4963617.9653682485],[-8228922.81138246,4963616.694136771],[-8228924.179545478,4963617.772662699],[-8228926.037318193,4963619.143475556],[-8228927.503793853,4963621.003833859],[-8228932.004986694,4963620.617020072],[-8228936.603677517,4963622.089265375],[-8228939.146428971,4963623.7551900465],[-8228940.318318614,4963625.712993308],[-8228938.262341812,4963627.570059816],[-8228942.565050531,4963630.705621546],[-8228944.519216523,4963633.447023709],[-8228945.104402067,4963636.18720534],[-8228944.218468906,4963642.155559511],[-8228943.333882487,4963646.557993096],[-8228943.428972151,4963649.494363179],[-8228941.764189499,4963650.863336266],[-8228941.173577008,4963654.287715571],[-8228937.8455848675,4963655.067647264],[-8228936.3756729225,4963658.588878385],[-8228940.18897042,4963661.331948909],[-8228942.243113014,4963662.997345162],[-8228938.717622554,4963664.75564397],[-8228937.252241054,4963663.090600344],[-8228935.002156171,4963662.012659278],[-8228930.696376366,4963662.301941397],[-8228930.302514182,4963664.258207274],[-8228929.128263774,4963664.747080176],[-8228930.985132924,4963667.195107712],[-8228932.160236662,4963667.098546325],[-8228934.701077425,4963670.916491386],[-8228935.284299691,4963674.341876386],[-8228936.163940098,4963675.321033235],[-8228935.967385052,4963676.593585717],[-8228935.379474333,4963677.180692932],[-8228935.377202564,4963679.724943942],[-8228937.237652813,4963679.432101058],[-8228940.073358585,4963681.392628806],[-8228941.5397591675,4963683.350632627],[-8228941.14519956,4963686.089954264],[-8228940.850149615,4963688.243108125],[-8228938.892731387,4963689.024265834],[-8228938.992824633,4963686.381088356],[-8228937.523953816,4963687.162829043],[-8228937.5224688435,4963688.826321786],[-8228933.706788308,4963688.822914849],[-8228932.5290726945,4963691.758259218],[-8228930.474482053,4963691.854133449],[-8228932.0418120185,4963690.2896503275],[-8228928.2250037305,4963690.090821085],[-8228929.400245829,4963688.33059373],[-8228924.900421469,4963687.151552064],[-8228920.301325965,4963687.637215349],[-8228920.20106494,4963690.572121494],[-8228922.548348652,4963690.771015418],[-8228922.744006216,4963692.042643111],[-8228922.057176377,4963693.705671197],[-8228923.913975426,4963696.153879224],[-8228925.675001683,4963696.5462920945],[-8228924.498034759,4963700.069100078],[-8228926.455344537,4963699.385580256],[-8228928.509500327,4963699.7781810695],[-8228930.269028958,4963701.737827706],[-8228930.070287888,4963705.455654365],[-8228927.135264928,4963705.453031637],[-8228926.1539935935,4963708.583950767],[-8228928.010695553,4963711.2275812235],[-8228926.730967376,4963719.054497605],[-8228924.6666846415,4963730.012595509],[-8228921.625991263,4963737.44723445],[-8228918.583644127,4963748.207853123],[-8228913.773958111,4963764.154364212],[-8228910.052428154,4963768.065758288],[-8228908.486498471,4963767.966645513],[-8228905.5469358675,4963771.584233058],[-8228905.545971709,4963772.660348361],[-8228899.181672449,4963777.352432585],[-8228896.048423851,4963779.110842685],[-8228891.936167179,4963781.45608573],[-8228888.606108014,4963784.485856057],[-8228886.551581598,4963784.679432531],[-8228885.963415879,4963785.266395674],[-8228883.9094158355,4963784.873684368],[-8228878.236822676,4963781.051565439],[-8228870.615784256,4963767.932652675],[-8228856.056837797,4963744.140490049],[-8228844.33746845,4963718.197600075],[-8228828.576912367,4963685.685831312],[-8228827.537279066,4963683.541398224],[-8228820.018933954,4963665.236361618],[-8228813.958270612,4963658.3811801],[-8228814.451824403,4963654.369235722],[-8228811.422178259,4963649.375709435],[-8228809.469110905,4963645.4591884455],[-8228805.751663069,4963644.869639363],[-8228793.329819635,4963638.00861909],[-8228793.42850621,4963636.931403779],[-8228785.017265041,4963632.129786607],[-8228785.801724634,4963630.466769076],[-8228784.7264493685,4963629.5852818005],[-8228790.046156725,4963622.121028678],[-8228744.904946057,4963596.059497022],[-8228732.529715737,4963589.024207041],[-8228727.340891263,4963586.126672634],[-8228719.114327839,4963581.52188661],[-8228711.628630556,4963576.768558986],[-8228711.404654214,4963577.8075069925],[-8228719.704946808,4963583.969020359],[-8228726.597738719,4963587.386944241],[-8228729.562606399,4963589.39125538],[-8228731.934472363,4963590.209380435],[-8228744.4596609855,4963597.858668725],[-8228747.499536071,4963598.826615935],[-8228758.839251634,4963604.990881247],[-8228759.9511170965,4963606.252700203],[-8228763.730401625,4963608.480845845],[-8228766.769975431,4963609.596652776],[-8228772.9959309045,4963613.235841586],[-8228788.115914938,4963621.999191036],[-8228786.259418978,4963625.112080962],[-8228785.146849471,4963624.592240521],[-8228782.733457288,4963628.34634021],[-8228781.070314754,4963630.538466543],[-8228775.159929577,4963627.863364639],[-8228766.428780159,4963623.0357036665],[-8228767.45117443,4963621.726725877],[-8228746.05836697,4963610.460867059],[-8228715.354492416,4963593.782341856],[-8228686.250819467,4963577.32654826],[-8228686.180526742,4963576.305629457],[-8228683.70715667,4963575.135351193],[-8228681.337539648,4963576.235083295],[-8228678.980425047,4963579.578534848],[-8228674.257487024,4963585.660906192],[-8228670.113748407,4963592.086921927],[-8228663.034861577,4963601.556154619],[-8228651.286097824,4963618.092621194],[-8228640.591790715,4963632.223210985],[-8228625.830207815,4963652.819536333],[-8228614.684483278,4963668.152528362],[-8228612.724996768,4963671.249437363],[-8228600.723236335,4963687.023946298],[-8228603.331949093,4963688.687427215],[-8228594.359470843,4963701.022572622],[-8228596.318093584,4963701.914412553],[-8228590.576260125,4963710.25078898],[-8228592.380831235,4963711.906775589],[-8228591.3255945295,4963713.712002924],[-8228591.475178435,4963714.6139066],[-8228589.668606792,4963716.567654379],[-8228588.315215067,4963715.213099458],[-8228585.604537115,4963719.422299189],[-8228583.19579553,4963721.526055971],[-8228581.987563627,4963725.8871480115],[-8228581.382319735,4963729.347207841],[-8228582.131020478,4963733.40918066],[-8228579.569444643,4963737.919613542],[-8228576.408115269,4963739.873455294],[-8228572.947254897,4963741.524553762],[-8228569.783503058,4963746.185021231],[-8228568.878838024,4963749.193101998],[-8228566.167919274,4963752.348794346],[-8228564.363148636,4963752.196692722],[-8228562.553465503,4963756.10700269],[-8228562.099352256,4963760.168015519],[-8228561.796796605,4963761.821980233],[-8228559.841280942,4963760.617448048],[-8228557.881504498,4963765.27887018],[-8228558.540878412,4963767.5848147785],[-8228557.2442032425,4963773.31774222],[-8228556.030049513,4963778.42631652],[-8228554.2784380475,4963782.19027121],[-8228562.476811081,4963785.962047799],[-8228543.358486203,4963812.836719134],[-8228519.932510812,4963844.547585647],[-8228508.623172915,4963860.806160485],[-8228506.13718006,4963863.43262913],[-8228501.193356281,4963871.104453197],[-8228505.476105014,4963872.831459442],[-8228505.427054919,4963874.506926001],[-8228503.22414201,4963876.587087898],[-8228503.514441984,4963878.091276283],[-8228500.997372323,4963881.581205776],[-8228500.436013371,4963883.07123434],[-8228499.132811696,4963883.163793934],[-8228498.664577226,4963885.537415757],[-8228496.424866043,4963890.052055436],[-8228494.234811255,4963892.611454758],[-8228494.604078923,4963897.233035576],[-8228495.812592137,4963899.654436546],[-8228494.330017594,4963903.6866377],[-8228494.8648348525,4963906.376717436],[-8228492.773210779,4963909.422119095],[-8228490.224775594,4963913.813737179],[-8228483.972454756,4963921.364176897],[-8228479.417301064,4963928.376144751],[-8228475.866996683,4963932.535908451],[-8228471.15704233,4963938.391131581],[-8228468.765072162,4963940.625574414],[-8228466.449666962,4963942.935326076],[-8228465.600011805,4963945.093330675],[-8228463.747888838,4963947.482734837],[-8228462.126160855,4963949.330984496],[-8228460.197788493,4963951.411337419],[-8228458.907306331,4963953.376132032],[-8228456.302665224,4963958.55343459],[-8228454.029996736,4963963.074748509],[-8228448.435800992,4963971.418085069],[-8228448.231239848,4963978.216373308],[-8228444.926399379,4963983.823398123],[-8228440.958969534,4963990.619520535],[-8228435.604306455,4963999.723553524],[-8228432.895514607,4964001.964285461],[-8228428.467812246,4964008.099719515],[-8228419.8092037,4964018.981871058],[-8228412.971072741,4964026.246858054],[-8228393.939785697,4964036.718751971],[-8228389.319398239,4964039.922982924],[-8228386.215231303,4964041.393470701],[-8228380.019235,4964043.445138536],[-8228378.961839938,4964044.702417791],[-8228378.436474165,4964045.327326972],[-8228376.255065369,4964047.921186702],[-8228375.45137654,4964048.876798589],[-8228374.624940499,4964049.859678811],[-8228372.974896132,4964051.821806647],[-8228372.169977106,4964052.77886672],[-8228365.146012087,4964060.458765997],[-8228363.501106116,4964062.257257709],[-8228362.528942027,4964063.282175002],[-8228360.1883758,4964065.749849466],[-8228358.486657045,4964067.543865817],[-8228357.871339323,4964068.192589953],[-8228354.814190864,4964069.876499412],[-8228350.968966898,4964072.494050985],[-8228349.9376154,4964073.196147929],[-8228346.246968656,4964075.708566842],[-8228338.909681506,4964083.236683642],[-8228336.030626392,4964086.190642746],[-8228335.090889704,4964087.154820696],[-8228322.244631074,4964100.33504456],[-8228316.553008563,4964106.174691185],[-8228314.285214176,4964108.501415815],[-8228310.333920163,4964111.4302032795],[-8228303.189979318,4964118.304697367],[-8228286.204965372,4964139.215712667],[-8228286.121891272,4964139.318038683],[-8228284.093388825,4964141.815539057],[-8228276.968018801,4964150.027936736],[-8228276.398322085,4964150.68461452],[-8228275.639423062,4964151.559358629],[-8228273.541058557,4964153.977845246],[-8228271.298060573,4964156.883634362],[-8228267.0076592695,4964162.441708028],[-8228260.209000825,4964171.782930485],[-8228257.3323983895,4964175.73521391],[-8228256.0873389095,4964177.80083103],[-8228254.968437725,4964179.050415674],[-8228248.987767358,4964175.601756852],[-8228247.355561356,4964175.725606729],[-8228246.28747983,4964177.104493349],[-8228246.914167314,4964177.544738572],[-8228245.720674083,4964178.861309214],[-8228244.7178496355,4964178.107661351],[-8228242.834518537,4964180.050899765],[-8228244.276742537,4964181.0556934085],[-8228243.144376353,4964183.250451883],[-8228238.065183338,4964180.171699128],[-8228235.783814397,4964184.285053074],[-8228230.22295363,4964194.31193865],[-8228223.21262841,4964205.730492816],[-8228225.909984619,4964207.27521261],[-8228218.574655838,4964217.483942352],[-8228225.895744942,4964222.02127148],[-8228227.245343739,4964221.829797723],[-8228230.808517213,4964223.923250911],[-8228230.324258012,4964227.519666275],[-8228231.355011476,4964231.051812329],[-8228232.823734143,4964234.462005024],[-8228234.170265679,4964237.451301748],[-8228233.241838955,4964244.10682323],[-8228232.330115634,4964246.510133898],[-8228227.698244019,4964256.286444007],[-8228226.534749893,4964259.033605466],[-8228221.800174307,4964270.884890318],[-8228220.448525375,4964273.597827742],[-8228218.518021394,4964276.0866005765],[-8228214.459794018,4964279.938287568],[-8228213.197594825,4964281.136305008],[-8228208.486026648,4964283.980899552],[-8228204.450308932,4964287.358915216],[-8228202.505008424,4964288.98722178],[-8228199.705712968,4964293.129171055],[-8228199.687177691,4964297.227707764],[-8228199.817174299,4964301.206419311],[-8228199.858685112,4964302.477279572],[-8228200.065929857,4964308.821315309],[-8228200.332505466,4964316.983251705],[-8228200.414333991,4964319.488964418],[-8228200.762718964,4964322.312549438],[-8228201.093433387,4964324.993400796],[-8228201.952552675,4964331.955623855],[-8228204.189961067,4964336.4613001],[-8228206.9534442,4964339.410858283],[-8228209.931591546,4964342.871828379],[-8228211.203477865,4964343.434095491],[-8228212.359947428,4964344.206376248],[-8228213.366658001,4964345.165737255],[-8228214.193714566,4964346.283689659],[-8228214.816556999,4964347.527035107],[-8228215.920162371,4964348.789190315],[-8228217.210716534,4964349.859447854],[-8228218.65526698,4964350.710480096],[-8228220.216929033,4964351.320556987],[-8228221.855827791,4964351.6741010025],[-8228222.957739835,4964351.7621717015],[-8228225.306726909,4964351.949973516],[-8228229.123225064,4964351.476991016],[-8228235.087943216,4964349.6935679475],[-8228240.133117695,4964349.785150206],[-8228245.147732964,4964350.347118574],[-8228248.7002973575,4964351.037134119],[-8228254.4435709575,4964352.692909569],[-8228260.975510885,4964354.009055125],[-8228262.74990663,4964354.133017865],[-8228268.115222671,4964355.091536891],[-8228272.8516154215,4964356.7486677915],[-8228275.267687748,4964357.961019938],[-8228277.1747678,4964358.916187193],[-8228280.153861338,4964361.423807792],[-8228283.2503012065,4964365.362895968],[-8228283.842787669,4964369.895091875],[-8228284.314102364,4964375.4993032245],[-8228282.877391521,4964381.10308677],[-8228280.251171139,4964385.15550667],[-8228275.954703195,4964387.775013803],[-8228271.063885346,4964389.081529772],[-8228264.623316481,4964390.148795827],[-8228259.375938858,4964390.262639042],[-8228254.127543844,4964391.331020793],[-8228251.266530925,4964391.090655864],[-8228250.556365037,4964385.365937365],[-8228230.637277172,4964388.923728452],[-8228231.226904119,4964396.318630506],[-8228228.483993431,4964396.792652523],[-8228227.884351214,4964399.773488482],[-8228228.240559273,4964401.324034847],[-8228208.561734086,4964403.570801546],[-8228198.900734771,4964404.395997421],[-8228189.717487744,4964405.699520636],[-8228188.40512777,4964405.459262399],[-8228185.068502191,4964404.024714975],[-8228114.609713476,4964365.446548408],[-8228121.605194234,4964385.631827677],[-8228128.996904139,4964400.451290262],[-8228172.2179133445,4964423.504098289],[-8228171.688264868,4964425.089531258],[-8228112.171041918,4964431.512087119],[-8228103.574968375,4964431.63569104],[-8228096.959370178,4964434.671415193],[-8228087.568317048,4964437.703163886],[-8228073.147032844,4964444.169621945],[-8228061.636729959,4964448.390298106],[-8228059.784575367,4964449.314064786],[-8228051.5713381795,4964464.38122412],[-8228015.406587176,4964525.840153398],[-8227970.898152187,4964601.176814785],[-8227938.442501464,4964656.292618159],[-8227907.178834854,4964708.763790195],[-8227907.673414687,4964710.769971789],[-8227927.660347106,4964722.000593505],[-8227914.948503313,4964741.70095794],[-8227896.536952948,4964771.17495879],[-8227871.76575531,4964811.486283457],[-8227857.06302626,4964834.6153612165],[-8227845.420697933,4964854.06604935],[-8227829.254653097,4964878.508829079],[-8227836.362720208,4964882.56849906],[-8227834.209127794,4964886.338930405],[-8227833.914093554,4964889.5407966],[-8227831.844510921,4964891.261960847],[-8227829.028130642,4964893.7205427755],[-8227826.584620059,4964895.602357366],[-8227824.558589184,4964898.739030957],[-8227823.579560585,4964901.806870473],[-8227823.294253371,4964907.176980604],[-8227824.89652106,4964909.271505714],[-8227823.570079049,4964911.2232552795],[-8227821.755840174,4964911.221428319],[-8227820.497352731,4964913.800233373],[-8227818.681182719,4964917.007233144],[-8227818.955286237,4964921.54211991],[-8227816.721915773,4964923.214113245],[-8227816.859779646,4964925.235927208],[-8227815.602208109,4964927.049050876],[-8227815.6697842255,4964929.490596546],[-8227815.668308453,4964930.955049546],[-8227816.574140032,4964930.955962339],[-8227816.15414035,4964932.420018382],[-8227816.361054911,4964935.420254441],[-8227815.961534241,4964937.453443763],[-8227814.102353323,4964941.236053779],[-8227809.224685061,4964946.38896436],[-8227807.597709921,4964949.15761739],[-8227804.442631274,4964952.116292449],[-8227801.860740731,4964953.8343897015],[-8227799.470749715,4964956.410857685],[-8227795.069952177,4964962.520150834],[-8227794.492809028,4964965.28994007],[-8227793.251432539,4964965.385131272],[-8227792.197831075,4964967.77143596],[-8227791.143506576,4964971.018842418],[-8227789.703110903,4964978.565255544],[-8227789.700119623,4964981.525768004],[-8227788.549082294,4964986.397172096],[-8227784.049284442,4964996.4230480045],[-8227781.563267994,4964998.617943699],[-8227780.508860911,4965001.770191309],[-8227778.3040851485,4965009.124362977],[-8227775.434434115,4965012.0819844855],[-8227774.857904815,4965015.426308274],[-8227773.5175885875,4965018.0038483115],[-8227773.897136892,4965021.347618541],[-8227772.176229552,4965022.970072229],[-8227771.122472655,4965025.64432497],[-8227771.786238693,4965029.9432361545],[-8227771.116187279,4965031.853340172],[-8227772.067675365,4965035.102697929],[-8227771.586891141,4965037.777607689],[-8227772.4455552595,4965040.070719787],[-8227772.157276629,4965041.694724807],[-8227771.416583652,4965044.200974028],[-8227768.696523219,4965047.501597454],[-8227766.339316411,4965050.8340822365],[-8227762.8193160435,4965052.821535271],[-8227757.132169304,4965053.294659789],[-8227753.277945793,4965055.113445314],[-8227751.437114145,4965055.6060904525],[-8227750.757286248,4965057.776169469],[-8227749.234085645,4965061.44661806],[-8227750.393300897,4965063.459106385],[-8227748.2815946,4965066.163616094],[-8227749.762994601,4965069.1671285],[-8227748.09961418,4965074.0765221855],[-8227745.128431747,4965075.989923533],[-8227745.2263971,4965077.808937494],[-8227743.292162047,4965079.510404285],[-8227743.7062565265,4965082.186650882],[-8227743.266019071,4965084.645334239],[-8227740.9946980085,4965089.554289299],[-8227738.288116299,4965095.960373932],[-8227741.703176783,4965097.688991589],[-8227745.652031208,4965099.420336495],[-8227747.898240004,4965099.43177907],[-8227748.514547549,4965104.569950818],[-8227774.436760966,4965119.1436798135],[-8227774.74025832,4965122.461785279],[-8227778.693281078,4965123.65805115],[-8227779.436154712,4965124.731923603],[-8227781.252474311,4965125.1690227715],[-8227782.101050939,4965126.564031969],[-8227786.477941662,4965128.512078319],[-8227787.414208792,4965133.759168575],[-8227787.703894527,4965139.858667382],[-8227786.510230814,4965143.061942746],[-8227783.822534581,4965145.722442518],[-8227777.38289811,4965149.862059319],[-8227773.4243562445,4965150.055123634],[-8227768.727421071,4965147.999585474],[-8227763.394816968,4965144.655913868],[-8227755.927387938,4965140.445863939],[-8227749.62875032,4965137.953038688],[-8227741.308928965,4965133.097549645],[-8227739.266129667,4965135.011657751],[-8227735.845369144,4965134.673788992],[-8227733.394813286,4965132.629534685],[-8227729.9666674705,4965133.467907035],[-8227726.525290684,4965137.194701411],[-8227728.974817395,4965139.453567163],[-8227728.304760111,4965144.905760042],[-8227725.817779628,4965150.028248316],[-8227726.556943289,4965152.065191541],[-8227725.586610138,4965153.556894691],[-8227725.464196737,4965156.5523702],[-8227726.418799539,4965158.161124132],[-8227724.801028964,4965160.613887065],[-8227723.389700791,4965164.6715738075],[-8227721.129371164,4965167.441570903],[-8227718.558125,4965168.28555533],[-8227721.971929955,4965170.01411771],[-8227721.847561119,4965173.650602791],[-8227720.755065183,4965178.0318369195],[-8227719.5645588115,4965180.592737113],[-8227721.166623315,4965181.243260028],[-8227719.322009736,4965186.3689004695],[-8227718.66068791,4965190.108686791],[-8227717.149614607,4965192.669189927],[-8227717.249277807,4965194.167760557],[-8227718.207360391,4965195.135541612],[-8227718.189948178,4965198.558000511],[-8227716.3562034555,4965201.544746517],[-8227716.343725936,4965204.004287241],[-8227715.584087135,4965206.248137366],[-8227713.637623035,4965210.3032333525],[-8227714.382701729,4965210.948047174],[-8227714.04542127,4965214.2629047865],[-8227713.163045165,4965219.500949448],[-8227713.6818097215,4965222.712741348],[-8227712.816745968,4965224.313709536],[-8227712.798829464,4965227.8435085965],[-8227713.087618131,4965234.3708174005],[-8227710.479793026,4965242.167007739],[-8227710.357623468,4965245.37574695],[-8227709.584051435,4965250.079083288],[-8227709.243386751,4965254.0363381365],[-8227706.866479452,4965258.624302462],[-8227708.671871484,4965261.20043492],[-8227709.070396893,4965266.980050259],[-8227708.509854762,4965272.004968],[-8227708.042409835,4965279.8120399695],[-8227709.528439353,4965282.173337813],[-8227707.69254535,4965285.587857686],[-8227706.934708672,4965287.188060523],[-8227708.613598552,4965293.829782339],[-8227708.267870492,4965298.535346585],[-8227707.290528803,4965301.632462501],[-8227707.811826792,4965304.096134424],[-8227707.373678066,4965306.1257210085],[-8227707.044989568,4965307.729337008],[-8227704.334944795,4965314.776592489],[-8227706.565562666,4965317.890027213],[-8227705.679585526,4965323.876372842],[-8227704.472313196,4965329.753810182],[-8227703.601901352,4965332.6382730575],[-8227703.263511276,4965336.166408937],[-8227700.577991809,4965338.400512972],[-8227700.455496275,4965341.394521746],[-8227699.158334717,4965344.06224316],[-8227699.03207701,4965347.806024429],[-8227701.469645299,4965352.418568452],[-8227703.060009897,4965355.102134147],[-8227704.1107279705,4965358.957890526],[-8227703.297920104,4965362.3507057885],[-8227701.851226686,4965365.854359726],[-8227700.827377432,4965369.355227577],[-8227699.476441907,4965371.80003252],[-8227697.158028173,4965372.66978711],[-8227696.1201228,4965374.582550539],[-8227695.922272969,4965376.065393402],[-8227692.410599996,4965384.66584748],[-8227690.533798056,4965387.433957538],[-8227689.19524487,4965391.148733008],[-8227687.9389175065,4965392.431113813],[-8227685.690763562,4965400.490213647],[-8227684.862516939,4965402.296108719],[-8227685.839068684,4965404.825644412],[-8227684.0753147565,4965408.438383291],[-8227681.555378896,4965410.260755831],[-8227680.521199403,4965412.703779221],[-8227678.519970986,4965413.463281761],[-8227676.20979671,4965415.177503882],[-8227675.293159594,4965418.78211929],[-8227673.402864219,4965420.175411904],[-8227671.836636658,4965422.306680016],[-8227673.22992937,4965424.197050081],[-8227673.777500039,4965426.095571825],[-8227673.608910413,4965430.432704679],[-8227673.0131274285,4965434.457133068],[-8227671.32477001,4965434.791825733],[-8227671.0356685575,4965437.649150593],[-8227672.840644579,4965438.372673179],[-8227673.3847969,4965440.0590836],[-8227674.021019986,4965451.05195316],[-8227674.85604715,4965460.668183456],[-8227676.201111072,4965468.481458471],[-8227676.908942907,4965473.473478976],[-8227677.519037933,4965478.138734792],[-8227678.683477879,4965487.526437184],[-8227681.2306754,4965499.367794329],[-8227684.205773032,4965511.103988311],[-8227685.160962803,4965515.798210137],[-8227683.45131375,4965518.036074038],[-8227683.768407181,4965521.662673698],[-8227686.425910507,4965530.198872841],[-8227690.361449458,4965540.228640896],[-8227695.2572004665,4965551.538880382],[-8227695.785586144,4965556.553655597],[-8227698.021964264,4965559.755617208],[-8227701.11323016,4965566.382800932],[-8227708.5047700675,4965579.4038671125],[-8227716.195300286,4965589.034989654],[-8227725.385002526,4965597.278163248],[-8227728.695269942,4965602.307925178],[-8227730.619671668,4965603.5934004625],[-8227731.257814331,4965607.550811722],[-8227732.751965429,4965609.9043811895],[-8227734.031593879,4965614.290311434],[-8227736.2723396905,4965618.998094039],[-8227739.263997672,4965622.850689648],[-8227738.939967748,4965626.272348548],[-8227741.07408867,4965630.552088378],[-8227744.2785403,4965634.297985808],[-8227746.521755688,4965637.937342081],[-8227745.984477375,4965640.395993534],[-8227746.410092994,4965642.855450826],[-8227751.96356977,4965651.416246817],[-8227753.458238897,4965653.343235251],[-8227754.953280735,4965656.017762307],[-8227754.309987369,4965657.514839678],[-8227755.379331665,4965658.050806404],[-8227756.12868912,4965657.516536301],[-8227759.86568252,4965663.82907446],[-8227765.420845717,4965669.074771815],[-8227768.840318075,4965673.248859315],[-8227774.823530621,4965678.494916076],[-8227782.30187724,4965686.4155769525],[-8227792.231507621,4965694.461503513],[-8227808.694250206,4965678.348241156],[-8227819.816775845,4965667.462199565],[-8227826.813863735,4965659.057199409],[-8227833.272343079,4965650.231605949],[-8227839.167064342,4965641.019810062],[-8227844.474938185,4965631.457845456],[-8227854.497673923,4965616.921750456],[-8227865.056664536,4965602.770422275],[-8227876.137154012,4965589.023632353],[-8227887.723776771,4965575.700540753],[-8227899.800226589,4965562.8198141],[-8227912.349627583,4965550.399448382],[-8227987.373845994,4965472.5934362225],[-8228048.105025386,4965410.53555106],[-8228069.939381398,4965390.011698639],[-8228078.902708686,4965395.003225473],[-8228085.238871256,4965398.215771817],[-8228186.49892614,4965454.874360699],[-8228322.4119671825,4965530.068266575],[-8228422.012576654,4965585.941488207],[-8228561.903677982,4965661.193279894],[-8228676.530082352,4965549.666018753],[-8228703.238392799,4965522.425684087],[-8228653.806035338,4965367.611104439],[-8228836.571785923,4965201.780837364],[-8228907.230101147,4965139.171180392],[-8228917.790109278,4965130.682756311],[-8228928.677465177,4965122.618496572],[-8228939.875142301,4965114.991011074],[-8228951.3656994505,4965107.81222877],[-8228963.131096555,4965101.093375546],[-8228975.152934331,4965094.844957373],[-8228997.648508574,4965140.4868442295],[-8229162.159487753,4965075.8671342125],[-8229426.7707819855,4965001.830381667],[-8229448.409674965,4964983.858143642],[-8229516.274759592,4964919.383171336],[-8229708.181309753,4964741.9750058],[-8229633.7387927305,4964662.488343552],[-8229545.95754387,4964565.004632976],[-8229506.022005668,4964483.201854216],[-8229473.637497897,4964416.865761053],[-8229405.892848653,4964277.473623943],[-8229334.899207966,4964131.8927654745],[-8229267.123428889,4963992.608617591],[-8229264.584499331,4963987.146150913],[-8229262.494531669,4963982.578468482],[-8229257.432517095,4963972.924386077],[-8229250.280029899,4963958.7142546605],[-8229191.122201809,4963834.867058095],[-8229141.247826629,4963738.871832228],[-8229066.102698546,4963594.234213773],[-8229058.244506355,4963585.883680197],[-8229050.5843558535,4963397.467680867],[-8229046.1397315515,4963288.141355901],[-8229045.7669046465,4963264.832177968],[-8229046.287707969,4963241.525892526],[-8229047.701369839,4963218.256746821],[-8229049.667917044,4963197.410233774],[-8229051.73666765,4963181.206975135],[-8229054.272140406,4963165.070190675],[-8229057.272221137,4963149.013320819],[-8229028.63752059,4963134.879039684],[-8228997.7582727205,4963120.435102699]]]]},"properties":{"shape_area":14437615.1412,"ntaname":"Inwood","cdtaname":"MN12 Washington Heights-Inwood (CD 12 Equivalent)","shape_leng":25462.4445264,"boroname":"Manhattan","ntatype":0,"nta2020":"MN1203","borocode":1,"countyfips":"061","ntaabbrev":"Inwd","cdta2020":"MN12"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8228997.7582727205,4963120.435102699],[-8229028.63752059,4963134.879039684],[-8229057.272221137,4963149.013320819],[-8229054.272140406,4963165.070190675],[-8229051.73666765,4963181.206975135],[-8229049.667917044,4963197.410233774],[-8229047.701369839,4963218.256746821],[-8229046.287707969,4963241.525892526],[-8229045.7669046465,4963264.832177968],[-8229046.1397315515,4963288.141355901],[-8229050.5843558535,4963397.467680867],[-8229058.244506355,4963585.883680197],[-8229066.102698546,4963594.234213773],[-8229141.247826629,4963738.871832228],[-8229191.122201809,4963834.867058095],[-8229250.280029899,4963958.7142546605],[-8229257.432517095,4963972.924386077],[-8229262.494531669,4963982.578468482],[-8229264.584499331,4963987.146150913],[-8229268.773841165,4963985.13389394],[-8229270.261749358,4963978.6504320055],[-8229285.378543124,4963942.160862546],[-8229286.164248106,4963916.358035962],[-8229289.848248138,4963885.578278599],[-8229294.347892204,4963864.373496679],[-8229295.7644146,4963855.416491783],[-8229303.014789424,4963826.757436949],[-8229312.894462867,4963798.764245617],[-8229325.294567743,4963771.79336328],[-8229340.045814437,4963746.175682777],[-8229356.918993843,4963722.204265087],[-8229375.645795251,4963700.119523088],[-8229395.923252279,4963680.10903078],[-8229417.43131367,4963662.296781579],[-8229437.736099334,4963648.728645236],[-8229456.866012143,4963632.730995851],[-8229474.399151252,4963614.46278541],[-8229489.949915808,4963594.182377904],[-8229503.188497633,4963572.230811693],[-8229513.870934605,4963549.014972305],[-8229521.84885517,4963524.984730695],[-8229527.074416506,4963500.599663985],[-8229529.601324087,4963476.311100578],[-8229506.107837063,4963388.215852396],[-8229498.215698381,4963398.145545435],[-8229490.71403969,4963408.373453645],[-8229483.61411825,4963418.884231293],[-8229476.926614799,4963429.662096642],[-8229470.661538519,4963440.6908891],[-8229464.828291495,4963451.954060975],[-8229458.415290376,4963459.800406298],[-8229451.263890355,4963466.980194177],[-8229443.44300085,4963473.424238516],[-8229435.027983024,4963479.070442741],[-8229426.099923471,4963483.864398304],[-8229416.744852925,4963487.7599090105],[-8229407.0529171545,4963490.719436318],[-8229397.117491709,4963492.714447337],[-8229387.034333077,4963493.725729385],[-8229376.900603605,4963493.743537334],[-8229366.81395297,4963492.767699598],[-8229356.871577196,4963490.807619737],[-8229347.169281869,4963487.882185856],[-8229337.800558986,4963484.019588587],[-8229328.909167776,4963478.884242915],[-8229320.533205261,4963472.945240904],[-8229312.745089256,4963466.253932715],[-8229305.612118959,4963458.868173062],[-8229299.195999875,4963450.851817258],[-8229293.552203799,4963442.274175951],[-8229259.699470091,4963356.476692677],[-8229256.53251185,4963344.8084559515],[-8229254.234966709,4963332.938396697],[-8229252.819352304,4963320.931203106],[-8229252.293379898,4963308.8523101825],[-8229252.659933541,4963296.767571914],[-8229253.916989304,4963284.742817879],[-8229256.057693524,4963272.843577314],[-8229259.070377114,4963261.134695046],[-8229268.443614437,4963240.18663039],[-8229278.481138867,4963219.548683591],[-8229289.1727037495,4963199.241911981],[-8229300.50745064,4963179.28705521],[-8229312.473746615,4963159.704450141],[-8229325.059377774,4963140.51407611],[-8229493.66276861,4962832.31752287],[-8229542.64000499,4962744.0929222405],[-8229593.436026,4962652.08429923],[-8229519.686788114,4962610.007535967],[-8229522.304008141,4962553.251477873],[-8229524.881705223,4962497.352326639],[-8229569.073449764,4962389.163593595],[-8229617.450863477,4962318.679385274],[-8229655.054898381,4962263.891143842],[-8229701.5157779,4962225.1412190115],[-8229736.678564205,4962178.316583473],[-8229817.372793716,4962012.14724226],[-8229919.09235724,4962064.682143598],[-8229948.394180626,4962011.608663648],[-8229979.253835473,4961956.146146229],[-8230005.083974527,4961910.354430898],[-8230055.906767506,4961818.6457409505],[-8230063.139285382,4961805.143102427],[-8230070.371783081,4961791.640673686],[-8230073.709805401,4961785.41796309],[-8230077.049101618,4961779.193758298],[-8230080.67295688,4961773.648526079],[-8230083.82946533,4961768.085472944],[-8230093.369296297,4961751.276859034],[-8230099.343456044,4961739.774631921],[-8230108.7891215645,4961723.546690865],[-8230158.332060896,4961633.716551021],[-8230200.659818242,4961557.612658011],[-8230209.792821853,4961541.19161621],[-8230263.566110225,4961443.923676258],[-8230318.830271261,4961344.615479168],[-8230340.69733375,4961306.250809459],[-8230369.934594754,4961254.9552505575],[-8230422.1558190705,4961160.3253884055],[-8230469.666806462,4961072.501188901],[-8230474.573595972,4961057.325289954],[-8230474.577461942,4961057.310731176],[-8230474.58029549,4961057.29762044],[-8230477.423283161,4961041.377143016],[-8230477.424590519,4961041.369089669],[-8230478.067350274,4961025.143511834],[-8230478.068816629,4961025.119620145],[-8230478.0661810385,4961025.097247909],[-8230476.4939585645,4961009.044268014],[-8230476.490105899,4961009.007333357],[-8230476.480901171,4961008.968945991],[-8230472.793831159,4960993.475437017],[-8230455.128318235,4960931.608510167],[-8230439.439054887,4960891.077485403],[-8230427.903487616,4960870.683941444],[-8230427.891667472,4960870.662937853],[-8230427.88235208,4960870.641764221],[-8230418.425748088,4960848.409760598],[-8230411.423932942,4960824.648573508],[-8230411.421286812,4960824.640689309],[-8230411.420089474,4960824.632781626],[-8230407.19490718,4960799.7874838915],[-8230407.192192376,4960799.772920641],[-8230407.1910009775,4960799.756983429],[-8230405.953294601,4960774.313296808],[-8230405.953303383,4960774.301461237],[-8230405.95468729,4960774.28955302],[-8230407.797696658,4960748.773153916],[-8230412.68412293,4960723.747231759],[-8230420.435238243,4960699.767643625],[-8230420.441835606,4960699.745180721],[-8230420.452410918,4960699.722671671],[-8230430.7854355015,4960677.236485954],[-8230430.789296447,4960677.228606688],[-8230430.793328315,4960677.222004404],[-8230443.344941737,4960656.586821136],[-8230443.35548956,4960656.568142694],[-8230443.368786354,4960656.551037778],[-8230457.716620759,4960638.02503837],[-8230477.474861406,4960620.878169706],[-8230496.071162076,4960602.344866644],[-8230513.363163566,4960582.545824017],[-8230529.226368112,4960561.625420383],[-8230543.5590343475,4960539.740082014],[-8230560.525325614,4960506.360792959],[-8230594.769201214,4960410.081082938],[-8230631.811096706,4960303.919574014],[-8230671.80506259,4960189.856969559],[-8230712.809717797,4960072.580719435],[-8230725.149594111,4960035.45534956],[-8230728.887585796,4960022.19099062],[-8230733.408269994,4960009.172592013],[-8230738.695621409,4959996.446292413],[-8230744.730897731,4959984.057194914],[-8230751.492706124,4959972.04920718],[-8230758.957091825,4959960.464853355],[-8230767.097587339,4959949.34522217],[-8230775.885339398,4959938.729721438],[-8230785.289201046,4959928.655972149],[-8230795.275842069,4959919.159675034],[-8230869.070726034,4959836.233102961],[-8230877.264303386,4959829.464323029],[-8230885.002585534,4959822.179412707],[-8230892.253120984,4959814.408924306],[-8230898.985504025,4959806.185446464],[-8230905.171502099,4959797.543467422],[-8230910.785177764,4959788.519223736],[-8230915.802986519,4959779.150567966],[-8230920.203886907,4959769.476790489],[-8230923.969424744,4959759.5384610295],[-8230927.083810521,4959749.377258529],[-8230936.548486628,4959723.622794834],[-8230975.201086301,4959654.170064835],[-8230981.6926578935,4959643.214131989],[-8230987.544334593,4959631.903496479],[-8230992.736803994,4959620.275492538],[-8230997.252994243,4959608.368437332],[-8231001.077932222,4959596.221693432],[-8231004.198995618,4959583.875354133],[-8231001.713941371,4959422.912867843],[-8230978.879947484,4959410.330517514],[-8230970.8832891015,4959427.62203583],[-8230962.864531176,4959463.479007543],[-8230961.363778009,4959469.137253548],[-8230958.884374013,4959478.160398422],[-8230955.81029115,4959486.998654028],[-8230952.155143045,4959495.612874649],[-8230948.046966534,4959502.872950892],[-8230943.520910647,4959509.880142988],[-8230938.5924920915,4959516.610426175],[-8230933.145662309,4959523.496407965],[-8230915.521735345,4959548.044119128],[-8230908.095740817,4959557.993538888],[-8230901.4706689045,4959568.493305249],[-8230895.687112202,4959579.479090741],[-8230890.780507975,4959590.883589857],[-8230886.780920939,4959602.636931277],[-8230877.273055511,4959635.416209814],[-8230872.804887763,4959648.569820936],[-8230868.558737337,4959660.679813735],[-8230863.704229737,4959672.55903426],[-8230858.253842583,4959684.176942888],[-8230846.107608918,4959710.093638459],[-8230826.627248747,4959746.129552473],[-8230805.131208062,4959780.639379729],[-8230794.7708598245,4959794.684176316],[-8230784.031296948,4959808.441203041],[-8230772.92050834,4959821.900223141],[-8230761.44671313,4959835.051247361],[-8230749.618491876,4959847.884462386],[-8230737.444643756,4959860.390318615],[-8230662.06698907,4959938.523515712],[-8230629.904984334,4959970.356164952],[-8230585.068907388,4960017.243241525],[-8230487.396665469,4960111.533789973],[-8230440.824776126,4960158.63725233],[-8230413.6052867295,4960187.941238761],[-8230402.404061455,4960200.000178049],[-8230390.516272967,4960214.223167239],[-8230370.253856637,4960244.699094438],[-8230365.931883969,4960242.542894588],[-8230345.592478461,4960232.395550714],[-8230335.455053823,4960264.613689825],[-8230330.014484319,4960281.868950347],[-8230326.9002491925,4960291.4458078835],[-8230317.299437286,4960319.14018126],[-8230306.651465837,4960347.4833157575],[-8230296.899885186,4960372.135580728],[-8230285.161097964,4960401.228842474],[-8230270.205232105,4960436.175941503],[-8230254.968823381,4960466.682910717],[-8230243.042338239,4960494.075032201],[-8230235.848015798,4960512.399497983],[-8230230.452401938,4960524.206700213],[-8230223.636468572,4960541.207765172],[-8230213.601427018,4960565.011515545],[-8230202.621594706,4960590.135757257],[-8230194.953654038,4960607.987554734],[-8230179.430257903,4960643.50358565],[-8230161.154436598,4960693.041415082],[-8230090.628856863,4960858.866756792],[-8230054.182744241,4960944.1617916245],[-8230020.194821525,4961024.546680639],[-8230001.225239466,4961068.9556586705],[-8229986.02003436,4961104.554173917],[-8229958.477536109,4961173.335739719],[-8229949.546479716,4961193.550754446],[-8229940.1447478635,4961219.881976975],[-8229939.237656572,4961226.916804317],[-8229937.4300522525,4961230.884605176],[-8229935.985301882,4961233.408489018],[-8229930.924025551,4961252.384436874],[-8229927.66183964,4961264.611611693],[-8229925.128295703,4961273.720174033],[-8229919.883195364,4961291.395785394],[-8229914.455955317,4961310.695030418],[-8229913.369601109,4961315.7445079945],[-8229910.656920618,4961324.041059946],[-8229905.590517068,4961342.672687417],[-8229900.344893667,4961361.070549358],[-8229894.373060511,4961382.713889552],[-8229885.5092684,4961414.278647622],[-8229880.443748278,4961431.773848363],[-8229874.83671668,4961449.5036555305],[-8229866.878956323,4961473.6706317635],[-8229860.913322696,4961489.181403035],[-8229855.128556817,4961504.6921924325],[-8229848.800872994,4961520.562894355],[-8229841.39021896,4961538.868394956],[-8229835.426284759,4961552.033438508],[-8229829.281258638,4961565.739590328],[-8229823.859906093,4961577.190393749],[-8229818.61844176,4961588.372033219],[-8229813.921581788,4961596.84823395],[-8229807.406673655,4961608.923831677],[-8229786.330715293,4961648.8233066425],[-8229781.023978883,4961658.927353545],[-8229774.839118096,4961670.936648309],[-8229770.40991312,4961679.616156607],[-8229685.231027345,4961838.632940992],[-8229681.856157865,4961844.933395595],[-8229677.967880858,4961852.191951441],[-8229674.293245177,4961859.698744801],[-8229670.898099711,4961866.9622058505],[-8229651.464194074,4961903.303960347],[-8229642.6763155265,4961919.648752124],[-8229624.396379314,4961954.172183281],[-8229588.72630589,4962021.645146438],[-8229574.208545852,4962049.79326066],[-8229560.934581693,4962075.87255884],[-8229544.344906857,4962106.090306715],[-8229528.997596202,4962136.101854942],[-8229507.428191855,4962177.496870815],[-8229484.202017629,4962218.890604353],[-8229459.730283279,4962263.804200201],[-8229433.182832637,4962313.892473085],[-8229431.189624287,4962317.7403237205],[-8229407.8796115,4962362.73970677],[-8229392.94506068,4962392.337644969],[-8229391.2851160485,4962397.721046867],[-8229388.3828937495,4962401.445405579],[-8229377.452395042,4962423.167366049],[-8229375.331097756,4962427.382890896],[-8229367.125445527,4962444.075830744],[-8229364.600292259,4962448.485480562],[-8229353.869392335,4962469.586777841],[-8229352.606187837,4962473.996452613],[-8229347.873422924,4962481.869620538],[-8229340.613803171,4962495.884782563],[-8229332.092298283,4962512.8929101955],[-8229326.412272506,4962522.970124848],[-8229324.201228672,4962528.955146008],[-8229317.573369354,4962542.182681837],[-8229312.523581035,4962551.632074287],[-8229310.631564269,4962553.520861035],[-8229309.683597996,4962556.669921459],[-8229305.580074587,4962564.859387676],[-8229301.477427718,4962572.10218271],[-8229297.060336136,4962579.661728763],[-8229295.796158507,4962583.755730201],[-8229290.459150709,4962590.968522122],[-8229256.6450427165,4962654.84881224],[-8229231.090491365,4962703.790752395],[-8229183.082937435,4962791.239097289],[-8229138.172202261,4962870.443372591],[-8229117.009212606,4962906.891637242],[-8229085.521752012,4962960.208362187],[-8229059.7130216975,4963003.61023123],[-8229050.664454935,4963018.73626955],[-8229033.904216347,4963046.753476558],[-8229003.966152858,4963095.176623089],[-8228990.426573815,4963116.425591826],[-8228992.36120173,4963117.7226698585],[-8228997.7582727205,4963120.435102699]]]},"properties":{"shape_area":7750591.84552,"ntaname":"Highbridge Park","cdtaname":"MN12 Washington Heights-Inwood (CD 12 Equivalent)","shape_leng":27454.0830968,"boroname":"Manhattan","ntatype":9,"nta2020":"MN1291","borocode":1,"countyfips":"061","ntaabbrev":"HghbrdgPk","cdta2020":"MN12"}}, +{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-8227792.231507621,4965694.461503513],[-8227799.2171243485,4965699.587967079],[-8227805.923370125,4965704.101529755],[-8227854.906995158,4965736.697758794],[-8227878.395574427,4965751.98932338],[-8227902.174127893,4965765.262587394],[-8227914.190008601,4965770.82121263],[-8227927.638320815,4965776.359732033],[-8227945.263075717,4965780.878627107],[-8227955.31860681,4965782.265963759],[-8227961.973499226,4965782.040972238],[-8227968.16394463,4965783.790247497],[-8227973.393183326,4965786.005255413],[-8227979.140417505,4965789.6023924025],[-8227985.2311964845,4965794.753092321],[-8227990.22130487,4965796.965203559],[-8227995.939927505,4965798.2306007575],[-8228012.896868803,4965804.698842346],[-8228022.660162321,4965810.133717383],[-8228058.015392511,4965820.446390027],[-8228068.515466655,4965824.93191256],[-8228090.8669490805,4965834.181354837],[-8228093.51969399,4965835.684240319],[-8228139.289117428,4965854.311952682],[-8228178.156683336,4965868.687098144],[-8228187.932242817,4965872.2743424],[-8228205.099866023,4965877.776783897],[-8228224.894315337,4965880.1811052915],[-8228244.450145529,4965882.585157107],[-8228256.374741818,4965883.789075328],[-8228265.43826716,4965883.797795078],[-8228279.273206214,4965882.618655924],[-8228293.346215416,4965881.916492336],[-8228302.449473745,4965879.761978167],[-8228315.099378185,4965880.264267011],[-8228329.718237433,4965883.236448619],[-8228339.987754973,4965881.161114555],[-8228345.651465973,4965877.812148044],[-8228359.281106701,4965875.099731237],[-8228368.721065773,4965868.819309267],[-8228379.417742846,4965864.007594185],[-8228391.371444998,4965859.826026526],[-8228399.128028112,4965860.252658758],[-8228408.984319251,4965857.1173233325],[-8228412.764103866,4965850.621871014],[-8228420.524051091,4965847.484493489],[-8228430.172540838,4965842.462104004],[-8228436.253296658,4965841.4196077],[-8228447.78873348,4965836.189347329],[-8228459.114347529,4965831.168516266],[-8228466.039734517,4965823.627785417],[-8228471.915076539,4965817.9729031995],[-8228479.258659308,4965811.690439398],[-8228484.082306495,4965809.808136958],[-8228499.810244817,4965792.478909127],[-8228501.326096936,4965800.5822237395],[-8228501.910971257,4965800.908909596],[-8228481.761116294,4965840.507829774],[-8228479.684951113,4965839.441376447],[-8228458.3727699965,4965882.683362324],[-8228467.065580119,4965886.877689877],[-8228488.506204908,4965843.706247787],[-8228486.811665822,4965842.860233219],[-8228506.220021629,4965803.316293613],[-8228507.561920612,4965803.940246525],[-8228518.2567250775,4965791.344252368],[-8228519.8709965795,4965786.253501723],[-8228520.680990751,4965784.39430758],[-8228525.7017148575,4965783.992873136],[-8228527.77833302,4965782.438422162],[-8228530.5040708985,4965780.236136276],[-8228532.194175769,4965775.828050383],[-8228534.012955487,4965772.587323919],[-8228536.219964919,4965770.254861086],[-8228541.539846648,4965767.795537318],[-8228546.9900460085,4965764.558248733],[-8228550.624423572,4965761.578577487],[-8228553.351238466,4965758.209005846],[-8228555.688053583,4965755.746970204],[-8228557.24615431,4965753.802980393],[-8228559.84073523,4965753.156933305],[-8228560.620846552,4965750.952833241],[-8228561.401854244,4965747.970504294],[-8228561.404028249,4965745.636012717],[-8228558.813194695,4965742.26146697],[-8228559.9821454445,4965740.576502178],[-8228562.964809632,4965740.968333138],[-8228564.648954488,4965743.045032791],[-8228566.334450385,4965743.565372417],[-8228569.320760462,4965740.066425139],[-8228572.825136992,4965737.216324759],[-8228576.588923105,4965734.49623138],[-8228577.887655999,4965732.552001372],[-8228580.61144023,4965732.4248403935],[-8228582.300550176,4965729.054306383],[-8228583.991057979,4965724.257084062],[-8228584.125448891,4965719.199107416],[-8228582.314407906,4965714.139252357],[-8228580.241212392,4965712.062200367],[-8228573.3717233585,4965707.386795401],[-8228569.223288425,4965705.437504432],[-8228563.90912284,4965701.801079943],[-8228560.539302767,4965699.3336445885],[-8228558.33968516,4965693.754763648],[-8228557.694391743,4965690.382072324],[-8228559.641389266,4965688.697816811],[-8228563.1431671865,4965688.701077858],[-8228567.6826474825,4965688.445919503],[-8228569.775373768,4965687.874608246],[-8228570.212182298,4965688.827374621],[-8228570.669751248,4965690.04340141],[-8228571.609063356,4965691.07630789],[-8228572.74901529,4965691.029879183],[-8228573.260407805,4965691.0090056285],[-8228575.43806313,4965692.34126417],[-8228577.155994245,4965694.590422262],[-8228577.095366923,4965695.64173372],[-8228577.03955962,4965696.608611127],[-8228577.611261804,4965698.283371608],[-8228579.444019873,4965700.509691321],[-8228581.300511811,4965701.704048475],[-8228582.855542705,4965701.75379312],[-8228584.474877147,4965701.475153599],[-8228585.494623257,4965701.109086085],[-8228586.351231502,4965700.620667971],[-8228586.800885594,4965699.357109621],[-8228587.576820781,4965698.093928112],[-8228588.882550411,4965696.994268803],[-8228591.573769757,4965696.711316778],[-8228592.879105091,4965696.141750544],[-8228593.5316488845,4965695.8977125315],[-8228594.3062382145,4965695.89843129],[-8228594.591334474,4965696.306443301],[-8228594.550009485,4965696.917989351],[-8228593.937769202,4965697.651278369],[-8228593.039900759,4965698.629044248],[-8228591.978867951,4965699.565827445],[-8228591.774085065,4965700.5849696435],[-8228592.588751231,4965701.319583116],[-8228593.5664795395,4965702.258283172],[-8228593.553571555,4965702.719158568],[-8228593.524346697,4965703.7667904375],[-8228593.523741305,4965704.419181461],[-8228594.257257852,4965704.786803222],[-8228594.868161285,4965705.521301455],[-8228596.212959813,4965706.215673826],[-8228597.354244828,4965706.746777782],[-8228598.535450105,4965707.889479162],[-8228599.390354933,4965709.317353162],[-8228600.490431132,4965710.256166752],[-8228601.794786921,4965710.665025939],[-8228603.140002398,4965710.910916948],[-8228604.444787236,4965710.83058657],[-8228605.546073224,4965710.464665882],[-8228606.443110244,4965710.383957366],[-8228607.462255733,4965710.507174515],[-8228608.563490564,4965710.222817808],[-8228609.868652881,4965709.734812704],[-8228610.318002193,4965708.797460045],[-8228611.134403828,4965707.819616115],[-8228612.35766167,4965707.698451595],[-8228613.988504726,4965707.699961665],[-8228616.068315428,4965707.3349704],[-8228617.481974181,4965706.175977187],[-8228625.370024574,4965714.577573252],[-8228631.284341086,4965722.245968762],[-8228636.500940022,4965730.958657263],[-8228642.413272615,4965740.717002548],[-8228645.890464098,4965747.338223321],[-8228650.06818193,4965749.431950389],[-8228652.157806543,4965749.782222211],[-8228655.984143918,4965755.358829809],[-8228652.847975826,4965756.749123834],[-8228654.229773352,4965769.289854787],[-8228653.521300666,4965782.176947609],[-8228648.981805548,4965794.363929812],[-8228647.2290244475,4965806.553471586],[-8228647.218100071,4965818.396213981],[-8228646.513470483,4965827.103617378],[-8228645.117855191,4965829.540543882],[-8228641.9776541265,4965835.459120402],[-8228638.14115083,4965840.680286178],[-8228637.088236881,4965849.3873872105],[-8228634.993863531,4965854.261917356],[-8228635.338970153,4965857.745405502],[-8228632.548144585,4965862.271020593],[-8228629.408470989,4965867.492939395],[-8228625.225728196,4965870.62399544],[-8228618.945894773,4965881.416065573],[-8228612.502263994,4965888.0832899725],[-8228608.609466345,4965897.04055616],[-8228611.553840378,4965905.9893734185],[-8228619.267244039,4965910.675430158],[-8228625.531537607,4965916.602748287],[-8228636.324008194,4965922.534175431],[-8228640.152333698,4965926.020904175],[-8228645.376291764,4965927.070755038],[-8228654.081443093,4965930.213651304],[-8228670.44456304,4965938.936848986],[-8228682.9771951,4965946.611476502],[-8228687.852743304,4965947.660912988],[-8228691.683654897,4965948.361036161],[-8228695.163412429,4965952.195784865],[-8228699.342924065,4965952.54797114],[-8228703.172241847,4965954.989727735],[-8228708.044285876,4965959.870710084],[-8228716.397774382,4965966.844873777],[-8228723.36158854,4965969.637847619],[-8228731.373134206,4965969.6451720465],[-8228734.159978742,4965969.299389983],[-8228742.516037304,4965973.486911951],[-8228750.871027368,4965978.71941359],[-8228760.967401184,4965984.30182593],[-8228765.841000439,4965987.441224013],[-8228769.6671791645,4965993.366202697],[-8228779.763920071,4965998.600274865],[-8228790.557597522,4966003.486593349],[-8228799.956630702,4966009.765055114],[-8228804.484816046,4966009.7691601785],[-8228809.3596097585,4966011.863506968],[-8228817.018348871,4966016.746987131],[-8228821.543411111,4966020.234382513],[-8228826.493684072,4966023.093678105],[-8228834.229894002,4966021.107585318],[-8228836.310735374,4966020.210213623],[-8228837.309342606,4966021.432629601],[-8228838.418828064,4966021.2117544925],[-8228838.97638672,4966019.54691632],[-8228840.197573784,4966018.548307819],[-8228840.309725963,4966017.217246655],[-8228843.0866564205,4966015.998165458],[-8228844.642545948,4966014.445179302],[-8228845.643380956,4966011.782382875],[-8228846.977821369,4966009.563567445],[-8228849.3098658575,4966008.344157865],[-8228850.754528958,4966007.012948177],[-8228852.088817465,4966003.461670353],[-8228854.424262678,4965999.911246803],[-8228854.980266606,4965998.358785375],[-8228856.980506194,4965996.028343619],[-8228859.757499378,4965994.921562059],[-8228859.980551079,4965993.589154238],[-8228860.647305467,4965992.25731817],[-8228859.649571416,4965990.147330689],[-8228860.429228191,4965988.039042702],[-8228862.206061761,4965987.485891073],[-8228862.762462896,4965985.59925392],[-8228862.985765931,4965983.823267499],[-8228864.31877157,4965983.380740121],[-8228864.097296459,4965982.93541294],[-8228865.874480502,4965981.827804748],[-8228865.653678137,4965980.716875024],[-8228866.985631499,4965979.941520315],[-8228866.875607119,4965978.830788806],[-8228867.320356757,4965977.721907921],[-8228866.989628227,4965975.500296798],[-8228866.990826157,4965974.169141346],[-8228868.324058761,4965973.281560405],[-8228867.550202884,4965970.284399291],[-8228867.551701447,4965968.6190677965],[-8228866.664420626,4965966.953010477],[-8228865.445138164,4965964.3991031125],[-8228863.674105597,4965960.178139748],[-8228860.791794078,4965955.070076139],[-8228857.133134694,4965949.8489400875],[-8228854.194479935,4965947.238159088],[-8228851.643703518,4965943.461550473],[-8228849.871803356,4965938.796793767],[-8228848.096527909,4965937.685818059],[-8228846.656182035,4965935.6852614675],[-8228846.65848473,4965933.132388029],[-8228844.996164204,4965929.800309461],[-8228842.220087015,4965930.019679344],[-8228841.2236672,4965927.798853238],[-8228840.0040694345,4965925.688702008],[-8228839.56205049,4965923.690178088],[-8228838.673475299,4965923.467648681],[-8228838.565029042,4965920.803728882],[-8228839.456209904,4965918.139582128],[-8228839.124789314,4965916.584982218],[-8228837.57202018,4965914.807257982],[-8228836.020126605,4965912.142258186],[-8228835.467658782,4965909.699888456],[-8228835.024739381,4965907.367290139],[-8228833.473322372,4965905.591066799],[-8228831.643257413,4965903.757692128],[-8228830.643434417,4965903.867639802],[-8228830.201316076,4965902.091067681],[-8228830.535245157,4965900.758871699],[-8228829.093779151,4965898.648573581],[-8228827.65133758,4965897.536646554],[-8228827.652340394,4965896.427277319],[-8228826.877500172,4965894.539554633],[-8228825.76823247,4965893.095105785],[-8228826.215188957,4965890.986368721],[-8228824.551677645,4965888.986820499],[-8228824.997606279,4965886.656381634],[-8228823.777040565,4965885.544631446],[-8228823.667122516,4965884.322961139],[-8228822.116742511,4965881.32509689],[-8228820.787208337,4965878.215157344],[-8228821.123045722,4965874.773976412],[-8228819.459638582,4965872.663481725],[-8228817.464338129,4965869.554293327],[-8228816.469283691,4965866.0008884175],[-8228814.694881061,4965863.8903189115],[-8228815.915848146,4965863.22452681],[-8228814.311184877,4965857.506135792],[-8228813.094212845,4965853.841708046],[-8228811.321569153,4965849.954954493],[-8228810.324288988,4965847.290347729],[-8228826.651382464,4965837.980396378],[-8228826.099597818,4965834.8711499125],[-8228824.436296804,4965832.649667088],[-8228822.438393068,4965832.425987659],[-8228806.555830753,4965840.847634583],[-8228804.262506934,4965836.763525992],[-8228800.976619123,4965832.894888043],[-8228797.366778211,4965827.703148647],[-8228792.402875255,4965829.883662503],[-8228789.499900328,4965826.511936686],[-8228785.886469435,4965822.090297221],[-8228781.937942808,4965818.7701728],[-8228775.904726133,4965813.8973568985],[-8228769.210892999,4965808.801550638],[-8228761.529010542,4965803.261049691],[-8228763.631384342,4965800.958798613],[-8228760.561970125,4965798.192617077],[-8228758.568648141,4965800.715560509],[-8228754.069040383,4965797.281353086],[-8228755.732961045,4965794.647114002],[-8228752.880373147,4965792.542150206],[-8228751.2166974945,4965795.176658697],[-8228736.624463443,4965783.769152209],[-8228735.138516531,4965783.597294738],[-8228733.937007682,4965781.38984048],[-8228734.7150954055,4965779.961862693],[-8228733.848170682,4965776.764413633],[-8228733.094684299,4965773.127380429],[-8228732.556018468,4965770.59297243],[-8228732.233579066,4965768.939687475],[-8228731.691612542,4965766.845383603],[-8228731.036936441,4965765.6307224585],[-8228731.601343892,4965762.5512333],[-8228732.055961834,4965759.359935437],[-8228730.300760808,4965757.920212097],[-8228728.439149616,4965755.7098257365],[-8228725.580604266,4965754.926573896],[-8228722.497273992,4965754.912403688],[-8228718.090991562,4965755.442424553],[-8228713.691423243,4965754.542069574],[-8228713.718165913,4965748.705451887],[-8228717.707365093,4965749.324302844],[-8228722.679923246,4965748.935597829],[-8228726.224524083,4965749.337081478],[-8228726.974271681,4965747.116284371],[-8228728.632191864,4965746.986731514],[-8228728.845655845,4965746.319717912],[-8228730.595084777,4965744.529646197],[-8228731.109571578,4965741.20613674],[-8228731.417093579,4965739.099643358],[-8228732.951880662,4965737.866128378],[-8228732.476638964,4965734.994768616],[-8228732.323012969,4965731.235534565],[-8228733.069959733,4965728.904282508],[-8228731.817396392,4965725.82076165],[-8228732.081603401,4965719.844441047],[-8228733.610036453,4965718.167428346],[-8228734.687400218,4965715.611392896],[-8228735.692470748,4965711.672972425],[-8228738.8495221725,4965707.212369459],[-8228742.321761803,4965701.309915108],[-8228743.820994872,4965696.977567811],[-8228746.222120785,4965694.185102776],[-8228748.30187816,4965692.28078042],[-8228750.714528302,4965690.372486809],[-8228751.682602126,4965688.038693174],[-8228753.545096875,4965686.4684138745],[-8228755.513119514,4965684.454737928],[-8228758.385566927,4965684.088818074],[-8228761.676102499,4965681.728347455],[-8228765.444224574,4965677.537172739],[-8228767.521525016,4965675.301087945],[-8228769.298763472,4965674.192156602],[-8228770.36939793,4965675.290462113],[-8228773.327372796,4965672.940300268],[-8228776.490308055,4965671.459914885],[-8228779.865845915,4965670.415745478],[-8228784.129736346,4965667.756088111],[-8228787.7261809595,4965666.281007152],[-8228791.327808609,4965664.480705751],[-8228795.261264234,4965662.2507985],[-8228798.655420727,4965659.579415378],[-8228800.180698533,4965659.056087016],[-8228803.9846555535,4965658.451835224],[-8228807.767509805,4965659.58180347],[-8228810.142713307,4965660.587584555],[-8228812.856305582,4965660.4037715895],[-8228813.493382913,4965661.495560932],[-8228815.21895932,4965662.111358607],[-8228815.670364093,4965662.161639149],[-8228816.123879277,4965662.136771525],[-8228816.567080222,4965662.037437027],[-8228816.987824863,4965661.866357069],[-8228817.374586353,4965661.6282185875],[-8228817.716768895,4965661.329545718],[-8228818.004997969,4965660.978520959],[-8228818.231377172,4965660.584761113],[-8228824.988676377,4965659.943854318],[-8228831.77567653,4965660.035943269],[-8228838.513099229,4965660.859952318],[-8228845.122245201,4965662.406256452],[-8228851.525913435,4965664.656793756],[-8228854.723656823,4965665.093413744],[-8228857.9510693755,4965665.09632167],[-8228861.149594387,4965664.665464772],[-8228876.511126285,4965671.850453445],[-8228891.422356294,4965679.928596443],[-8228905.831411828,4965688.871794702],[-8228919.688166842,4965698.648939954],[-8228932.944416139,4965709.226022899],[-8228945.554043053,4965720.5662515145],[-8228957.473179872,4965732.630178878],[-8228968.660360453,4965745.375840405],[-8228979.076664527,4965758.75889977],[-8228988.68585298,4965772.732803029],[-8228997.45449409,4965787.248940551],[-8229005.352079777,4965802.256816034],[-8229012.351131757,4965817.704222148],[-8229018.427297293,4965833.537422055],[-8229023.55943382,4965849.701336362],[-8229027.729682634,4965866.139734615],[-8229030.923531081,4965882.795430957],[-8229033.129863093,4965899.61048299],[-8229034.340997897,4965916.526393342],[-8229034.5527168475,4965933.484313168],[-8229033.764278213,4965950.425246847],[-8229031.978419714,4965967.290257258],[-8229029.201349185,4965984.0206707865],[-8229025.442722973,4966000.558281496],[-8229020.715612452,4966016.845553613],[-8229015.036458598,4966032.82582177],[-8229012.269891232,4966039.843131478],[-8229010.217642046,4966047.101567039],[-8229008.90021442,4966054.528617847],[-8229008.330771021,4966062.050088501],[-8229008.515002321,4966069.590840003],[-8229005.50388365,4966097.452560671],[-8229003.274414072,4966125.387776318],[-8229001.828350171,4966153.374562675],[-8229001.166833572,4966181.390954618],[-8229001.290390302,4966209.414963419],[-8229001.331710873,4966211.027796672],[-8229002.539890152,4966211.658026026],[-8229003.6681439495,4966214.149986813],[-8229005.827991944,4966214.9812951],[-8229006.531119706,4966217.265273059],[-8229012.485662647,4966221.750110941],[-8229014.206166753,4966224.226283486],[-8229019.272952189,4966227.508842906],[-8229022.689228794,4966225.39808005],[-8229025.242474731,4966226.705435309],[-8229024.163958457,4966228.503022415],[-8229023.651505829,4966231.082425227],[-8229024.391999645,4966231.179930242],[-8229023.990522136,4966232.622408057],[-8229024.537717836,4966233.737434934],[-8229025.741364777,4966234.830586027],[-8229027.193964949,4966233.842283484],[-8229028.367415982,4966234.213499694],[-8229028.664004505,4966236.418918606],[-8229031.062981388,4966234.114681209],[-8229034.105310474,4966234.63002197],[-8229038.998846448,4966236.382810921],[-8229043.065576599,4966239.601141047],[-8229045.729513177,4966242.216909709],[-8229047.797990086,4966242.863372266],[-8229049.841677188,4966243.314710805],[-8229049.272228147,4966244.976935295],[-8229047.838541502,4966247.086093361],[-8229048.80038776,4966248.343740085],[-8229049.624485351,4966247.109558192],[-8229051.957714928,4966247.257897314],[-8229055.12522855,4966248.218900047],[-8229060.155574812,4966250.251051823],[-8229062.230026817,4966251.425552736],[-8229067.283692441,4966255.105733223],[-8229070.452257626,4966260.492953004],[-8229070.605299763,4966261.13342841],[-8229078.841749695,4966267.232688893],[-8229081.727034228,4966270.840745194],[-8229084.400685985,4966274.054062723],[-8229087.6134844255,4966275.920071802],[-8229091.488278278,4966278.170453147],[-8229103.53126133,4966285.16465044],[-8229131.747004757,4966290.583005444],[-8229139.067682163,4966289.72257175],[-8229143.2248645,4966287.319538435],[-8229147.7045454625,4966285.590613082],[-8229152.398228754,4966284.577669251],[-8229157.192235127,4966284.305239824],[-8229161.970455657,4966284.77992285],[-8229166.617163695,4966285.990221831],[-8229182.599576531,4966292.938808804],[-8229226.990931341,4966304.631453934],[-8229236.133034017,4966307.46811347],[-8229245.530690108,4966309.287119414],[-8229255.070980148,4966310.0666145515],[-8229264.639270747,4966309.797232418],[-8229274.120591993,4966308.482209943],[-8229281.743922109,4966306.636205149],[-8229289.170851357,4966304.113589454],[-8229296.3414579155,4966300.934716402],[-8229301.509686822,4966304.090145899],[-8229302.502569643,4966304.696327919],[-8229305.908641182,4966300.872416684],[-8229308.724282195,4966296.595061857],[-8229310.889986595,4966291.954663258],[-8229312.15000521,4966287.94050998],[-8229312.922444409,4966283.804763801],[-8229322.6615324225,4966276.882420697],[-8229332.956210145,4966270.817102055],[-8229343.731257161,4966265.653123289],[-8229354.907943646,4966261.428214176],[-8229366.4046054315,4966258.173243242],[-8229378.137240738,4966255.911992245],[-8229390.020123847,4966254.660982572],[-8229401.966431332,4966254.429354416],[-8229413.888876465,4966255.218800137],[-8229420.045053921,4966255.034943002],[-8229426.152414805,4966255.8302229885],[-8229432.0562171955,4966257.584490368],[-8229437.606876664,4966260.25329788],[-8229442.66375601,4966263.769026739],[-8229447.098728554,4966268.042599849],[-8229450.799424397,4966272.965738526],[-8229453.672077688,4966278.413705819],[-8229455.643902258,4966284.248466841],[-8229455.147952761,4966289.763488449],[-8229453.773927629,4966295.127583753],[-8229451.557397689,4966300.201880876],[-8229448.555745715,4966304.855010183],[-8229446.157510994,4966305.823571538],[-8229443.949414824,4966307.170402563],[-8229441.99062042,4966308.859416776],[-8229440.3336113375,4966310.845359351],[-8229439.022785241,4966313.075019627],[-8229438.09326434,4966315.488656715],[-8229437.569954315,4966318.021600295],[-8229437.466876964,4966320.605983215],[-8229437.786794433,4966323.172560001],[-8229438.354470475,4966325.202515864],[-8229439.187288189,4966327.138850906],[-8229437.717113357,4966329.249969707],[-8229443.727559588,4966333.252669647],[-8229449.824067901,4966337.528820808],[-8229450.975779279,4966335.8525751075],[-8229458.316479936,4966337.048721076],[-8229463.78289036,4966331.987099979],[-8229472.385834065,4966320.832090309],[-8229504.797692862,4966278.15411314],[-8229575.086628358,4966203.929516849],[-8229623.524539573,4966190.312262315],[-8229631.365810289,4966185.143490884],[-8229643.594212453,4966176.356684741],[-8229664.02508489,4966151.562457213],[-8229692.996795612,4966113.8039252125],[-8229740.133004581,4966061.395708587],[-8229758.637026966,4966034.541207037],[-8229774.166723134,4965996.247600494],[-8229834.609024557,4965867.77782365],[-8229847.379240967,4965856.121847952],[-8229926.0766384825,4965682.765451852],[-8229944.032045223,4965643.2126030065],[-8229968.23598065,4965610.896067271],[-8229985.883859698,4965562.283908699],[-8230003.19219247,4965529.039102215],[-8230013.394891439,4965483.444288562],[-8230028.2320518065,4965452.084880988],[-8230048.592985035,4965415.503186521],[-8230062.994400901,4965385.450851476],[-8230076.107603065,4965330.8554818425],[-8230077.870408111,4965305.295268133],[-8230086.317308251,4965275.385353079],[-8230095.525661771,4965233.958381881],[-8230094.695867589,4965197.066461713],[-8230024.210470909,4965158.541274684],[-8230017.343943319,4965121.386897369],[-8230017.361589262,4965078.336579224],[-8230007.40162033,4965067.588864086],[-8229992.077529579,4965050.236130369],[-8229981.174943867,4965039.27805913],[-8229952.304048126,4965010.262574026],[-8229848.798550157,4964896.089434642],[-8229776.599296094,4964817.612684992],[-8229708.181309753,4964741.9750058],[-8229516.274759592,4964919.383171336],[-8229448.409674965,4964983.858143642],[-8229426.7707819855,4965001.830381667],[-8229162.159487753,4965075.8671342125],[-8228997.648508574,4965140.4868442295],[-8228975.152934331,4965094.844957373],[-8228963.131096555,4965101.093375546],[-8228951.3656994505,4965107.81222877],[-8228939.875142301,4965114.991011074],[-8228928.677465177,4965122.618496572],[-8228917.790109278,4965130.682756311],[-8228907.230101147,4965139.171180392],[-8228836.571785923,4965201.780837364],[-8228653.806035338,4965367.611104439],[-8228703.238392799,4965522.425684087],[-8228676.530082352,4965549.666018753],[-8228561.903677982,4965661.193279894],[-8228422.012576654,4965585.941488207],[-8228322.4119671825,4965530.068266575],[-8228186.49892614,4965454.874360699],[-8228085.238871256,4965398.215771817],[-8228078.902708686,4965395.003225473],[-8228069.939381398,4965390.011698639],[-8228048.105025386,4965410.53555106],[-8227987.373845994,4965472.5934362225],[-8227912.349627583,4965550.399448382],[-8227899.800226589,4965562.8198141],[-8227887.723776771,4965575.700540753],[-8227876.137154012,4965589.023632353],[-8227865.056664536,4965602.770422275],[-8227854.497673923,4965616.921750456],[-8227844.474938185,4965631.457845456],[-8227839.167064342,4965641.019810062],[-8227833.272343079,4965650.231605949],[-8227826.813863735,4965659.057199409],[-8227819.816775845,4965667.462199565],[-8227808.694250206,4965678.348241156],[-8227792.231507621,4965694.461503513]]],[[[-8229140.820118288,4966534.419333676],[-8229143.335496666,4966526.738943163],[-8229144.432923044,4966526.739899593],[-8229146.163387432,4966519.999600888],[-8229144.910044097,4966519.687260907],[-8229146.32534314,4966514.198710051],[-8229141.623109062,4966512.626752178],[-8229140.207044761,4966518.426646912],[-8229139.423400827,4966518.112307964],[-8229137.222424112,4966524.852193798],[-8229138.790060202,4966525.167216846],[-8229136.588446875,4966532.690915448],[-8229140.820118288,4966534.419333676]]]]},"properties":{"shape_area":10007466.5221,"ntaname":"Inwood Hill Park","cdtaname":"MN12 Washington Heights-Inwood (CD 12 Equivalent)","shape_leng":25493.1347011,"boroname":"Manhattan","ntatype":9,"nta2020":"MN1292","borocode":1,"countyfips":"061","ntaabbrev":"InwdHlPk","cdta2020":"MN12"}}, +{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-8231990.992187567,4954501.945059545],[-8232048.261516793,4954534.903758419],[-8232181.23853414,4954609.04106511],[-8232340.637780649,4954697.906114968],[-8232350.293397986,4954702.699525672],[-8232358.190270063,4954706.691044204],[-8232384.395053914,4954721.683665762],[-8232657.761942266,4954873.231669385],[-8232661.621432349,4954875.363419119],[-8232666.742578247,4954878.190792732],[-8232673.742013516,4954881.988684503],[-8232951.871074988,4955035.632663621],[-8232986.154989603,4955045.486897292],[-8233003.974898885,4955013.62721354],[-8233035.783688736,4954956.785439848],[-8233086.75675191,4954865.260899832],[-8233137.952284863,4954772.431231597],[-8233192.899081153,4954672.760015037],[-8233247.727553259,4954574.668966039],[-8233272.78937863,4954528.894162852],[-8233298.600689991,4954482.410952879],[-8233349.906658912,4954390.4476916],[-8233401.029746379,4954297.937781638],[-8233452.230800799,4954206.065340974],[-8233503.626721826,4954116.007800222],[-8233520.802939327,4954083.628817269],[-8233532.326247722,4954061.905782456],[-8233547.801496101,4954034.1776688015],[-8233602.646333313,4953936.106737448],[-8233613.222066877,4953917.1930803815],[-8233654.407544356,4953841.341577955],[-8233711.1160886055,4953739.102160639],[-8233765.846122171,4953640.427828251],[-8233816.044552917,4953548.999076394],[-8233868.031905965,4953456.472027896],[-8233918.934072987,4953364.603676402],[-8233969.863007444,4953272.278358585],[-8234018.767180187,4953182.047390034],[-8234071.964444065,4953088.436840503],[-8234122.929972854,4952996.7715187585],[-8234174.757549455,4952905.726689889],[-8234200.997479077,4952856.757801718],[-8234203.604458876,4952851.424548332],[-8234228.787963541,4952806.0229098005],[-8234284.060680074,4952706.086313966],[-8234336.20570338,4952613.015421795],[-8234387.35129474,4952519.977681903],[-8234438.869056035,4952427.145126881],[-8234494.543645791,4952326.859464987],[-8234700.275201957,4951954.35747818],[-8234755.434840309,4951855.883069596],[-8234780.622661675,4951810.862862559],[-8234807.26881624,4951762.507691601],[-8234831.973570162,4951718.122883108],[-8234859.455923009,4951668.785706151],[-8234910.463110142,4951576.667765811],[-8234965.660798805,4951477.242077482],[-8235021.609919899,4951376.609313475],[-8235072.138928455,4951285.320574376],[-8235123.2530676825,4951193.344116382],[-8235173.964060636,4951101.851878839],[-8235224.704280842,4951010.433957826],[-8235273.714592285,4950920.676471936],[-8235325.998720737,4950827.309979153],[-8235377.226788646,4950735.656751019],[-8235428.376045826,4950643.855006403],[-8235478.680352059,4950552.627315229],[-8235529.797551403,4950460.017267678],[-8235602.882792437,4950330.772437394],[-8235597.545997619,4950327.5215560235],[-8235592.618111098,4950323.678963843],[-8235588.164417087,4950319.2955682445],[-8235584.243961522,4950314.429367078],[-8235580.908645714,4950309.144907462],[-8235578.202654711,4950303.5121981725],[-8235576.5121140145,4950297.658285965],[-8235575.43131183,4950291.661783864],[-8235574.971662059,4950285.586024903],[-8235575.138018182,4950279.495178971],[-8235575.928615686,4950273.45356018],[-8235577.335111324,4950267.524991512],[-8235579.342649216,4950261.772087585],[-8235581.930025641,4950256.255607518],[-8235313.134936679,4950109.090862897],[-8235233.490513298,4950064.818723825],[-8234996.687041155,4949933.181349686],[-8234769.4174922025,4949806.081092683],[-8234707.474442331,4949771.43819859],[-8234638.347894098,4949728.82805847],[-8234586.990755677,4949819.434237085],[-8234536.340503762,4949910.138857731],[-8234486.159781143,4950001.678826294],[-8234434.670782239,4950093.600650151],[-8234383.74935218,4950184.880906703],[-8234334.087598508,4950277.315495208],[-8234283.203196031,4950368.237757774],[-8234248.861034767,4950429.615583159],[-8234232.028899067,4950459.698548976],[-8234180.788548039,4950551.96954847],[-8234129.249083407,4950642.091880288],[-8234079.346608037,4950734.471211087],[-8234028.218308807,4950826.565864269],[-8233973.169784747,4950926.873286226],[-8233917.142127694,4951026.320080607],[-8233865.748102576,4951118.9544255],[-8233814.00638534,4951211.8228772925],[-8233762.550902407,4951305.104964141],[-8233711.333275488,4951397.775559086],[-8233660.220188039,4951490.502348722],[-8233608.2381528,4951584.318712234],[-8233604.3183964435,4951592.335894108],[-8233549.091923023,4951690.644270591],[-8233538.48092765,4951709.828359763],[-8233497.577275236,4951783.779034631],[-8233445.976223222,4951876.622807673],[-8233394.185304324,4951969.928942606],[-8233341.904959391,4952064.466826307],[-8233290.857698575,4952156.330156569],[-8233235.659316286,4952254.044180714],[-8233180.738396543,4952354.899613219],[-8233130.435090595,4952445.753907398],[-8233079.529283597,4952537.719891034],[-8233030.140630848,4952628.836750388],[-8232978.425318516,4952721.038447699],[-8232926.378943211,4952813.3718211455],[-8232873.662510656,4952906.544694923],[-8232824.258631062,4952997.517965693],[-8232773.349815894,4953088.627920132],[-8232719.476666507,4953189.839675914],[-8232664.3709100215,4953288.047356402],[-8232644.5377104245,4953322.849512729],[-8232612.596515041,4953378.897382738],[-8232566.059868052,4953462.406666386],[-8232509.284603075,4953564.287454694],[-8232458.931188564,4953654.313108621],[-8232428.163669746,4953710.226280638],[-8232421.242603152,4953722.803893513],[-8232407.7874307,4953747.2555533005],[-8232357.117119655,4953839.229045446],[-8232306.916898188,4953932.026572873],[-8232254.750750677,4954023.811980853],[-8232200.0226841,4954123.009468123],[-8232146.347855746,4954221.816173215],[-8232093.956198233,4954313.825007519],[-8232041.482061989,4954405.99776305],[-8232012.099179141,4954463.037278293],[-8231990.992187567,4954501.945059545]]]},"properties":{"shape_area":38312378.2439,"ntaname":"Central Park","cdtaname":"MN64 Central Park (JIA 64 Equivalent)","shape_leng":32721.0971792,"boroname":"Manhattan","ntatype":9,"nta2020":"MN6491","borocode":1,"countyfips":"061","ntaabbrev":"CntrlPk","cdta2020":"MN64"}} +]} \ No newline at end of file diff --git a/tests/fixtures/runtime/one-manhattan-polygon.geojson b/tests/fixtures/runtime/one-manhattan-polygon.geojson new file mode 100644 index 00000000..4b246591 --- /dev/null +++ b/tests/fixtures/runtime/one-manhattan-polygon.geojson @@ -0,0 +1,24 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [[ + [-8237729.476530929, 4939486.723352737], + [-8237720.358874049, 4939501.544867631], + [-8237672.23274935, 4939586.072452652], + [-8237909.265717435, 4939695.158246201], + [-8237935.70874332, 4939644.024264006], + [-8237977.828631048, 4939558.58263889], + [-8237749.03886236, 4939454.9231396075], + [-8237729.476530929, 4939486.723352737] + ]] + }, + "properties": { + "name": "Test Polygon" + } + } + ] +} diff --git a/tests/fixtures/runtime/polygons-center.geojson b/tests/fixtures/runtime/polygons-center.geojson new file mode 100644 index 00000000..17e80f83 --- /dev/null +++ b/tests/fixtures/runtime/polygons-center.geojson @@ -0,0 +1,19 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"value": 50, "location": "Near Origin"}, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [-0.01, -0.01], + [0.01, -0.01], + [0.01, 0.01], + [-0.01, 0.01], + [-0.01, -0.01] + ]] + } + } + ] +} diff --git a/tests/fixtures/runtime/polygons-london.geojson b/tests/fixtures/runtime/polygons-london.geojson new file mode 100644 index 00000000..0de68f66 --- /dev/null +++ b/tests/fixtures/runtime/polygons-london.geojson @@ -0,0 +1,19 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"value": 75, "location": "London"}, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [-0.13, 51.50], + [-0.12, 51.50], + [-0.12, 51.51], + [-0.13, 51.51], + [-0.13, 51.50] + ]] + } + } + ] +} diff --git a/tests/fixtures/runtime/polygons-nyc.geojson b/tests/fixtures/runtime/polygons-nyc.geojson new file mode 100644 index 00000000..c3ed1117 --- /dev/null +++ b/tests/fixtures/runtime/polygons-nyc.geojson @@ -0,0 +1,19 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"value": 100, "location": "Times Square"}, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [-73.99, 40.755], + [-73.98, 40.755], + [-73.98, 40.765], + [-73.99, 40.765], + [-73.99, 40.755] + ]] + } + } + ] +} diff --git a/tests/fixtures/runtime/runtime-test-harness.html b/tests/fixtures/runtime/runtime-test-harness.html new file mode 100644 index 00000000..e5ed9d67 --- /dev/null +++ b/tests/fixtures/runtime/runtime-test-harness.html @@ -0,0 +1,166 @@ + + + + + + Autark Runtime Test Harness + + + +
Loading runtime...
+
+
+ + + + diff --git a/tests/fixtures/runtime/simple-lines.geojson b/tests/fixtures/runtime/simple-lines.geojson new file mode 100644 index 00000000..0ca45f70 --- /dev/null +++ b/tests/fixtures/runtime/simple-lines.geojson @@ -0,0 +1,18 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "test line" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [100, 0] + ] + } + } + ] +} diff --git a/tests/fixtures/runtime/simple-map-test.html b/tests/fixtures/runtime/simple-map-test.html new file mode 100644 index 00000000..ec8e74f6 --- /dev/null +++ b/tests/fixtures/runtime/simple-map-test.html @@ -0,0 +1,122 @@ + + + + + + Simple Map Test - Direct AutkMap API + + + +
Loading...
+ + + + + diff --git a/tests/fixtures/runtime/simple-points.csv b/tests/fixtures/runtime/simple-points.csv new file mode 100644 index 00000000..f59c5c32 --- /dev/null +++ b/tests/fixtures/runtime/simple-points.csv @@ -0,0 +1,4 @@ +id,name,value,latitude,longitude +1,Point A,10,40.7484,-73.9857 +2,Point B,20,40.7514,-73.9897 +3,Point C,30,40.7454,-73.9837 diff --git a/tests/fixtures/runtime/simple-points.geojson b/tests/fixtures/runtime/simple-points.geojson new file mode 100644 index 00000000..98bcbb4b --- /dev/null +++ b/tests/fixtures/runtime/simple-points.geojson @@ -0,0 +1,20 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "id": 1, "name": "Point A", "value": 10 }, + "geometry": { "type": "Point", "coordinates": [-73.9857, 40.7484] } + }, + { + "type": "Feature", + "properties": { "id": 2, "name": "Point B", "value": 20 }, + "geometry": { "type": "Point", "coordinates": [-73.9897, 40.7514] } + }, + { + "type": "Feature", + "properties": { "id": 3, "name": "Point C", "value": 30 }, + "geometry": { "type": "Point", "coordinates": [-73.9837, 40.7454] } + } + ] +} diff --git a/tests/fixtures/runtime/simple-polygons.geojson b/tests/fixtures/runtime/simple-polygons.geojson new file mode 100644 index 00000000..f4b567ed --- /dev/null +++ b/tests/fixtures/runtime/simple-polygons.geojson @@ -0,0 +1,47 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"value": 10, "name": "Square 1"}, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [-73.99, 40.74], + [-73.98, 40.74], + [-73.98, 40.75], + [-73.99, 40.75], + [-73.99, 40.74] + ]] + } + }, + { + "type": "Feature", + "properties": {"value": 30, "name": "Square 2"}, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [-73.98, 40.74], + [-73.97, 40.74], + [-73.97, 40.75], + [-73.98, 40.75], + [-73.98, 40.74] + ]] + } + }, + { + "type": "Feature", + "properties": {"value": 50, "name": "Square 3"}, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [-73.99, 40.75], + [-73.98, 40.75], + [-73.98, 40.76], + [-73.99, 40.76], + [-73.99, 40.75] + ]] + } + } + ] +} diff --git a/tests/gallery/autk-plot/barchart-click.test.ts b/tests/gallery/autk-plot/barchart-click.test.ts index 472ca00d..fc39c861 100644 --- a/tests/gallery/autk-plot/barchart-click.test.ts +++ b/tests/gallery/autk-plot/barchart-click.test.ts @@ -1,7 +1,7 @@ /** * Visual regression test stub for the barchart-click gallery example. */ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; -test('barchart-click', async ({ page }) => { +test('barchart-click', async () => { }); diff --git a/tests/gallery/autk-plot/heatmatrix-click.test.ts b/tests/gallery/autk-plot/heatmatrix-click.test.ts index c7b49324..ea7faea8 100644 --- a/tests/gallery/autk-plot/heatmatrix-click.test.ts +++ b/tests/gallery/autk-plot/heatmatrix-click.test.ts @@ -1,7 +1,7 @@ /** * Visual regression test stub for the heatmatrix-click gallery example. */ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; -test('heatmatrix-click', async ({ page }) => { +test('heatmatrix-click', async () => { }); diff --git a/tests/gallery/autk-plot/histogram-brush-landuse.test.ts b/tests/gallery/autk-plot/histogram-brush-landuse.test.ts index cd45ddab..ec5e7798 100644 --- a/tests/gallery/autk-plot/histogram-brush-landuse.test.ts +++ b/tests/gallery/autk-plot/histogram-brush-landuse.test.ts @@ -1,7 +1,7 @@ /** * Visual regression test stub for the histogram-brush-landuse gallery example. */ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; -test('histogram-brush-landuse', async ({ page }) => { +test('histogram-brush-landuse', async () => { }); diff --git a/tests/gallery/autk-plot/histogram-brush.test.ts b/tests/gallery/autk-plot/histogram-brush.test.ts index 1065781c..18f597f4 100644 --- a/tests/gallery/autk-plot/histogram-brush.test.ts +++ b/tests/gallery/autk-plot/histogram-brush.test.ts @@ -1,7 +1,7 @@ /** * Visual regression test stub for the histogram-brush gallery example. */ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; -test('histogram-brush', async ({ page }) => { +test('histogram-brush', async () => { }); diff --git a/tests/gallery/autk-plot/parallel-coordinates.test.ts b/tests/gallery/autk-plot/parallel-coordinates.test.ts index 01a8276e..eb766a75 100644 --- a/tests/gallery/autk-plot/parallel-coordinates.test.ts +++ b/tests/gallery/autk-plot/parallel-coordinates.test.ts @@ -1,7 +1,7 @@ /** * Visual regression test stub for the parallel-coordinates gallery example. */ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; -test('parallel-coordinates', async ({ page }) => { +test('parallel-coordinates', async () => { }); diff --git a/tests/gallery/autk-plot/scatterplot-brush.test.ts b/tests/gallery/autk-plot/scatterplot-brush.test.ts index 5cc4781e..011a53cc 100644 --- a/tests/gallery/autk-plot/scatterplot-brush.test.ts +++ b/tests/gallery/autk-plot/scatterplot-brush.test.ts @@ -1,7 +1,7 @@ /** * Visual regression test stub for the scatterplot-brush gallery example. */ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; -test('scatterplot-brush', async ({ page }) => { +test('scatterplot-brush', async () => { }); diff --git a/tests/gallery/autk-plot/scatterplot-click.test.ts b/tests/gallery/autk-plot/scatterplot-click.test.ts index 96ac2575..d89a57ac 100644 --- a/tests/gallery/autk-plot/scatterplot-click.test.ts +++ b/tests/gallery/autk-plot/scatterplot-click.test.ts @@ -1,7 +1,7 @@ /** * Visual regression test stub for the scatterplot-click gallery example. */ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; -test('scatterplot-click', async ({ page }) => { +test('scatterplot-click', async () => { }); diff --git a/tests/gallery/autk-plot/table-click.test.ts b/tests/gallery/autk-plot/table-click.test.ts index a56556f3..74de5545 100644 --- a/tests/gallery/autk-plot/table-click.test.ts +++ b/tests/gallery/autk-plot/table-click.test.ts @@ -1,7 +1,7 @@ /** * Visual regression test stub for the table-click gallery example. */ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; -test('table-click', async ({ page }) => { +test('table-click', async () => { }); diff --git a/tests/gallery/autk-plot/temporal-events-click.test.ts b/tests/gallery/autk-plot/temporal-events-click.test.ts index 6a3df977..8948f1d7 100644 --- a/tests/gallery/autk-plot/temporal-events-click.test.ts +++ b/tests/gallery/autk-plot/temporal-events-click.test.ts @@ -1,7 +1,7 @@ /** * Visual regression test stub for the temporal-events-click gallery example. */ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; -test('temporal-events-click', async ({ page }) => { +test('temporal-events-click', async () => { }); diff --git a/tests/gallery/autk-plot/timeseries-click.test.ts b/tests/gallery/autk-plot/timeseries-click.test.ts index 98503445..0c782a16 100644 --- a/tests/gallery/autk-plot/timeseries-click.test.ts +++ b/tests/gallery/autk-plot/timeseries-click.test.ts @@ -1,7 +1,7 @@ /** * Visual regression test stub for the timeseries-click gallery example. */ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; -test('timeseries-click', async ({ page }) => { +test('timeseries-click', async () => { }); diff --git a/tests/helpers/route-duckdb-cdn.ts b/tests/helpers/route-duckdb-cdn.ts new file mode 100644 index 00000000..f22e7948 --- /dev/null +++ b/tests/helpers/route-duckdb-cdn.ts @@ -0,0 +1,34 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { BrowserContext } from '@playwright/test'; + +const DUCKDB_DIST = path.resolve(__dirname, '..', '..', 'node_modules', '@duckdb', 'duckdb-wasm', 'dist'); + +/** + * Serves jsDelivr DuckDB asset requests from the local @duckdb/duckdb-wasm + * package so tests exercising the CDN fallback do not depend on the network. + * + * @param context Playwright browser context (covers worker-initiated requests). + * @returns Array that accumulates the intercepted CDN request pathnames. + */ +export async function routeDuckdbCdnLocal(context: BrowserContext): Promise { + const cdnRequests: string[] = []; + await context.route('https://cdn.jsdelivr.net/**', async (route) => { + const url = new URL(route.request().url()); + cdnRequests.push(url.pathname); + const fileName = path.basename(url.pathname); + const filePath = path.join(DUCKDB_DIST, fileName); + if (!fs.existsSync(filePath)) { + await route.fulfill({ status: 404, body: `No local asset for ${fileName}` }); + return; + } + const contentType = fileName.endsWith('.wasm') ? 'application/wasm' : 'text/javascript'; + await route.fulfill({ + status: 200, + contentType, + headers: { 'Access-Control-Allow-Origin': '*' }, + body: fs.readFileSync(filePath), + }); + }); + return cdnRequests; +} diff --git a/tests/helpers/route-overpass-har.ts b/tests/helpers/route-overpass-har.ts index 67f20b76..83bef538 100644 --- a/tests/helpers/route-overpass-har.ts +++ b/tests/helpers/route-overpass-har.ts @@ -50,7 +50,39 @@ export async function routeOverpassHar(page: Page, harPath: string, update: bool await page.route(OVERPASS_STATUS_PATTERN, route => route.fulfill({ status: 200, contentType: 'text/plain', body: OVERPASS_STATUS_MOCK }), ); - await page.routeFromHAR(harPath, { url: OVERPASS_INTERPRETER_PATTERN, update: false }); + const har = JSON.parse(fs.readFileSync(harPath, 'utf-8')) as { log?: { entries?: HarEntry[] } }; + const entries = har.log?.entries ?? []; + const exactEntries = new Map(entries.map(entry => [normalizePostData(entry.request.postData?.text), entry])); + const entriesByGroup = new Map(); + + for (const entry of entries) { + const group = classifyOverpassQuery(entry.request.postData?.text); + if (group && !entriesByGroup.has(group)) { + entriesByGroup.set(group, entry); + } + } + + await page.route(OVERPASS_INTERPRETER_PATTERN, async (route) => { + const postData = await route.request().postData(); + const entry = exactEntries.get(normalizePostData(postData)) + ?? entriesByGroup.get(classifyOverpassQuery(postData) ?? ''); + + if (!entry) { + await route.fulfill({ + status: 599, + contentType: 'text/plain', + body: `No HAR entry for Overpass query:\n${normalizePostData(postData)}`, + }); + return; + } + + const content = entry.response.content; + await route.fulfill({ + status: entry.response.status, + contentType: content.mimeType || 'application/json', + body: content.text, + }); + }); return; } @@ -108,3 +140,35 @@ export async function routeOverpassHar(page: Page, harPath: string, update: bool console.log(`[har-recorder] Saved ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'} to ${harPath}`); }); } + +function normalizePostData(postData: string | null | undefined): string { + if (!postData) return ''; + const decoded = decodeURIComponent(postData); + const query = decoded.startsWith('data=') ? decoded.slice('data='.length) : decoded; + return query.replace(/\s+/g, ' ').trim(); +} + +function classifyOverpassQuery(postData: string | null | undefined): string | null { + const query = normalizePostData(postData); + if (!query) return null; + + if (query.includes('boundaryWays') || (query.includes('relation["name"') && query.includes('out body'))) { + return 'boundaries'; + } + if (query.includes('"highway"')) { + return 'roads'; + } + if (query.includes('"building"') || query.includes('"building:part"') || query.includes('"type"="building"')) { + return 'buildings'; + } + if ( + query.includes('"leisure"') + || query.includes('"landuse"') + || query.includes('"water"') + || query.includes('"natural"') + ) { + return 'surface'; + } + + return null; +} diff --git a/tests/helpers/static-server.ts b/tests/helpers/static-server.ts new file mode 100644 index 00000000..91157767 --- /dev/null +++ b/tests/helpers/static-server.ts @@ -0,0 +1,59 @@ +import * as fs from 'fs'; +import * as http from 'http'; +import * as path from 'path'; + +const MIME_TYPES: Record = { + '.html': 'text/html', + '.js': 'text/javascript', + '.mjs': 'text/javascript', + '.json': 'application/json', + '.geojson': 'application/json', + '.csv': 'text/csv', + '.wasm': 'application/wasm', + '.map': 'application/json', +}; + +/** + * Minimal static file server for tests. + * + * @param root Directory to serve files from. + * @param cors When true, adds permissive CORS headers (mirrors cors_server.py). + */ +export function createStaticServer(root: string, { cors }: { cors: boolean }): http.Server { + return http.createServer((req, res) => { + const urlPath = decodeURIComponent(new URL(req.url ?? '/', 'http://localhost').pathname); + const filePath = path.join(root, urlPath); + // Prevent path traversal outside the served root. + if (!filePath.startsWith(root + path.sep) && filePath !== root) { + res.writeHead(403).end(); + return; + } + if (cors) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', '*'); + } + if (req.method === 'OPTIONS') { + res.writeHead(200).end(); + return; + } + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404).end('Not found'); + return; + } + res.setHeader('Content-Type', MIME_TYPES[path.extname(filePath)] ?? 'application/octet-stream'); + res.writeHead(200).end(data); + }); + }); +} + +/** Starts a server and resolves once it is listening. */ +export function listen(server: http.Server, port: number, host = '127.0.0.1'): Promise { + return new Promise((resolve, reject) => server.listen(port, host, resolve).once('error', reject)); +} + +/** Closes a server, resolving once it has stopped. */ +export function close(server: http.Server | undefined): Promise { + return new Promise((resolve) => (server ? server.close(() => resolve()) : resolve())); +} diff --git a/tests/runtime/cross-origin-worker.test.ts b/tests/runtime/cross-origin-worker.test.ts new file mode 100644 index 00000000..6b1f4faa --- /dev/null +++ b/tests/runtime/cross-origin-worker.test.ts @@ -0,0 +1,59 @@ +import * as http from 'http'; +import * as path from 'path'; +import { test, expect } from '@playwright/test'; +import { createStaticServer, listen, close } from '../helpers/static-server'; + +/** + * Regression test for cross-origin (Jupyter-like) embedding. + * + * A Jupyter notebook page (e.g. :8888) loads the runtime module, the DuckDB + * worker, and data files from a separate dev server (e.g. :8000). Browsers + * block `new Worker(url)` for cross-origin URLs even with CORS headers, so + * the runtime must create its DuckDB worker via a same-origin blob: URL + * (see autk-db/src/duckdb.ts). + * + * This test reproduces the scenario with two local origins: + * - RUNTIME_PORT serves the repo root with CORS headers (like cors_server.py) + * - PAGE_PORT serves the test page without CORS (like Jupyter) + */ + +const REPO_ROOT = path.resolve(__dirname, '..', '..'); +const RUNTIME_PORT = 8901; +const PAGE_PORT = 8902; + +let runtimeServer: http.Server; +let pageServer: http.Server; + +test.beforeAll(async () => { + runtimeServer = createStaticServer(REPO_ROOT, { cors: true }); + pageServer = createStaticServer(REPO_ROOT, { cors: false }); + await Promise.all([listen(runtimeServer, RUNTIME_PORT), listen(pageServer, PAGE_PORT)]); +}); + +test.afterAll(async () => { + await Promise.all([close(runtimeServer), close(pageServer)]); +}); + +test.describe('Cross-origin runtime embedding (Jupyter scenario)', () => { + test('DuckDB worker initializes and map renders when runtime is loaded cross-origin', async ({ page }) => { + const errors: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'error') errors.push(msg.text()); + }); + page.on('pageerror', (error) => errors.push(error.message)); + + const runtimeOrigin = `http://127.0.0.1:${RUNTIME_PORT}`; + await page.goto( + `http://127.0.0.1:${PAGE_PORT}/test-cross-origin.html?runtimeOrigin=${encodeURIComponent(runtimeOrigin)}`, + ); + + const status = page.locator('#status'); + await expect(status).not.toHaveAttribute('data-state', 'loading', { timeout: 60000 }); + + const statusText = await status.textContent(); + expect(errors.filter((e) => e.includes('SecurityError')), 'no cross-origin Worker SecurityError').toEqual([]); + expect(statusText, `status: ${statusText}\nconsole errors:\n${errors.join('\n')}`).toContain('SUCCESS'); + await expect(status).toHaveAttribute('data-state', 'success'); + expect(await page.locator('canvas').count()).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/tests/runtime/embedded-html.test.ts b/tests/runtime/embedded-html.test.ts new file mode 100644 index 00000000..c582a720 --- /dev/null +++ b/tests/runtime/embedded-html.test.ts @@ -0,0 +1,119 @@ +import { execFileSync } from 'child_process'; +import * as fs from 'fs'; +import * as http from 'http'; +import * as path from 'path'; +import { test, expect } from '@playwright/test'; +import { createStaticServer, listen, close } from '../helpers/static-server'; +import { routeDuckdbCdnLocal } from '../helpers/route-duckdb-cdn'; + +/** + * Regression test for self-contained HTML export (Spec.save_html()). + * + * The exported document embeds the widget runtime bundle inline and loads it + * via a blob: URL, so it must work without any local server. The file is + * generated through the real Python code path and opened over plain http; + * jsDelivr DuckDB requests are served locally so the test is hermetic. + */ + +const REPO_ROOT = path.resolve(__dirname, '..', '..'); +const OUTPUT_DIR = path.join(REPO_ROOT, 'tests', 'results'); +const OUTPUT_FILE = path.join(OUTPUT_DIR, 'embedded-export.html'); +const PORT = 8904; + +const GENERATE_SCRIPT = ` +import sys +sys.path.insert(0, ${JSON.stringify(path.join(REPO_ROOT, 'python'))}) +import autark as ak + +inline = ak.GeoJSON( + "inline_polys", + values={ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"name": "a"}, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [-74.01, 40.70], [-74.00, 40.70], + [-74.00, 40.71], [-74.01, 40.71], + [-74.01, 40.70], + ]], + }, + } + ], + }, + coordinate_format="EPSG:4326", + layer_type="polygons", +) + +spec = ak.Spec( + metadata=ak.Metadata(title="Embedded export test"), + workspace=ak.Workspace(coordinate_format="EPSG:4326"), + data=[inline], + views=[ + ak.Map( + camera=ak.Camera(pitch=0, bearing=0, zoom=13), + layers=[ak.Layer(inline, type="polygons").style(color="#4a90e2", opacity=0.7)], + ) + ], +) +spec.save_html(${JSON.stringify(OUTPUT_FILE)}) +`; + +let server: http.Server; + +test.beforeAll(async () => { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + execFileSync('python3', ['-c', GENERATE_SCRIPT], { stdio: 'pipe' }); + server = createStaticServer(REPO_ROOT, { cors: false }); + await listen(server, PORT); +}); + +test.afterAll(async () => { + await close(server); +}); + +test.describe('self-contained HTML export', () => { + test('save_html output renders without any local runtime server', async ({ page, context }) => { + // The exported page is a top-level-await module: the load event (and the + // first canvas) only appears after DuckDB init + rendering complete. + test.setTimeout(120000); + const html = fs.readFileSync(OUTPUT_FILE, 'utf-8'); + expect(html).toContain(''); + expect(html).toContain('Embedded export test'); + // Runtime must be inlined, not referenced from localhost. + expect(html).not.toContain('autk-runtime/dist/autk-runtime.js'); + + const cdnRequests = await routeDuckdbCdnLocal(context); + + const errors: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'error') errors.push(msg.text()); + }); + page.on('pageerror', (error) => errors.push(error.message)); + + // Fail fast if the page tries to reach any local dev server. + const localRequests: string[] = []; + await context.route('http://localhost:8000/**', async (route) => { + localRequests.push(route.request().url()); + await route.fulfill({ status: 404, body: 'dev server must not be needed' }); + }); + + await page.goto(`http://127.0.0.1:${PORT}/tests/results/embedded-export.html`, { + waitUntil: 'commit', + }); + + // Either the map canvas appears (success) or the error
 does.
+    const outcome = page.locator('canvas, pre').first();
+    await outcome.waitFor({ state: 'attached', timeout: 90000 });
+
+    const tagName = await outcome.evaluate((node) => node.tagName.toLowerCase());
+    const errorText = tagName === 'pre' ? await outcome.textContent() : null;
+    expect(errorText, `page error: ${errorText}\nconsole: ${errors.join('\n')}`).toBeNull();
+    expect(await page.locator('canvas').count()).toBeGreaterThanOrEqual(1);
+    expect(localRequests, 'no dev-server requests expected').toEqual([]);
+    expect(cdnRequests.length, 'DuckDB assets should come from CDN URLs').toBeGreaterThan(0);
+  });
+});
diff --git a/tests/runtime/example-specs.test.ts b/tests/runtime/example-specs.test.ts
new file mode 100644
index 00000000..f67441a6
--- /dev/null
+++ b/tests/runtime/example-specs.test.ts
@@ -0,0 +1,180 @@
+import { test, expect } from '@playwright/test';
+import { execFileSync } from 'node:child_process';
+import { join } from 'node:path';
+
+const HARNESS_URL = '/tests/fixtures/runtime/runtime-test-harness.html';
+const PYTHON = process.env.PYTHON ?? 'python';
+
+test.describe('Runtime - Example Specs', () => {
+  test('executes local spatial join example successfully', async ({ page }) => {
+    page.on('console', msg => console.log(`Browser: [${msg.type()}] ${msg.text()}`));
+    page.on('pageerror', err => console.error(`Browser error: ${err.message}`));
+
+    await page.goto(`${HARNESS_URL}?spec=/examples/specs/03-spatial-join.json`);
+
+    await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 30000 });
+    await expect(page.locator('#metadata')).toContainText('Tables: neighborhoods, trees');
+    await expect(page.locator('#metadata')).toContainText('Maps: neighborhood_map');
+    await expect(page.locator('#metadata')).toContainText('Plots: tree_count_histogram');
+
+    const joinedProperties = await page.evaluate(async () => {
+      const runtime = window.__autarkRuntime;
+      const db = runtime.getDb();
+      const layer = await db.getLayer('neighborhoods');
+      return layer.features.map(feature => feature.properties);
+    });
+
+    expect(joinedProperties).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({
+          name: 'West Square',
+          sjoin: expect.objectContaining({
+            count: expect.objectContaining({ trees: 2 }),
+          }),
+        }),
+        expect.objectContaining({
+          name: 'North Square',
+          sjoin: expect.objectContaining({
+            count: expect.objectContaining({ trees: 1 }),
+          }),
+        }),
+        expect.objectContaining({
+          name: 'East Square',
+          sjoin: expect.objectContaining({
+            count: expect.objectContaining({ trees: 1 }),
+          }),
+        }),
+      ])
+    );
+
+    const highlightedIds = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      const plot = runtime.getPlot('tree_count_histogram');
+      const map = runtime.getMap('neighborhood_map');
+      const layer = map.layerManager.searchByLayerId('neighborhoods_layer');
+
+      plot.setSelection([0, 2]);
+      plot.events.emit('brushX', { selection: plot.selection });
+
+      return layer.highlightedIds;
+    });
+
+    expect(highlightedIds).toEqual([0, 2]);
+
+    const clearedIds = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      const plot = runtime.getPlot('tree_count_histogram');
+      const map = runtime.getMap('neighborhood_map');
+      const layer = map.layerManager.searchByLayerId('neighborhoods_layer');
+
+      plot.setSelection([]);
+      plot.events.emit('brushX', { selection: [] });
+
+      return layer.highlightedIds;
+    });
+
+    expect(clearedIds).toEqual([]);
+
+    await expect(page.locator('canvas').first()).toBeVisible();
+  });
+
+  test('executes geojson input example successfully', async ({ page }) => {
+    page.on('pageerror', err => console.error(`Browser error: ${err.message}`));
+
+    await page.goto(`${HARNESS_URL}?spec=/examples/specs/04-geojson-input.json`);
+
+    await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 30000 });
+    await expect(page.locator('#metadata')).toContainText('Tables: neighborhoods');
+    await expect(page.locator('#metadata')).toContainText('Maps: neighborhoods_map');
+    await expect(page.locator('canvas').first()).toBeVisible();
+  });
+
+  test('executes csv points example successfully', async ({ page }) => {
+    page.on('pageerror', err => console.error(`Browser error: ${err.message}`));
+
+    await page.goto(`${HARNESS_URL}?spec=/examples/specs/05-csv-points.json`);
+
+    await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 30000 });
+    await expect(page.locator('#metadata')).toContainText('Tables: trees');
+    await expect(page.locator('#metadata')).toContainText('Maps: trees_map');
+    await expect(page.locator('#metadata')).toContainText('Plots: latitude_histogram');
+
+    const highlightedIds = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      const plot = runtime.getPlot('latitude_histogram');
+      const map = runtime.getMap('trees_map');
+      const layer = map.layerManager.searchByLayerId('trees_layer');
+
+      // Includes an out-of-range id (99) to verify it is ignored rather than
+      // crashing the sprite layer (regression for former runtime TODO 5).
+      plot.setSelection([0, 2, 99]);
+      plot.events.emit('brushX', { selection: plot.selection });
+
+      return layer.highlightedIds;
+    });
+
+    expect(highlightedIds.sort()).toEqual([0, 2]);
+    await expect(page.locator('canvas').first()).toBeVisible();
+  });
+
+  test('executes Python-generated spatial join spec successfully', async ({ page }) => {
+    page.on('console', msg => console.log(`Browser: [${msg.type()}] ${msg.text()}`));
+    page.on('pageerror', err => console.error(`Browser error: ${err.message}`));
+
+    const generatedSpec = execFileSync(PYTHON, ['python/examples/spatial_join.py'], {
+      cwd: process.cwd(),
+      encoding: 'utf-8',
+      env: {
+        ...process.env,
+        PYTHONPATH: join(process.cwd(), 'python'),
+      },
+    });
+
+    await page.route('**/*', route => {
+      const url = new URL(route.request().url());
+      if (url.pathname !== '/generated-specs/python-spatial-join.json') {
+        return route.continue();
+      }
+
+      return route.fulfill({
+        status: 200,
+        contentType: 'application/json',
+        body: generatedSpec,
+      });
+    });
+
+    await page.goto(`${HARNESS_URL}?spec=/generated-specs/python-spatial-join.json`);
+
+    await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 30000 });
+    await expect(page.locator('#metadata')).toContainText('Tables: neighborhoods, trees');
+    await expect(page.locator('#metadata')).toContainText('Maps: neighborhood_map');
+    await expect(page.locator('#metadata')).toContainText('Plots: tree_count_histogram');
+
+    const runtimeState = await page.evaluate(async () => {
+      const runtime = window.__autarkRuntime;
+      const db = runtime.getDb();
+      const layer = await db.getLayer('neighborhoods');
+      const plot = runtime.getPlot('tree_count_histogram');
+      const map = runtime.getMap('neighborhood_map');
+      const mapLayer = map.layerManager.searchByLayerId('neighborhoods_layer');
+
+      plot.setSelection([0, 2]);
+      plot.events.emit('brushX', { selection: plot.selection });
+
+      return {
+        joinedNames: layer.features.map(feature => feature.properties.name).sort(),
+        highlightedIds: mapLayer.highlightedIds,
+      };
+    });
+
+    expect(runtimeState.joinedNames).toEqual(['East Square', 'North Square', 'West Square']);
+    expect(runtimeState.highlightedIds).toEqual([0, 2]);
+    await expect(page.locator('canvas').first()).toBeVisible();
+  });
+});
+
+declare global {
+  interface Window {
+    __autarkRuntime: any;
+  }
+}
diff --git a/tests/runtime/jupyter-test.test.ts b/tests/runtime/jupyter-test.test.ts
new file mode 100644
index 00000000..1b9d303a
--- /dev/null
+++ b/tests/runtime/jupyter-test.test.ts
@@ -0,0 +1,77 @@
+import * as http from 'http';
+import * as path from 'path';
+import { test, expect } from '@playwright/test';
+import { createStaticServer, listen, close } from '../helpers/static-server';
+
+const REPO_ROOT = path.resolve(__dirname, '..', '..');
+const PORT = 8765;
+
+let server: http.Server;
+
+test.beforeAll(async () => {
+  server = createStaticServer(REPO_ROOT, { cors: false });
+  await listen(server, PORT);
+});
+
+test.afterAll(async () => {
+  await close(server);
+});
+
+test.describe('Jupyter Integration Test', () => {
+  test('Python-generated HTML renders correctly', async ({ page }) => {
+    // Set up console listeners BEFORE navigation
+    const errors: string[] = [];
+    const allLogs: string[] = [];
+    page.on('console', msg => {
+      const text = msg.text();
+      allLogs.push(`[${msg.type()}] ${text}`);
+      if (msg.type() === 'error') {
+        errors.push(text);
+      }
+      console.log(`Browser [${msg.type()}]:`, text);
+    });
+
+    page.on('pageerror', error => {
+      console.log('Page Error:', error.message);
+      errors.push(error.message);
+    });
+
+    // Navigate to the test HTML served via HTTP
+    await page.goto('http://127.0.0.1:8765/test-jupyter.html');
+
+    // Wait for both tests to complete
+    await page.waitForTimeout(8000);
+
+    // Check status for test 1
+    const status1 = await page.locator('#status1').textContent();
+    console.log('Test 1 Status:', status1);
+
+    // Wait a bit more for rendering
+    await page.waitForTimeout(3000);
+
+    // Check if there's a canvas (map should create one)
+    const canvas = page.locator('canvas');
+    const canvasCount = await canvas.count();
+    console.log('Canvas elements found:', canvasCount);
+
+    // Get final status for both tests
+    const finalStatus1 = await page.locator('#status1').textContent();
+    const statusClass1 = await page.locator('#status1').getAttribute('class');
+    const finalStatus2 = await page.locator('#status2').textContent();
+    const statusClass2 = await page.locator('#status2').getAttribute('class');
+
+    console.log('Test 1 Final status:', finalStatus1);
+    console.log('Test 1 Status class:', statusClass1);
+    console.log('Test 2 Final status:', finalStatus2);
+    console.log('Test 2 Status class:', statusClass2);
+    console.log('Errors:', errors);
+
+    // Assertions
+    expect(statusClass1).toContain('success');
+    expect(statusClass2).toContain('success');
+    expect(canvasCount).toBeGreaterThanOrEqual(2); // Both tests should create canvases
+    // Note: errors array may have warnings, just check no critical failures
+    expect(finalStatus1).toContain('Success');
+    expect(finalStatus2).toContain('Success');
+  });
+});
diff --git a/tests/runtime/runtime-fixtures.test.ts b/tests/runtime/runtime-fixtures.test.ts
new file mode 100644
index 00000000..5336b32e
--- /dev/null
+++ b/tests/runtime/runtime-fixtures.test.ts
@@ -0,0 +1,237 @@
+/**
+ * Browser-based runtime tests using local fixtures (no network calls).
+ * These tests verify that the runtime can:
+ * - Load and parse spec JSON
+ * - Initialize the database
+ * - Load local GeoJSON and CSV files
+ * - Render maps and plots
+ * - Expose correct metadata APIs
+ */
+import { test, expect } from '@playwright/test';
+
+const HARNESS_URL = '/tests/fixtures/runtime/runtime-test-harness.html';
+
+test.describe('Runtime - Local Fixtures (CI-safe)', () => {
+  test('loads GeoJSON map spec successfully', async ({ page }) => {
+    page.on('console', msg => console.log(`Browser: [${msg.type()}] ${msg.text()}`));
+    page.on('pageerror', err => console.error(`Browser error: ${err.message}`));
+
+    await page.goto(`${HARNESS_URL}?spec=/tests/fixtures/runtime/fixture-01-geojson-map.json`);
+
+    // Wait for success status
+    await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 30000 });
+    await expect(page.locator('#status')).toContainText('Runtime loaded successfully');
+
+    // Verify runtime is exposed
+    const hasRuntime = await page.evaluate(() => window.__autarkRuntime !== undefined);
+    expect(hasRuntime).toBe(true);
+
+    // Verify database has correct table
+    const tables = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      const db = runtime.getDb();
+      return db.getTablesMetadata().map(t => ({ name: t.name, type: t.type }));
+    });
+
+    expect(tables).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ name: 'manhattan', type: 'polygons' })
+      ])
+    );
+
+    // Verify map was created
+    const maps = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      return runtime.getMaps().map(m => m.name);
+    });
+
+    expect(maps).toContain('manhattan_map');
+
+    // Verify constant layer styles are applied through render info.
+    const layerColor = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      const map = runtime.getMaps()[0].map;
+      return map.layerManager.searchByLayerId('manhattan_layer')?.layerRenderInfo.color;
+    });
+    expect(layerColor).toMatchObject({ r: 47, g: 111, b: 115, alpha: 1 });
+
+    const strokeColor = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      const map = runtime.getMaps()[0].map;
+      return map.layerManager.searchByLayerId('manhattan_layer')?.layerRenderInfo.strokeColor;
+    });
+    expect(strokeColor).toMatchObject({ r: 18, g: 52, b: 86, alpha: 1 });
+
+    // Verify canvas is visible and non-blank
+    const canvas = page.locator('canvas').first();
+    await expect(canvas).toBeVisible();
+
+    // Wait for rendering
+    await page.waitForTimeout(2000);
+
+    // Check rendered pixels via a screenshot. WebGPU canvases cannot be
+    // reliably read through a 2D context after the GPU context is created.
+    const screenshot = await canvas.screenshot();
+    const hasContent = await page.evaluate(async (bytes) => {
+      const image = await createImageBitmap(
+        new Blob([new Uint8Array(bytes)], { type: 'image/png' })
+      );
+      const surface = new OffscreenCanvas(image.width, image.height);
+      const ctx = surface.getContext('2d');
+      if (!ctx) return false;
+      ctx.drawImage(image, 0, 0);
+      const imageData = ctx.getImageData(0, 0, image.width, image.height);
+      const [baseR, baseG, baseB, baseA] = imageData.data;
+
+      // A blank canvas screenshot is a flat color. Rendered content should
+      // introduce at least some pixels that differ from the background.
+      for (let i = 4; i < imageData.data.length; i += 4) {
+        const colorDelta =
+          Math.abs(imageData.data[i] - baseR) +
+          Math.abs(imageData.data[i + 1] - baseG) +
+          Math.abs(imageData.data[i + 2] - baseB) +
+          Math.abs(imageData.data[i + 3] - baseA);
+        if (colorDelta > 10) return true;
+      }
+      return false;
+    }, [...screenshot]);
+
+    expect(hasContent).toBe(true);
+
+    // Verify no console errors
+    const errors: string[] = [];
+    page.on('pageerror', err => errors.push(err.message));
+    await page.waitForTimeout(500);
+    expect(errors).toHaveLength(0);
+  });
+
+  test('loads CSV with histogram spec successfully', async ({ page }) => {
+    page.on('console', msg => console.log(`Browser: [${msg.type()}] ${msg.text()}`));
+    page.on('pageerror', err => console.error(`Browser error: ${err.message}`));
+
+    await page.goto(`${HARNESS_URL}?spec=/tests/fixtures/runtime/fixture-02-csv-histogram.json`);
+
+    // Wait for success status
+    await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 30000 });
+
+    // Verify table was loaded from CSV
+    const tables = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      const db = runtime.getDb();
+      return db.getTablesMetadata().map(t => ({ name: t.name, source: t.source }));
+    });
+
+    expect(tables).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ name: 'points', source: 'csv' })
+      ])
+    );
+
+    // Verify both map and plot were created
+    const viewCounts = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      return {
+        maps: runtime.getMaps().length,
+        plots: runtime.getPlots().length
+      };
+    });
+
+    expect(viewCounts.maps).toBe(1);
+    expect(viewCounts.plots).toBe(1);
+
+    // Verify metadata display shows correct counts
+    await expect(page.locator('#metadata')).toContainText('Table count: 1');
+    await expect(page.locator('#metadata')).toContainText('Maps: points_map');
+    await expect(page.locator('#metadata')).toContainText('Plots: value_histogram');
+
+    // Verify canvas exists
+    await expect(page.locator('canvas').first()).toBeVisible();
+  });
+
+  test('loads scatterplot and table views successfully', async ({ page }) => {
+    page.on('console', msg => console.log(`Browser: [${msg.type()}] ${msg.text()}`));
+    page.on('pageerror', err => console.error(`Browser error: ${err.message}`));
+
+    await page.goto(`${HARNESS_URL}?spec=/tests/fixtures/runtime/fixture-06-scatter-table.json`);
+    await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 30000 });
+
+    const viewState = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      return {
+        tables: runtime.getDb().getTablesMetadata().map(t => t.name),
+        plots: runtime.getPlots().map(p => ({ name: p.name, type: p.plot.type })),
+        scatterMarks: document.querySelectorAll('.autark-view-scatterplot circle.autkMark').length,
+        tableRows: document.querySelectorAll('.autark-view-table tbody tr').length,
+      };
+    });
+
+    expect(viewState.tables).toContain('points');
+    expect(viewState.plots).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ name: 'value_scatterplot', type: 'scatterplot' }),
+        expect.objectContaining({ name: 'points_table', type: 'table' }),
+      ])
+    );
+    expect(viewState.scatterMarks).toBe(3);
+    expect(viewState.tableRows).toBe(3);
+  });
+
+  test('applies line width style while loading polylines', async ({ page }) => {
+    page.on('console', msg => console.log(`Browser: [${msg.type()}] ${msg.text()}`));
+    page.on('pageerror', err => console.error(`Browser error: ${err.message}`));
+
+    await page.goto(`${HARNESS_URL}?spec=/tests/fixtures/runtime/fixture-05-line-style.json`);
+    await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 30000 });
+
+    const lineState = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      const map = runtime.getMaps()[0].map;
+      const layer = map.layerManager.searchByLayerId('line_layer');
+      const yValues = Array.from(layer.position).filter((_, index) => index % 2 === 1);
+      return {
+        color: layer.layerRenderInfo.color,
+        ySpan: Math.max(...yValues) - Math.min(...yValues),
+      };
+    });
+
+    expect(lineState.color).toMatchObject({ r: 101, g: 67, b: 33, alpha: 1 });
+    expect(lineState.ySpan).toBeCloseTo(12, 5);
+  });
+
+  test('exposes correct runtime metadata APIs', async ({ page }) => {
+    page.on('console', msg => console.log(`Browser: [${msg.type()}] ${msg.text()}`));
+
+    await page.goto(`${HARNESS_URL}?spec=/tests/fixtures/runtime/fixture-01-geojson-map.json`);
+    await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 30000 });
+
+    // Test getDb().getTablesMetadata()
+    const hasGetTablesMetadata = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      const db = runtime.getDb();
+      return typeof db.getTablesMetadata === 'function';
+    });
+    expect(hasGetTablesMetadata).toBe(true);
+
+    // Test getMaps() returns array
+    const mapsIsArray = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      return Array.isArray(runtime.getMaps());
+    });
+    expect(mapsIsArray).toBe(true);
+
+    // Test getPlots() returns array
+    const plotsIsArray = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      return Array.isArray(runtime.getPlots());
+    });
+    expect(plotsIsArray).toBe(true);
+  });
+});
+
+// Declare window extensions for TypeScript
+declare global {
+  interface Window {
+    __autarkRuntime: any;
+    __autarkSpec: any;
+  }
+}
diff --git a/tests/runtime/runtime-osm-har.test.ts b/tests/runtime/runtime-osm-har.test.ts
new file mode 100644
index 00000000..da62fbdd
--- /dev/null
+++ b/tests/runtime/runtime-osm-har.test.ts
@@ -0,0 +1,95 @@
+/**
+ * Runtime tests for OSM-based specs using HAR fixtures.
+ * These tests use recorded Overpass API responses for repeatability.
+ *
+ * To re-record HAR files, run with HAR_UPDATE=1 environment variable.
+ */
+import { test, expect } from '@playwright/test';
+import * as path from 'path';
+import { routeOverpassHar } from '../helpers/route-overpass-har';
+
+const HARNESS_URL = '/tests/fixtures/runtime/runtime-test-harness.html';
+
+test.describe('Runtime - OSM with HAR (CI-safe)', () => {
+  test('loads basic OSM map spec with HAR', async ({ page }) => {
+    test.setTimeout(60000);
+
+    page.on('console', msg => console.log(`Browser: [${msg.type()}] ${msg.text()}`));
+    page.on('pageerror', err => console.error(`Browser error: ${err.message}`));
+
+    // Route Overpass calls through HAR
+    await routeOverpassHar(
+      page,
+      path.join(__dirname, '../data/osm-layers-api.har'),
+      false
+    );
+
+    await page.goto(`${HARNESS_URL}?spec=/tests/fixtures/runtime/fixture-03-osm-har.json`);
+
+    // Wait for runtime to load
+    await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 60000 });
+
+    // Verify OSM tables were created
+    const tables = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      const db = runtime.getDb();
+      return db.getTablesMetadata().map(t => t.name);
+    });
+
+    // OSM layers should be named as ${name}_${layer}
+    expect(tables).toEqual(
+      expect.arrayContaining([
+        expect.stringMatching(/_buildings$/),
+        expect.stringMatching(/_roads$/)
+      ])
+    );
+
+    // Verify map was created
+    const maps = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      return runtime.getMaps().map(m => m.name);
+    });
+
+    expect(maps.length).toBeGreaterThan(0);
+
+    // Verify canvas rendered
+    await page.waitForTimeout(2000);
+    const canvas = page.locator('canvas').first();
+    await expect(canvas).toBeVisible();
+  });
+});
+
+test.describe('Runtime - OSM with Real Network (Manual)', () => {
+  test.skip(process.env['RUN_REAL_OVERPASS'] !== '1', 'Set RUN_REAL_OVERPASS=1 to run the live Overpass test');
+
+  test('loads basic OSM map spec with real Overpass API', async ({ page }) => {
+    // This test requires network access and is NOT CI-safe
+    // Run manually to verify real Overpass integration
+    test.setTimeout(120000);
+
+    page.on('console', msg => console.log(`Browser: [${msg.type()}] ${msg.text()}`));
+    page.on('pageerror', err => console.error(`Browser error: ${err.message}`));
+
+    await page.goto(`${HARNESS_URL}?spec=/tests/fixtures/runtime/fixture-04-osm-live.json`);
+
+    // Wait for runtime to load (may take longer with real network)
+    await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 120000 });
+
+    // Verify tables were created
+    const tableCount = await page.evaluate(() => {
+      const runtime = window.__autarkRuntime;
+      const db = runtime.getDb();
+      return db.getTablesMetadata().length;
+    });
+
+    expect(tableCount).toBeGreaterThan(0);
+  });
+});
+
+// Declare window extensions for TypeScript
+declare global {
+  interface Window {
+    __autarkRuntime: any;
+    __autarkSpec: any;
+  }
+}
diff --git a/tests/runtime/schema-validation.test.ts b/tests/runtime/schema-validation.test.ts
new file mode 100644
index 00000000..437b6b50
--- /dev/null
+++ b/tests/runtime/schema-validation.test.ts
@@ -0,0 +1,410 @@
+/**
+ * Schema validation tests for AutarkSpec JSON files.
+ * Tests both positive (valid) and negative (invalid) fixtures.
+ */
+import { test, expect } from '@playwright/test';
+import Ajv from 'ajv';
+import * as fs from 'fs';
+import * as path from 'path';
+import { TransformExecutor } from '../../autk-runtime/src/transform-executor';
+
+const schemaPath = path.join(__dirname, '../../schema/autark-spec-v0.1.json');
+const examplesDir = path.join(__dirname, '../../examples/specs');
+
+test.describe('Schema Validation - Positive Cases', () => {
+  const ajv = new Ajv({ strict: false });
+  const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
+  const validate = ajv.compile(schema);
+
+  test('01-basic-osm-map.json validates successfully', () => {
+    const spec = JSON.parse(fs.readFileSync(path.join(examplesDir, '01-basic-osm-map.json'), 'utf-8'));
+    const valid = validate(spec);
+    expect(valid).toBe(true);
+  });
+
+  test('02-linked-map-histogram.json validates successfully', () => {
+    const spec = JSON.parse(fs.readFileSync(path.join(examplesDir, '02-linked-map-histogram.json'), 'utf-8'));
+    const valid = validate(spec);
+    expect(valid).toBe(true);
+  });
+
+  test('03-spatial-join.json validates successfully', () => {
+    const spec = JSON.parse(fs.readFileSync(path.join(examplesDir, '03-spatial-join.json'), 'utf-8'));
+    const valid = validate(spec);
+    expect(valid).toBe(true);
+  });
+
+  test('OSM source accepts explicit geocode area and relation areas', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        {
+          "type": "osm",
+          "name": "nyc_osm",
+          "area": "Battery Park City",
+          "geocodeArea": "New York",
+          "areas": ["Battery Park City", "Financial District"],
+          "layers": ["buildings", "roads"]
+        }
+      ],
+      "views": [{ "type": "map", "layers": [{ "source": "nyc_osm_buildings" }] }]
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(true);
+  });
+
+  test('accepts heatmap transform output', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        { "type": "geojson", "name": "trees", "url": "trees.geojson", "layerType": "points" }
+      ],
+      "transforms": [
+        {
+          "type": "heatmap",
+          "source": "trees",
+          "output": "tree_heatmap",
+          "near": { "distance": 250, "useCentroid": true },
+          "grid": { "rows": 32, "columns": 32 },
+          "groupBy": [{ "column": "*", "op": "count" }]
+        }
+      ],
+      "views": [{ "type": "map", "layers": [{ "source": "tree_heatmap" }] }]
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(true);
+  });
+
+  test('accepts GPGPU compute transform output', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        { "type": "geojson", "name": "buildings", "url": "buildings.geojson", "layerType": "buildings" }
+      ],
+      "transforms": [
+        {
+          "type": "gpgpuCompute",
+          "source": "buildings",
+          "output": "building_compute",
+          "layerType": "buildings",
+          "variableMapping": { "height": "properties.height" },
+          "wgslBody": "return height * 2.0;",
+          "resultField": "height_double"
+        }
+      ],
+      "views": [{ "type": "map", "layers": [{ "source": "building_compute", "type": "buildings" }] }]
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(true);
+  });
+
+  test('accepts render compute transform output', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        { "type": "geojson", "name": "buildings", "url": "buildings.geojson", "layerType": "buildings" },
+        { "type": "geojson", "name": "viewpoints", "url": "viewpoints.geojson", "layerType": "points" }
+      ],
+      "transforms": [
+        {
+          "type": "renderCompute",
+          "output": "view_metrics",
+          "layerType": "points",
+          "layers": [{ "id": "buildings_layer", "source": "buildings", "type": "buildings" }],
+          "viewpoints": {
+            "source": "viewpoints",
+            "strategy": { "type": "centroid" },
+            "sampling": { "directions": 4 }
+          },
+          "aggregation": { "type": "classes", "includeBackground": true },
+          "tileSize": 64
+        }
+      ],
+      "views": [{ "type": "map", "layers": [{ "source": "view_metrics", "type": "points" }] }]
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(true);
+  });
+
+  test('accepts scatterplot and table views', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        { "type": "geojson", "name": "points", "url": "points.geojson", "layerType": "points" }
+      ],
+      "views": [
+        {
+          "type": "scatterplot",
+          "name": "value_scatterplot",
+          "source": "points",
+          "x": { "field": "id" },
+          "y": { "field": "value" },
+          "color": { "field": "name" },
+          "selection": { "name": "scatter_brush", "type": "interval" }
+        },
+        {
+          "type": "table",
+          "name": "points_table",
+          "source": "points",
+          "columns": [{ "field": "id" }, { "field": "name" }, { "field": "value" }],
+          "sort": { "column": "value", "direction": "desc" }
+        }
+      ]
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(true);
+  });
+});
+
+test.describe('Schema Validation - Negative Cases', () => {
+  const ajv = new Ajv({ strict: false });
+  const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
+  const validate = ajv.compile(schema);
+
+  test('rejects spec with misspelled top-level key', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "metadta": { // typo: should be "metadata"
+        "title": "Invalid Spec"
+      },
+      "data": [],
+      "views": []
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(false);
+    expect(validate.errors).toBeTruthy();
+  });
+
+  test('rejects data source with unsafe identifier', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        {
+          "type": "geojson",
+          "name": "my-invalid-name!", // unsafe identifier
+          "url": "data/test.geojson"
+        }
+      ],
+      "views": []
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(false);
+  });
+
+  test('rejects osm data source missing url when using PBF', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        {
+          "type": "osm",
+          "name": "osm_data",
+          "source": "pbf",
+          "layers": ["buildings"]
+          // missing required "url" field for PBF source
+        }
+      ],
+      "views": []
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(false);
+  });
+
+  test('rejects spatial join with empty near object', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        { "type": "geojson", "name": "a", "url": "a.geojson" },
+        { "type": "geojson", "name": "b", "url": "b.geojson" }
+      ],
+      "transforms": [
+        {
+          "type": "spatialJoin",
+          "root": "a",
+          "join": "b",
+          "near": {} // empty near object should be rejected
+        }
+      ],
+      "views": []
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(false);
+  });
+
+  test('rejects link with unknown target', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        { "type": "geojson", "name": "data", "url": "data.geojson" }
+      ],
+      "views": [
+        {
+          "type": "histogram",
+          "name": "hist",
+          "source": "data",
+          "x": { "field": "value" },
+          "selection": { "name": "brush", "type": "interval" }
+        }
+      ],
+      "links": [
+        {
+          "selection": "brush",
+          "target": "nonexistent_layer", // target doesn't exist
+          "action": "highlight"
+        }
+      ]
+    };
+    // Note: This may require runtime validation beyond schema
+    validate(spec);
+    // Schema may not catch this - it's more of a semantic validation issue
+    // We keep the test to document expected behavior
+  });
+
+  test('rejects CSV source without geometry configuration', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        {
+          "type": "csv",
+          "name": "points",
+          "url": "points.csv"
+          // missing required "geometry" field
+        }
+      ],
+      "views": []
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(false);
+  });
+
+  test('rejects heatmap collect aggregation', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        { "type": "geojson", "name": "trees", "url": "trees.geojson", "layerType": "points" }
+      ],
+      "transforms": [
+        {
+          "type": "heatmap",
+          "source": "trees",
+          "output": "tree_heatmap",
+          "near": { "distance": 250 },
+          "grid": { "rows": 32, "columns": 32 },
+          "groupBy": [{ "column": "species", "op": "collect" }]
+        }
+      ],
+      "views": []
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(false);
+  });
+
+  test('rejects GPGPU compute without result field or output columns', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        { "type": "geojson", "name": "buildings", "url": "buildings.geojson", "layerType": "buildings" }
+      ],
+      "transforms": [
+        {
+          "type": "gpgpuCompute",
+          "source": "buildings",
+          "output": "building_compute",
+          "variableMapping": { "height": "properties.height" },
+          "wgslBody": "return height * 2.0;"
+        }
+      ],
+      "views": []
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(false);
+  });
+
+  test('rejects render compute tile sizes that are not multiples of 8', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        { "type": "geojson", "name": "buildings", "url": "buildings.geojson", "layerType": "buildings" },
+        { "type": "geojson", "name": "viewpoints", "url": "viewpoints.geojson", "layerType": "points" }
+      ],
+      "transforms": [
+        {
+          "type": "renderCompute",
+          "output": "view_metrics",
+          "layers": [{ "id": "buildings_layer", "source": "buildings", "type": "buildings" }],
+          "viewpoints": { "source": "viewpoints" },
+          "aggregation": { "type": "objects" },
+          "tileSize": 63
+        }
+      ],
+      "views": []
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(false);
+  });
+
+  test('rejects scatterplot without y encoding', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        { "type": "geojson", "name": "points", "url": "points.geojson", "layerType": "points" }
+      ],
+      "views": [
+        {
+          "type": "scatterplot",
+          "source": "points",
+          "x": { "field": "id" }
+        }
+      ]
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(false);
+  });
+
+  test('rejects table without columns', () => {
+    const spec = {
+      "$schema": "https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json",
+      "version": "0.1",
+      "data": [
+        { "type": "geojson", "name": "points", "url": "points.geojson", "layerType": "points" }
+      ],
+      "views": [
+        {
+          "type": "table",
+          "source": "points",
+          "columns": []
+        }
+      ]
+    };
+    const valid = validate(spec);
+    expect(valid).toBe(false);
+  });
+});
+
+test.describe('Runtime transform validation', () => {
+  test('rejects full-module WGSL in GPGPU transform body before execution', async () => {
+    const executor = new TransformExecutor();
+    await expect(executor.executeTransform({} as any, {
+      type: 'gpgpuCompute',
+      source: 'buildings',
+      output: 'building_compute',
+      variableMapping: { height: 'properties.height' },
+      wgslBody: '@compute @workgroup_size(1) fn main() { return; }',
+      resultField: 'height_double',
+    })).rejects.toThrow(/function body/);
+  });
+});
diff --git a/tests/runtime/widget-bundle.test.ts b/tests/runtime/widget-bundle.test.ts
new file mode 100644
index 00000000..aae94130
--- /dev/null
+++ b/tests/runtime/widget-bundle.test.ts
@@ -0,0 +1,173 @@
+import * as fs from 'fs';
+import * as http from 'http';
+import * as path from 'path';
+import { test, expect } from '@playwright/test';
+import { createStaticServer, listen, close } from '../helpers/static-server';
+import { routeDuckdbCdnLocal } from '../helpers/route-duckdb-cdn';
+
+/**
+ * Regression test for the anywidget bundle (python/autark/static/autark-widget.js).
+ *
+ * anywidget loads the widget ESM from a blob: URL, so the bundle must be fully
+ * self-contained: no relative chunk imports, bundled schema, and DuckDB assets
+ * resolved from the jsDelivr CDN (import.meta.url is not http(s)).
+ *
+ * The test mimics anywidget: fetch the bundle, import it via a blob: URL, and
+ * call its default render() with a fake model. jsDelivr requests are routed to
+ * the local @duckdb/duckdb-wasm package so the test is hermetic.
+ */
+
+const REPO_ROOT = path.resolve(__dirname, '..', '..');
+const WIDGET_BUNDLE = path.join(REPO_ROOT, 'python', 'autark', 'static', 'autark-widget.js');
+const PORT = 8903;
+
+let server: http.Server;
+
+test.beforeAll(async () => {
+  server = createStaticServer(REPO_ROOT, { cors: false });
+  await listen(server, PORT);
+});
+
+test.afterAll(async () => {
+  await close(server);
+});
+
+const INLINE_GEOJSON = {
+  type: 'FeatureCollection',
+  features: [
+    {
+      type: 'Feature',
+      properties: { name: 'a', value: 1 },
+      geometry: {
+        type: 'Polygon',
+        coordinates: [
+          [
+            [-74.01, 40.7],
+            [-74.0, 40.7],
+            [-74.0, 40.71],
+            [-74.01, 40.71],
+            [-74.01, 40.7],
+          ],
+        ],
+      },
+    },
+  ],
+};
+
+const SPEC = {
+  $schema: 'https://urban-toolkit.github.io/autark/schema/autark-spec-v0.1.json',
+  version: '0.1',
+  workspace: { name: 'widget_test', coordinateFormat: 'EPSG:4326' },
+  data: [
+    {
+      name: 'inline_polys',
+      values: INLINE_GEOJSON,
+      layerType: 'polygons',
+      coordinateFormat: 'EPSG:4326',
+      type: 'geojson',
+    },
+  ],
+  views: [
+    {
+      type: 'map',
+      name: 'widget_map',
+      camera: { pitch: 0, bearing: 0, zoom: 13 },
+      layers: [
+        {
+          source: 'inline_polys',
+          id: 'inline_layer',
+          type: 'polygons',
+          style: { color: '#4a90e2', opacity: 0.7 },
+        },
+      ],
+    },
+    {
+      type: 'histogram',
+      name: 'widget_hist',
+      source: 'inline_polys',
+      x: { field: 'value' },
+      bins: 5,
+      selection: { name: 'widget_brush', type: 'interval' },
+    },
+  ],
+  layout: { type: 'vertical' },
+};
+
+test.describe('anywidget bundle', () => {
+  test('renders a spec when imported from a blob: URL with CDN DuckDB assets', async ({ page, context }) => {
+    expect(fs.existsSync(WIDGET_BUNDLE), `widget bundle missing - run: cd autk-runtime && npm run build:widget`).toBe(true);
+
+    const cdnRequests = await routeDuckdbCdnLocal(context);
+
+    const errors: string[] = [];
+    page.on('console', (msg) => {
+      if (msg.type() === 'error') errors.push(msg.text());
+    });
+    page.on('pageerror', (error) => errors.push(error.message));
+
+    // Any served page works; we only need an http origin to host the widget.
+    await page.goto(`http://127.0.0.1:${PORT}/test-cross-origin.html?runtimeOrigin=http://127.0.0.1:${PORT}`);
+
+    const result = await page.evaluate(
+      async ({ spec }) => {
+        // Mimic anywidget: load the ESM from a blob: URL.
+        const source = await (await fetch('/python/autark/static/autark-widget.js')).text();
+        const moduleUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
+        const mod = await import(/* @vite-ignore */ moduleUrl);
+        URL.revokeObjectURL(moduleUrl);
+
+        const values = new Map([
+          ['spec', spec],
+          ['height', '400px'],
+        ]);
+        // Mimic the real anywidget model, recording set()/save_changes() so
+        // kernel-bound selection sync can be asserted.
+        const recordedSets: Array<{ name: string; value: unknown }> = [];
+        let saveCount = 0;
+        const model = {
+          get: (name: string) => values.get(name),
+          on: () => undefined,
+          off: () => undefined,
+          set: (name: string, value: unknown) => {
+            values.set(name, value);
+            recordedSets.push({ name, value });
+          },
+          save_changes: () => {
+            saveCount += 1;
+          },
+        };
+        const el = document.createElement('div');
+        document.body.appendChild(el);
+        await mod.default.render({ model, el });
+
+        // Simulate a histogram brush; the widget should sync it to the model.
+        const runtime = (el as HTMLElement & { __autarkRuntime?: any }).__autarkRuntime;
+        const plot = runtime?.getPlot('widget_hist');
+        if (plot) {
+          plot.setSelection([0]);
+          plot.events.emit('brushX', { selection: plot.selection });
+        }
+
+        return {
+          html: el.innerHTML.slice(0, 500),
+          canvases: el.querySelectorAll('canvas').length,
+          errorText: el.querySelector('pre')?.textContent ?? null,
+          hadPlot: Boolean(plot),
+          selections: values.get('selections') ?? null,
+          recordedSets,
+          saveCount,
+        };
+      },
+      { spec: SPEC },
+    );
+
+    expect(result.errorText, `widget error: ${result.errorText}\nconsole: ${errors.join('\n')}`).toBeNull();
+    expect(result.canvases).toBeGreaterThanOrEqual(1);
+    expect(cdnRequests.length, 'DuckDB assets should be loaded via the jsDelivr URLs').toBeGreaterThan(0);
+
+    // Selection sync back to the kernel-side model.
+    expect(result.hadPlot, 'histogram plot should be retrievable from the runtime').toBe(true);
+    expect(result.selections).toEqual({ widget_brush: [0] });
+    expect(result.saveCount).toBeGreaterThan(0);
+  });
+});