312 lines
9.3 KiB
Markdown
312 lines
9.3 KiB
Markdown
# ⬡ ParcelGen — Land Subdivision Tool
|
||
|
||
A full-stack GIS application for automatic land parcel generation from user-drawn site boundaries, road networks, and existing building footprints.
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ Docker Network │
|
||
│ │
|
||
│ ┌──────────────────┐ ┌────────────────────────┐ │
|
||
│ │ Nginx + Front │────▶│ FastAPI Backend v3 │ │
|
||
│ │ (port 3000) │ │ (port 8000) │ │
|
||
│ │ OpenLayers UI │ │ Shapely / GEOS engine │ │
|
||
│ └──────────────────┘ └────────────────────────┘ │
|
||
└──────────────────────────────────────────────────────┘
|
||
▲
|
||
│ Browser
|
||
http://localhost:3000
|
||
```
|
||
|
||
---
|
||
|
||
## Quick Start
|
||
|
||
### Prerequisites
|
||
- Docker and Docker Compose
|
||
|
||
### Run
|
||
|
||
```bash
|
||
cd parcel-tool
|
||
docker compose up --build
|
||
```
|
||
|
||
Open **http://localhost:3000** in your browser.
|
||
|
||
### Stop
|
||
|
||
```bash
|
||
docker compose down
|
||
```
|
||
|
||
---
|
||
|
||
## Feature Overview
|
||
|
||
### Drawing Tools
|
||
|
||
| Tool | Description |
|
||
|---|---|
|
||
| **◇ Boundary** | Draw the site boundary polygon. Double-click to finish. Only one boundary is kept at a time. |
|
||
| **⟋ Road** | Draw road centrelines. Each line is buffered to the configured road width. Draw multiple; all are used. |
|
||
| **▦ Building** | Draw existing building footprints. Carved out before subdivision; containing parcel flagged as "built". |
|
||
| **📏 Measure** | Click to place measure points, double-click to finish. Shows live distance in the active unit system (m/km or ft/mi). |
|
||
| **✕ Stop** | Exit any active draw or measure mode. |
|
||
|
||
### Map Interaction
|
||
|
||
| Action | Result |
|
||
|---|---|
|
||
| **Hover** over a parcel | Floating tooltip: area (dual units), frontage, depth, status, road access |
|
||
| **Click** a parcel | Pins a detail panel bottom-left; highlights parcel in gold |
|
||
| **Click** empty map | Clears selection |
|
||
| **Scroll / drag** | Standard map pan and zoom |
|
||
|
||
### Sidebar Tabs
|
||
|
||
- **Draw** — Drawing tools and Run Subdivision / Export buttons
|
||
- **Config** — Subdivision parameters with live unit conversion
|
||
- **Layers** — Visibility, opacity, basemap switcher, and legend
|
||
- **Results** — Stats dashboard and scrollable parcel list
|
||
|
||
---
|
||
|
||
## UI Controls
|
||
|
||
### Theme Toggle (sun/moon button)
|
||
|
||
Click the ☀️ / 🌙 button in the top-left of the sidebar header to switch between **dark** and **light** themes. The preference is saved in `localStorage` and restored on next visit. Switching theme also switches the default basemap automatically.
|
||
|
||
### Unit System (m / ft)
|
||
|
||
Click the **m** or **ft** pill to toggle metric/imperial. All config inputs convert in-place. Results display both units. The backend always receives and returns SI (metres, m²).
|
||
|
||
### Basemap Switcher
|
||
|
||
12 basemaps in 5 categories, accessible from the **Layers** tab or the floating **Basemap** panel (bottom-right):
|
||
|
||
| Category | Options |
|
||
|---|---|
|
||
| Style | Dark (CARTO), Light (CARTO), Night Lights |
|
||
| Street | OpenStreetMap |
|
||
| Terrain | OpenTopoMap, ESRI Topo, ESRI Shaded Relief, Ocean |
|
||
| Imagery | ESRI Satellite, ESRI Satellite + Labels |
|
||
| Artistic | Stamen Terrain, Stamen Watercolor |
|
||
|
||
### Layer Switcher
|
||
|
||
Both the floating **Layers** panel and the **Layers** tab expose per-layer controls:
|
||
- Visibility checkbox
|
||
- Opacity slider (0–100%, live update)
|
||
- Colour swatch
|
||
|
||
Layers: Boundary · Buildings (drawn) · Roads (drawn) · Road surfaces · Cul-de-sacs · Blocks · Parcels
|
||
|
||
---
|
||
|
||
## Subdivision Engine
|
||
|
||
### Algorithm
|
||
|
||
```
|
||
1. Parse boundary polygon (WGS84 / EPSG:4326)
|
||
2. Parse user-drawn road centrelines + existing building footprints
|
||
3. Build road network:
|
||
a. Buffer user roads → road polygons
|
||
b. Auto-generate grid roads to fill blocks > max_block_length
|
||
(combined with user roads, never either/or)
|
||
c. Add perimeter access ring inside the boundary edge
|
||
4. Carve: buildable = boundary − roads − buildings
|
||
5. Extract contiguous buildable blocks
|
||
6. Generate cul-de-sacs for oversized blocks (if enabled)
|
||
7. Subdivide each block into rectangular parcels:
|
||
a. Detect dominant orientation via oriented bounding box
|
||
b. Double-bank parcels back-to-back where depth allows
|
||
c. Clip each cell to the actual block polygon
|
||
8. Quality passes:
|
||
a. Absorb undersized / badly-shaped parcels into neighbours
|
||
b. Enforce road access — merge landlocked parcels into accessed neighbour
|
||
9. Apply existing buildings → merge spanning parcels, mark status="built"
|
||
10. Re-number sequentially, assign addresses
|
||
11. Return GeoJSON + stats
|
||
```
|
||
|
||
### Parcel Access Guarantee
|
||
|
||
Every parcel is checked for road adjacency. Landlocked parcels are merged into their nearest same-block neighbour that has road access. This loop runs until stable. Any remaining inaccessible parcels are flagged in red on the map and counted in the stats panel.
|
||
|
||
### Metric Scaling
|
||
|
||
All internal geometry is in WGS84 degrees. A scale factor (metres-per-degree) is computed at the site centroid so that metre-based config values work correctly at any latitude:
|
||
|
||
```
|
||
m_per_deg = (111 320 + 111 320 × cos(lat)) / 2
|
||
```
|
||
|
||
---
|
||
|
||
## Default Configuration
|
||
|
||
| Parameter | Default | Description |
|
||
|---|---|---|
|
||
| `min_frontage` | 12 m | Minimum parcel road frontage |
|
||
| `min_depth` | 25 m | Minimum parcel depth |
|
||
| `road_width` | 9 m | Road right-of-way width |
|
||
| `max_block_length` | 120 m | Max block length before road/cul-de-sac insertion |
|
||
| `allow_culdesac` | true | Generate cul-de-sacs for oversized blocks |
|
||
| `corner_radius` | 3 m | Road corner rounding radius |
|
||
|
||
---
|
||
|
||
## Parcel Properties (GeoJSON)
|
||
|
||
```json
|
||
{
|
||
"parcel_id": "P0042",
|
||
"parcel_num": 42,
|
||
"block_id": 3,
|
||
"area_m2": 340.5,
|
||
"area_ha": 0.034,
|
||
"frontage_m": 12.3,
|
||
"depth_m": 27.7,
|
||
"address": "Block 3, Plot 42",
|
||
"zone": "Residential",
|
||
"status": "vacant",
|
||
"has_access": true,
|
||
"frontage_ok": true,
|
||
"area_ok": true,
|
||
"building_area_m2": 0
|
||
}
|
||
```
|
||
|
||
`status` is `"built"` when an existing building footprint overlaps the parcel.
|
||
|
||
---
|
||
|
||
## API Reference
|
||
|
||
### `POST /subdivide`
|
||
|
||
```json
|
||
{
|
||
"boundary": { "type": "Polygon", "coordinates": [[...]] },
|
||
"roads": [
|
||
{ "type": "Feature", "geometry": { "type": "LineString", "coordinates": [[...]] }, "properties": {} }
|
||
],
|
||
"existing_features": [
|
||
{ "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [[...]] }, "properties": {} }
|
||
],
|
||
"config": {
|
||
"min_frontage": 12,
|
||
"min_depth": 25,
|
||
"road_width": 9,
|
||
"max_block_length": 120,
|
||
"allow_culdesac": true,
|
||
"corner_radius": 3
|
||
}
|
||
}
|
||
```
|
||
|
||
Both bare geometry dicts and GeoJSON Feature wrappers are accepted.
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"parcels": [ ],
|
||
"roads": [ ],
|
||
"blocks": [ ],
|
||
"culdesacs": [ ],
|
||
"stats": {
|
||
"total_parcels": 42,
|
||
"total_blocks": 4,
|
||
"total_roads": 3,
|
||
"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,
|
||
"existing_buildings": 2,
|
||
"user_roads_drawn": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
### `GET /health`
|
||
Returns `{"status": "ok", "version": "3.0.0"}`
|
||
|
||
### `GET /config/defaults`
|
||
Returns the default `SubdivisionConfig` as JSON.
|
||
|
||
---
|
||
|
||
## Map Colour Legend
|
||
|
||
| Colour | Meaning |
|
||
|---|---|
|
||
| Blue outline | Vacant parcel |
|
||
| Orange outline | Built parcel (existing building) |
|
||
| Red dashed outline | No road access ⚠ |
|
||
| Gold outline | Currently selected parcel |
|
||
| Yellow fill | Road surface |
|
||
| Purple fill | Cul-de-sac |
|
||
| Green dashed | Site boundary |
|
||
| Purple dashed | Drawn building footprint |
|
||
|
||
---
|
||
|
||
## Export
|
||
|
||
**Export GeoJSON** in the Draw tab downloads a `.geojson` file with all parcels, roads, and cul-de-sacs plus a `metadata` block (timestamp, display units). All attribute values are always in SI units regardless of the unit switcher.
|
||
|
||
---
|
||
|
||
## Development
|
||
|
||
### Backend
|
||
|
||
```bash
|
||
cd backend
|
||
pip install -r requirements.txt
|
||
uvicorn main:app --reload --port 8000
|
||
```
|
||
|
||
### Frontend
|
||
|
||
Open `frontend/index.html` directly in a browser. `API_URL` auto-detects: port 3000 proxies to port 8000, otherwise uses same origin.
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
```
|
||
parcel-tool/
|
||
├── docker-compose.yml
|
||
├── README.md
|
||
├── backend/
|
||
│ ├── Dockerfile
|
||
│ ├── requirements.txt
|
||
│ └── main.py ← Subdivision engine (v3)
|
||
└── frontend/
|
||
├── Dockerfile
|
||
├── nginx.conf
|
||
└── index.html ← OpenLayers UI
|
||
```
|
||
|
||
---
|
||
|
||
## Technical Notes
|
||
|
||
- **Coordinate system:** EPSG:4326 for API I/O; EPSG:3857 for map display
|
||
- **Geometry engine:** Shapely 2.x + GEOS
|
||
- **Basemaps:** CARTO, OSM, Esri, Stamen — no API key needed
|
||
- **Theme:** Persisted in `localStorage` under key `parcelgen-theme`
|
||
- **Imperial mode:** Display only — backend always receives metres
|
||
- **Performance:** Sites up to ~50 ha typically process in under 2 s |