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

23 KiB
Raw Permalink Blame History

⬡ 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
  2. Quick Start
  3. Interface Overview
  4. Digitizing Tools
  5. Configuration
  6. Land Use Zones
  7. Road Types
  8. Import Data
  9. Export Formats
  10. Map Controls
  11. Subdivision Engine
  12. API Reference
  13. Parcel Properties
  14. Colour Legend
  15. Development
  16. File Structure
  17. 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

cd parcel-tool
docker compose up --build

Open http://localhost:3000 in your browser (desktop or mobile).

Stop

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:

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

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

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

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

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