diff --git a/README.md b/README.md index a61e24c..4962ac0 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,24 @@ -# πΊ Parcel Subdivision Tool +# ⬑ ParcelGen β Land Subdivision Tool -A full-stack GIS application for automatic land parcel generation from user-drawn site boundaries and road networks. +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 ββββββΆβ FastAPI Backend β β -β β (port 80) β β (port 8000) β β -β β + Frontend β β Shapely/GIS engine β β -β βββββββββββββββ ββββββββββββββββββββββββ β -ββββββββββββββββββββββββββββββββββββββββββββββββββββ - β² - β :8080 - Browser - (OpenLayers UI) -``` - -## Features - -| Feature | Description | -|---|---| -| **Draw Boundary** | Freehand polygon drawing on map | -| **Draw Roads** | Road centerline drawing (buffered to configured width) | -| **Auto Road Grid** | Automatic internal road generation if no roads drawn | -| **Block Detection** | Remaining buildable areas become blocks | -| **Cul-de-sacs** | Auto-generated for blocks exceeding max length | -| **Parcel Subdivision** | Rectangular parcel generation respecting min frontage/depth | -| **Shape Optimization** | Bad/small parcels absorbed by neighbors | -| **Addressing** | Auto address assignment per block/plot | -| **GeoJSON Export** | Download all results | -| **Layer Controls** | Toggle parcels, roads, blocks, cul-de-sacs | -| **Hover Tooltips** | Area, frontage, depth, address per parcel | - -## Default Parameters - -``` -min_frontage = 12 m -min_depth = 25 m -road_width = 9 m -max_block_length = 120 m -allow_culdesac = true -corner_radius = 3 m +ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ +β Docker Network β +β β +β ββββββββββββββββββββ ββββββββββββββββββββββββββ β +β β Nginx + Front ββββββΆβ FastAPI Backend v3 β β +β β (port 3000) β β (port 8000) β β +β β OpenLayers UI β β Shapely / GEOS engine β β +β ββββββββββββββββββββ ββββββββββββββββββββββββββ β +ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ + β² + β Browser + http://localhost:3000 ``` --- @@ -54,21 +26,17 @@ corner_radius = 3 m ## Quick Start ### Prerequisites -- Docker & Docker Compose installed +- Docker and Docker Compose ### Run ```bash -# Clone / extract project cd parcel-tool - -# Build and start all services docker compose up --build - -# Access at: -http://localhost:8080 ``` +Open **http://localhost:3000** in your browser. + ### Stop ```bash @@ -77,55 +45,161 @@ docker compose down --- -## Usage Guide +## Feature Overview -### Step 1 β Draw Boundary -1. Click **Draw Boundary** in the toolbar -2. Click on the map to add polygon vertices -3. **Double-click** to finish the polygon -4. Press **Escape** to cancel +### Drawing Tools -### Step 2 β (Optional) Draw Roads -1. Click **Draw Road** -2. Click to add road centerline vertices -3. Double-click to finish -4. Repeat for additional roads +| 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. | -> If no roads are drawn, the engine auto-generates an internal road grid based on `max_block_length`. +### Map Interaction -### Step 3 β Configure Parameters -Adjust the configuration panel: -- **Min Frontage** β minimum plot road frontage -- **Min Depth** β minimum plot depth -- **Road Width** β road right-of-way width -- **Max Block Length** β triggers cul-de-sac or cross-road insertion -- **Corner Radius** β road intersection corner rounding -- **Allow Cul-de-sacs** β toggle cul-de-sac generation +| 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 | -### Step 4 β Generate -Click **β‘ Generate Subdivision** +### Sidebar Tabs -Results appear color-coded: -- π΅ **Blue** β parcels -- β« **Dark grey** β roads -- π’ **Green dashed** β block boundaries -- π‘ **Yellow** β cul-de-sacs +- **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 -### Step 5 β Inspect & Export -- Hover over parcels to see attributes -- Click parcels to highlight/select -- Export all data as GeoJSON +--- + +## 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 `/api/subdivide` +### `POST /subdivide` ```json { - "boundary": { /* GeoJSON Polygon */ }, - "roads": [ /* GeoJSON LineStrings */ ], + "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, @@ -137,57 +211,66 @@ Results appear color-coded: } ``` +Both bare geometry dicts and GeoJSON Feature wrappers are accepted. + **Response:** + ```json { - "parcels": [ /* GeoJSON Features with properties */ ], - "roads": [ /* GeoJSON Features */ ], - "blocks": [ /* GeoJSON Features */ ], - "culdesacs": [ /* GeoJSON Features */ ], + "parcels": [ ], + "roads": [ ], + "blocks": [ ], + "culdesacs": [ ], "stats": { "total_parcels": 42, "total_blocks": 4, - "avg_parcel_area_m2": 340.5, - "culdesacs": 2 + "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 `/api/health` -Returns `{"status": "ok"}` +### `GET /health` +Returns `{"status": "ok", "version": "3.0.0"}` -### GET `/api/config/defaults` -Returns default configuration values. +### `GET /config/defaults` +Returns the default `SubdivisionConfig` as JSON. --- -## Subdivision Algorithm +## Map Colour Legend -``` -1. Parse boundary polygon (EPSG:4326 β WGS84) -2. Buffer user roads β road polygons - ββ If no roads: auto-generate grid roads at max_block_length intervals -3. Subtract roads from boundary β buildable blocks -4. For each oversized block (> max_block_length): - ββ Insert cul-de-sac if allow_culdesac=true -5. For each block: - a. Detect dominant orientation via OBB - b. Determine double/single frontage layout - c. Calculate parcel columns and rows - d. Clip each parcel cell to block boundary -6. Shape QC: - - Compactness check (reject triangular/complex shapes) - - Minimum area check - - Absorb rejected parcels into neighboring plots -7. Assign addresses: "Plot N, Block B Road" -8. Return GeoJSON FeatureCollection -``` +| 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 (Python/FastAPI) +### Backend ```bash cd backend @@ -196,7 +279,8 @@ uvicorn main:app --reload --port 8000 ``` ### Frontend -Pure HTML/JS β just open `frontend/index.html` in a browser (update API_BASE to `http://localhost:8000`). + +Open `frontend/index.html` directly in a browser. `API_URL` auto-detects: port 3000 proxies to port 8000, otherwise uses same origin. --- @@ -209,18 +293,20 @@ parcel-tool/ βββ backend/ β βββ Dockerfile β βββ requirements.txt -β βββ main.py β Subdivision engine -βββ frontend/ -β βββ index.html β OpenLayers UI -βββ nginx/ - βββ nginx.conf β Reverse proxy config +β βββ main.py β Subdivision engine (v3) +βββ frontend/ + βββ Dockerfile + βββ nginx.conf + βββ index.html β OpenLayers UI ``` --- -## Notes +## Technical Notes -- The map uses CartoDB Dark Matter basemap (no API key needed) -- Coordinate system: EPSG:4326 (WGS84) for I/O, EPSG:3857 for display -- Geometry engine: Shapely 2.x with GEOS backend -- For large sites (>100ha), processing may take a few seconds +- **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 \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 566549b..4767463 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,10 +1,1672 @@
- -