ParcelToolv4/README.md
2026-04-30 20:46:07 +03:00

615 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ⬡ ParcelGen — Land Subdivision Tool
> **Version 4.0** — Full-stack GIS platform for professional land parcel subdivision. Digitize site boundaries, road networks, and existing buildings; configure land use zones and road types; generate, validate, and export subdivision layouts in multiple formats.
---
## Table of Contents
1. [Architecture](#architecture)
2. [Quick Start](#quick-start)
3. [Interface Overview](#interface-overview)
4. [Digitizing Tools](#digitizing-tools)
5. [Configuration](#configuration)
6. [Land Use Zones](#land-use-zones)
7. [Road Types](#road-types)
8. [Import Data](#import-data)
9. [Export Formats](#export-formats)
10. [Map Controls](#map-controls)
11. [Subdivision Engine](#subdivision-engine)
12. [API Reference](#api-reference)
13. [Parcel Properties](#parcel-properties)
14. [Colour Legend](#colour-legend)
15. [Development](#development)
16. [File Structure](#file-structure)
17. [Technical Notes](#technical-notes)
---
## Architecture
```
┌──────────────────────────────────────────────────────┐
│ Docker Network │
│ │
│ ┌──────────────────┐ ┌────────────────────────┐ │
│ │ Nginx + Front │────▶│ FastAPI Backend v4 │ │
│ │ (port 3000) │ │ (port 8000) │ │
│ │ OpenLayers UI │ │ Shapely / GEOS engine │ │
│ └──────────────────┘ └────────────────────────┘ │
└──────────────────────────────────────────────────────┘
│ Browser (desktop or mobile)
http://localhost:3000
```
The frontend is a single HTML file with no build step. The backend is a stateless FastAPI service — each subdivision request is independent, making the system safe for multiple simultaneous users.
---
## Quick Start
### Prerequisites
- Docker and Docker Compose installed
### Run
```bash
cd parcel-tool
docker compose up --build
```
Open **http://localhost:3000** in your browser (desktop or mobile).
### Stop
```bash
docker compose down
```
---
## Interface Overview
The UI is fully **mobile responsive**. On small screens a `☰` button slides the sidebar in and out. On desktop the sidebar is always visible on the left.
### Sidebar Tabs
| Tab | Purpose |
|---|---|
| **Digitize** | All digitizing tools, import, and generation controls |
| **Config** | Subdivision parameters, land use zone, numbering |
| **Layers** | Basemap switcher, layer visibility and opacity, legend |
| **Results** | Statistics dashboard, warnings, and scrollable plot list |
### Header Controls
| Control | Description |
|---|---|
| ☀️ / 🌙 | Toggle dark / light theme (saved to localStorage) |
| **m / ft** | Switch between metric and imperial units (live conversion) |
---
## Digitizing Tools
All geometry is entered on the map using the **Digitize** tab or the floating **map toolbar**.
### Toolbar Buttons
| Button | Mode | Description |
|---|---|---|
| ◇ **Boundary** | Polygon | Digitize the site boundary. One boundary is kept at a time. Double-click to close the polygon. |
| **Road** | LineString | Digitize a straight road centreline. The road type and width set in the sidebar apply to this line. |
| ⌒ **Arc Road** | LineString (curved) | Digitize a curved road centreline using free-form multi-vertex input. Treated identically to straight roads by the engine. |
| ▦ **Building** | Polygon | Digitize an existing building footprint. The engine carves it from the buildable area and marks the containing plot as "built". |
| 📏 **Measure** | LineString | Click points, double-click to finish. Shows live geodesic distance in the active unit (m/km or ft/mi). |
| ✋ **Pan** | — | Temporarily disables digitizing and enables free map panning. Click Stop or Pan again to exit. |
| ✕ **Stop** | — | Exits the current digitizing or measure mode. |
### Editing Vertices
Click **Edit Vertices** in the Digitize tab to enter modify mode on the boundary:
- **Drag** any vertex to move it.
- **Alt + Click** a vertex to delete that single vertex without clearing the entire feature.
- **Right-click** during active digitizing to undo the last placed point.
### Managing Drawn Roads
Each digitized road appears in a list in the sidebar showing its type and a colour indicator. Click the **✕** button next to any road to delete just that line — other roads and all results are unaffected.
---
## Configuration
All parameters are in the **Config** tab. Switching between **m** and **ft** converts all values in-place; the backend always receives metres.
### Plot Dimensions
| Parameter | Default | Description |
|---|---|---|
| **Min Frontage** | 10 m | Minimum road-facing width of a plot (the side that faces the street) |
| **Min Plot Depth** | 25 m | Minimum perpendicular dimension (how deep the plot runs away from the road) |
| **Min Plot Area** | 250 m² | Smallest acceptable plot area. Plots below this are absorbed into neighbours. |
| **Max Plot Area** | 1 000 m² | Largest acceptable plot area. Oversized plots are flagged in the results. Set to 0 for no upper limit. |
> **Terminology note:** *Frontage* is the road-facing dimension. *Plot Depth* is the dimension perpendicular to the road (sometimes called breadth or width in other tools — here it is consistently called depth to match survey practice).
### Road Parameters
| Parameter | Default | Description |
|---|---|---|
| **Default Road Width** | 9 m | Width applied to auto-generated grid roads and to digitized roads with no custom width |
| **Max Block Length** | 120 m | Maximum buildable block length before a cross-road or cul-de-sac is inserted |
| **Splay Radius** | 3 m | Corner treatment at road intersections (also called a *splay* in survey practice — a diagonal cut-off at junctions). Changing the unit system converts this value correctly. |
### Numbering & Zoning
| Parameter | Default | Description |
|---|---|---|
| **Parcel ID Prefix** | P | Letter(s) prepended to every plot number (e.g. `P0001`, `R0001`) |
| **Start Number** | 1 | First sequential number used in plot IDs |
### Options
- **Allow Cul-de-sacs** — Insert a cul-de-sac when a block exceeds Max Block Length and no through-road is available.
- **Auto-generate Roads** — When enabled, internal grid roads are generated to fill any blocks that exceed Max Block Length, even when user-drawn roads are present. User roads and auto-roads are always combined.
---
## Land Use Zones
Select a zone type in the **Config** tab. Choosing a zone automatically fills all dimension parameters with appropriate defaults for that use class.
| Zone | Min Frontage | Min Depth | Min Area | Max Area | Typical Use |
|---|---|---|---|---|---|
| Residential Low Density | 15 m | 30 m | 450 m² | 2 000 m² | Large single-family plots |
| Residential Medium Density | 10 m | 25 m | 250 m² | 1 000 m² | Standard residential (default) |
| Residential High Density | 6 m | 20 m | 120 m² | 500 m² | Town houses, maisonettes |
| Commercial | 12 m | 20 m | 300 m² | 5 000 m² | Shops, offices, mixed retail |
| Mixed Use | 8 m | 20 m | 200 m² | 2 000 m² | Ground-floor commercial + residential above |
| Industrial | 20 m | 40 m | 1 000 m² | 20 000 m² | Warehousing, light manufacturing |
| Open Space / Park | 30 m | 30 m | 500 m² | 50 000 m² | Public parks, green spaces |
Each zone type is colour-coded on the map so mixed-zone layouts are immediately legible.
---
## Road Types
When digitizing a road, select its type from the **Road Type** dropdown. Each type has a default width that determines how much land is allocated to the road corridor.
| Type | Default Width | Typical Use |
|---|---|---|
| **Primary** | 20 m | Arterial roads, dual carriageways |
| **Secondary** | 14 m | Collector roads, main estate roads |
| **Local** | 9 m | Residential access roads (default) |
| **Lane / Alley** | 4 m | Service lanes, pedestrian alleys |
You can override the width for any individual road using the **Custom Width** field before digitizing. This is useful when a road does not match a standard type exactly.
Multiple roads of different types can coexist in the same layout. The engine buffers each road to its own width before computing the buildable area.
---
## Import Data
Click **Import Data…** in the Digitize tab or drag a file onto the drop zone.
### Supported Formats
| Format | Extension | What is imported |
|---|---|---|
| **GeoJSON** | `.geojson`, `.json` | All feature types auto-routed by geometry |
| **KML** | `.kml` | Polygons → boundary or buildings; Lines → roads |
| **GPX** | `.gpx` | Tracks and routes → road centrelines |
| **CSV** | `.csv` | Rows with `lon`/`lat` columns or a `wkt` column |
| **Shapefile** | `.zip` | Zipped `.shp` package — polygons and lines |
### Geometry Routing
Imported features are automatically assigned to the correct layer:
- **Polygon / MultiPolygon** — If small (< ~50 m across) it is added as a building footprint; otherwise it replaces the boundary.
- **LineString / MultiLineString** — Added as a road centreline with type *Local*.
- **Point** — Placed on the map for reference only.
After import the map zooms to fit the imported data.
---
## Export Formats
Click **Export…** in the Digitize tab after generating a subdivision.
| Format | File | Contents | Best for |
|---|---|---|---|
| **GeoJSON** | `.geojson` | All parcels, roads, cul-de-sacs + metadata | Web apps, QGIS, PostGIS |
| **KML** | `.kml` | All features as Placemarks with attributes | Google Earth, Google Maps |
| **Shapefile** | `.zip` | Parcels as `.shp/.shx/.dbf/.prj` package | ArcGIS, MapInfo, QGIS |
| **CSV** | `.csv` | Parcel attribute table only (no geometry) | Excel, data analysis |
All exports use **WGS84 (EPSG:4326)** coordinates. All attribute values are in SI units (metres, m²) regardless of the active unit display.
---
## Map Controls
### Theme
The ☀️ / 🌙 button in the sidebar header switches between **dark** and **light** themes. The preference is saved in `localStorage` and restored on next visit. Switching theme also switches the default basemap to match.
### Unit System
The **m / ft** pill converts all configuration inputs in-place when toggled. Results always display both metric and imperial values simultaneously. The backend always receives and processes in metres.
### Basemap Switcher
Available in the **Layers** tab and the floating **Basemap** panel (bottom-right of map).
| Category | Basemaps |
|---|---|
| Style | Dark (CARTO), Light (CARTO), Night Lights |
| Street | OpenStreetMap |
| Terrain | OpenTopoMap, ESRI World Topo, ESRI Shaded Relief, Ocean |
| Imagery | ESRI World Imagery (Satellite) |
| No API key required for any basemap. |
### Layer Switcher
Both the floating **Layers** panel and the **Layers** tab provide per-layer controls:
- **Visibility checkbox** — show/hide the layer
- **Opacity slider** — 0100%, updates live
Layers available: Site Boundary · Buildings (digitized) · Roads (digitized) · Road Surfaces (result) · Cul-de-sacs (result) · Blocks (result) · Parcels (result)
### Hover Tooltip
Hovering over any generated plot shows a floating tooltip with: area (both units), frontage, plot depth, land use zone, status, and road access. The tooltip flips direction automatically near the map edges.
### Click Selection
Clicking a plot highlights it in gold and opens a pinned detail panel (bottom-left). Clicking empty map or pressing ✕ clears the selection.
### Measure Tool
The 📏 Measure button activates a distance tool. Click to place vertices; double-click to finish. The measured length is shown as a live floating label near the last vertex and reported in the status bar. The result respects the active unit system.
### Pan Mode
The ✋ Pan button temporarily suspends all digitizing tools and sets the cursor to a grab hand for comfortable map navigation. Useful mid-digitizing session when you need to reposition the view.
### Progress Bar
A floating progress bar appears at the bottom of the map during subdivision generation. It polls the backend every 400 ms and shows the current processing stage (parsing → roads → blocks → subdividing → QC → access enforcement → buildings → numbering).
---
## Subdivision Engine
### Processing Pipeline
```
1. Parse and validate the site boundary polygon (WGS84)
2. Validate configuration:
- Warn if min_area > 50% of site area
- Warn if max_area < min_area
- Warn if splay_radius is incompatible with road width
3. Parse road centrelines (with per-road type and width)
4. Parse existing building footprints
5. Build road network:
a. Buffer each user road to its configured width
b. Auto-generate grid roads for blocks that exceed max_block_length
(user roads and auto-roads are always combined)
c. Add perimeter access ring at half road-width inside boundary edge
6. Carve buildable area: boundary all roads all buildings
7. Extract contiguous buildable blocks
8. Generate cul-de-sacs for oversized blocks (if allow_culdesac = true)
9. Subdivide each block into rectangular plots:
a. Detect dominant axis via oriented bounding box (OBB)
b. Orient frontage to face the block's longest road-adjacent edge
c. Apply double-banking (back-to-back plots) where plot depth allows
d. Clip each plot cell to the actual block polygon
10. Quality pass A — Shape control:
Absorb undersized or badly-shaped plots into best-touching neighbour
11. Quality pass B — Access enforcement:
Any plot with no road adjacency is merged into its nearest
same-block neighbour that does have access (loop until stable)
12. Apply existing buildings:
Merge plots spanning a building footprint into one, mark status="built"
13. Renumber all plots sequentially using configured prefix and start number
14. Assign addresses: "Block N, Plot N"
15. Return GeoJSON feature collections + statistics + warnings
```
### Road Access Guarantee
Every plot is tested with `plot.buffer(1 m).intersects(road_union)`. Any plot that fails is merged iteratively into its nearest same-block neighbour that passes. The loop runs until all plots are accessible or no further merging is possible. Remaining inaccessible plots (rare edge cases on very irregular geometries) are flagged in red on the map and counted in the stats.
### Coordinate Scaling
All internal geometry is in WGS84 degrees. A scale factor is computed at the site centroid to convert metre-based parameters to degrees correctly at any latitude:
```
m_per_deg = (111 320 + 111 320 × cos(lat)) / 2
```
This ensures road widths, plot dimensions, and block lengths behave correctly whether the site is near the equator or at higher latitudes.
### Multi-User Safety
Each `POST /subdivide` request receives a unique `session_id`. Progress state is stored per session in an in-memory dict and purged 10 seconds after completion. There is no shared mutable state between concurrent requests.
---
## API Reference
### `POST /subdivide`
**Request body:**
```json
{
"boundary": {
"type": "Polygon",
"coordinates": [[[lon, lat], ...]]
},
"roads": [
{
"geometry": { "type": "LineString", "coordinates": [[lon, lat], ...] },
"road_type": "local",
"width": null,
"label": "Main Access Road"
}
],
"existing_features": [
{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [[[lon, lat], ...]] },
"properties": {}
}
],
"config": {
"min_frontage": 10,
"min_plot_depth": 25,
"min_area": 250,
"max_area": 1000,
"default_road_width": 9,
"max_block_length": 120,
"splay_radius": 3,
"allow_culdesac": true,
"land_use": "residential_medium",
"parcel_prefix": "P",
"start_number": 1
},
"session_id": "optional-client-generated-id"
}
```
- `road_type`: `"primary"` | `"secondary"` | `"local"` | `"lane"`
- `width`: optional float (metres) to override the type default
- Both bare geometry dicts and GeoJSON Feature wrappers are accepted for all geometry fields.
**Response:**
```json
{
"parcels": [ ],
"roads": [ ],
"blocks": [ ],
"culdesacs": [ ],
"session_id": "...",
"stats": {
"total_parcels": 42,
"total_blocks": 4,
"total_roads": 5,
"culdesacs": 1,
"parcels_no_access": 0,
"parcels_built": 2,
"parcels_vacant": 40,
"boundary_area_m2": 18400.0,
"road_area_m2": 2100.0,
"buildable_area_m2": 14200.0,
"avg_parcel_area_m2": 338.1,
"min_parcel_area_m2": 215.0,
"max_parcel_area_m2": 490.0,
"existing_buildings": 2,
"user_roads_drawn": 2,
"land_use": "residential_medium",
"land_use_label": "Residential Medium Density",
"warnings": []
}
}
```
---
### `GET /progress/{session_id}`
Returns live progress for an ongoing or recently completed subdivision.
```json
{ "pct": 75, "msg": "Subdividing block 3/4…" }
```
`pct` ranges from 0100. A value of `-1` means the session was not found or errored.
---
### `GET /land-use-presets`
Returns the full land use preset table (min/max dimensions and areas for all 7 zone types).
---
### `GET /road-type-defaults`
Returns default widths and display colours for all 4 road types.
---
### `GET /health`
Returns `{"status": "ok", "version": "4.0.0"}`.
---
### `GET /config/defaults`
Returns the default `SubdivisionConfig` as a JSON object.
---
## Parcel Properties
Each parcel feature in the response includes:
```json
{
"parcel_id": "P0042",
"parcel_num": 42,
"block_id": 3,
"area_m2": 338.5,
"area_ha": 0.0339,
"frontage_m": 10.2,
"plot_depth_m": 33.2,
"address": "Block 3, Plot 42",
"land_use": "residential_medium",
"land_use_label": "Residential Medium Density",
"zone_color": "#3fb950",
"zone": "Residential Medium Density",
"status": "vacant",
"has_access": true,
"frontage_ok": true,
"area_ok": true,
"within_max_area": true,
"building_area_m2": 0
}
```
| Property | Type | Description |
|---|---|---|
| `parcel_id` | string | Formatted ID using prefix + sequential number |
| `frontage_m` | float | Road-facing plot width in metres |
| `plot_depth_m` | float | Perpendicular plot dimension in metres |
| `land_use` | string | Zone key (e.g. `residential_medium`) |
| `land_use_label` | string | Human-readable zone name |
| `zone_color` | string | Hex fill colour for the zone type |
| `status` | string | `"vacant"` or `"built"` (existing building present) |
| `has_access` | bool | Whether the plot has direct road adjacency |
| `frontage_ok` | bool | `true` if frontage ≥ 85% of min_frontage |
| `area_ok` | bool | `true` if area is within min/max bounds |
| `within_max_area` | bool | `true` if area ≤ max_area × 1.1 |
| `building_area_m2` | float | Area of existing building inside this plot (0 if vacant) |
---
## Colour Legend
| Colour | Meaning |
|---|---|
| Blue outline | Vacant residential (medium density) plot |
| Green outline | Vacant low-density / open-space plot |
| Yellow outline | Vacant high-density plot |
| Orange outline | Vacant commercial or industrial plot |
| Red outline | Vacant mixed-use plot |
| Orange fill, orange outline | Built plot (existing building footprint) |
| Red dashed outline | ⚠ Plot with no road access |
| Gold outline | Currently selected plot |
| Yellow fill | Road surface |
| Purple fill | Cul-de-sac |
| Green dashed line | Site boundary |
| Purple dashed outline | Digitized building footprint |
| Teal dashed line | Measure tool sketch |
---
## Export Formats — Technical Details
### GeoJSON
Standard RFC 7946 FeatureCollection. Includes a `metadata` block at the top level with generation timestamp and display units. Compatible with QGIS, PostGIS, Mapbox, Leaflet, and all major GIS tools.
### KML
Valid KML 2.2 document. Each feature becomes a `<Placemark>` with all parcel properties in a CDATA description block. Open in Google Earth Pro or import into Google My Maps.
### Shapefile
Pure in-browser Shapefile generation (no server required). The download is a `.zip` containing:
- `parcels.shp` — polygon geometry (type 5)
- `parcels.shx` — spatial index
- `parcels.dbf` — attribute table (parcel_id, area_m2, frontage_m, plot_dep, block_id, status, zone)
- `parcels.prj` — WGS84 projection definition
Open in ArcGIS, QGIS, MapInfo, or AutoCAD Map. The `.zip` is generated entirely in the browser using JSZip.
### CSV
Attribute-only export of all parcel properties as comma-separated values. No geometry column. Use in Excel, Google Sheets, or any data analysis tool.
---
## Development
### Backend (Python 3.11 / FastAPI)
```bash
cd backend
pip install -r requirements.txt
uvicorn main:app --reload --port 8000
```
The backend has no database dependency. All state is computed per request. The in-memory progress dict is the only cross-request state and is keyed by session ID.
### Frontend (plain HTML + OpenLayers)
Open `frontend/index.html` directly in a browser for development. The `API_URL` is auto-detected:
- If served on port 3000 → proxies to port 8000
- Otherwise → uses same origin with port 8000
No build step, no npm, no bundler. All dependencies are loaded from CDNs:
- **OpenLayers 9.1** — map rendering and interactions
- **JSZip 3.10** — in-browser Shapefile ZIP generation
---
## File Structure
```
parcel-tool/
├── docker-compose.yml
├── README.md
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── main.py ← Subdivision engine (v4)
└── frontend/
├── Dockerfile
├── nginx.conf
└── index.html ← Single-file OpenLayers UI (v4)
```
---
## Technical Notes
| Topic | Detail |
|---|---|
| **Coordinate system** | EPSG:4326 (WGS84) for all API I/O; EPSG:3857 (Web Mercator) for map display |
| **Geometry engine** | Shapely 2.x with GEOS backend |
| **Basemaps** | CARTO, OpenStreetMap, Esri — no API key required |
| **Theme persistence** | `localStorage` key `pg-theme` |
| **Unit system** | Imperial is display-only — backend always processes in metres |
| **Multi-user** | Stateless per request; progress keyed by session_id |
| **Mobile** | Responsive at ≤ 768 px; sidebar slides in/out |
| **Arc roads** | Free-form polyline — curved roads are supported by the digitizer and treated identically to straight roads by the subdivision engine |
| **Vertex delete** | Alt + Click on a vertex in Edit mode deletes that vertex only |
| **Right-click** | During active digitizing, right-click removes the last placed point |
| **Performance** | Sites up to ~50 ha typically complete in under 3 s on standard hardware |