A web service that turns raw field data from engineering and cadastral surveys into ready-to-use survey plans. Plans are drawn as DXF (via ezdxf), converted to DWG with the ODA File Converter so they can be edited in AutoCAD, rendered to PDF, and uploaded as a ZIP bundle.
This service is drawing-only: it validates a plan payload, generates the drawing, and returns a download URL. User management, projects, and persistence are handled by a separate API server.
| Type | Endpoint | Description |
|---|---|---|
| Cadastral | POST /cadastral/plan |
Property beacons, parcel boundaries, bearing/distance labels |
| Topographic | POST /topographic/plan |
Spot heights, site boundary, TIN/grid contours |
| Route | POST /route/plan |
Plan-and-profile sheet (see below) |
| Layout | POST /layout/plan |
Estate subdivision schemes (see below) |
Route plans are drawn as the industry-standard plan-and-profile sheet:
- Plan view (horizontal alignment) — drawn when the payload carries
station coordinates (
coordinateswhose ids match theelevationsids). The route is rotated to run left-to-right above the profile, with chainage ticks/labels, right-of-way edges (route_parameters.right_of_way_width), and a north arrow rotated to match. - Longitudinal profile — existing ground level against chainage over a
station/elevation grid, at the scales in
longitudinal_profile_parameters.
Payloads without station coordinates draw the profile only (backward compatible).
Layout plans work in two modes:
- Draw mode — the payload provides the plot corner coordinate register
(
coordinates), theplots(corner ids per plot, with block/number/use), and optionallyroads; the scheme is drawn as given. - Generate mode — only the perimeter (
layout_boundary) and design parameters (layout_parameters) are provided. The subdivision is designed automatically using the standard Nigerian pattern: a major spine road along the site's long axis, cross streets limiting block length, double-loaded blocks of frontage x depth plots (default 15 m x 30 m), commercial plots along the spine, open-space and facility reservations, and per-block plot numbering with a land-use schedule table.
Either way the exported ZIP includes setting_out_coordinates.csv — the
coordinates of every boundary beacon, plot corner, and road centerline point,
ready for field setting-out.
Perimeter bearings/distances are computed upstream by the AutoPlan API and
arrive in the payload as layout_boundary.legs; when absent, plans are
drawn without the perimeter leg labels.
All endpoints accept a JSON payload described by models/plan.py
(PlanProps) and respond with:
{ "message": "Cadastral plan generated", "filename": "<plan name>", "url": "<zip url>" }Invalid payloads return 400 with validation details. See
tests/smoke_test.py for complete example payloads for every plan type.
app.py Flask entry point and endpoints
gunicorn.conf.py Production server settings (timeouts, worker recycling)
dxf_manager.py Low-level DXF drawing primitives (ezdxf wrapper)
models/plan.py Pydantic models: the JSON contract for plan payloads
plans/base.py Shared drawing logic (frame, title block, footers, north arrow)
plans/*.py One generator per plan type
utils.py Geometry and HTML→MText helpers
upload.py Cloudinary upload helper
tests/ Smoke test with sample payloads
Requirements: Python 3.11+ and, for DWG output, the ODA File Converter on
your PATH (DXF generation and the smoke test work without it).
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # fill in your Cloudinary URL
# generate sample plans without any credentials
python tests/smoke_test.py out/
# run the API
python app.py| Variable | Purpose | Default |
|---|---|---|
CLOUDINARY_URL |
Upload target for generated bundles | required for save() |
PORT |
HTTP port | 8080 |
WEB_CONCURRENCY |
Gunicorn workers | 1 |
GUNICORN_TIMEOUT |
Request timeout (seconds) | 300 |
GUNICORN_MAX_REQUESTS |
Requests per worker before recycling | 50 |
Worker recycling is deliberate: plan generation allocates large numpy/ezdxf buffers and CPython rarely returns that memory to the OS, so long-lived workers slowly grow. Recycling keeps memory bounded on small machines.
The Dockerfile installs the ODA File Converter and runs the service under Gunicorn:
docker build -t survey-plan-generator .
docker run --env-file .env -p 8080:8080 survey-plan-generatorPushes to main trigger .github/workflows/prod.yml, which:
- builds the Docker image and pushes it to Docker Hub as
<DOCKER_USERNAME>/autoplan-python:latest, then - connects to the production Ubuntu server over SSH and restarts the
service with Docker Compose (
docker compose pull && up -d).
The workflow needs these repository secrets: DOCKER_USERNAME,
DOCKER_PASSWORD, SERVER_HOST, SERVER_USERNAME, SERVER_SSH_KEY,
and SERVER_PORT.
- Fonts: text styles reference the font by file name (e.g.
Times New Roman.ttf). Install the fonts you use in the runtime environment or PDF output falls back to a default font. - The drawing is scaled so the output plan is at the requested scale
(
scale, default 1:1000); the graphical scale bar labels true ground distances.
MIT — see LICENSE.