2026-03-03 12:38:36 +03:00
2026-03-03 11:33:31 +03:00
2026-03-03 11:33:31 +03:00
2026-03-03 12:38:36 +03:00
2026-03-03 11:33:31 +03:00
2026-03-03 11:33:31 +03:00
2026-03-03 12:38:36 +03:00

⬡ 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

cd parcel-tool
docker compose up --build

Open http://localhost:3000 in your browser.

Stop

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 (0100%, 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)

{
  "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

{
  "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:

{
  "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

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
Description
A improved version. improved UI/ux
Readme 195 KiB
Languages
HTML 76%
Python 23.7%
Dockerfile 0.3%