Open source  ·  MIT  ·  Python ≥ 3.11

gisc

A stateless civil/GIS compiler. It reads the LandXML and GIS files you already own — including the two things most readers get quietly wrong, clothoid spirals and station equations — answers one civil question about them, and writes exactly one folder with the provenance to prove it.

pip install gisc Source on GitHub Civil 3D COM notes
gisc compile corridor.conflicts \
  --alignment alignment.xml --utils utilities.geojson \
  --buffer-ft 15 --crs EPSG:2264 --out ./run

Two tasks today. corridor.conflicts asks what is inside N feet of an alignment; alignment.crossings asks where something crosses the centreline, and returns the crossing point with its station. gisc ir prints the whole compiled plan and executes nothing.

Stateless, and what that buys

gisc has no database. It reads your files in place, read-only, and writes one folder: the plan it compiled, the results, a summary, and provenance.json — source path, mtime, sha256, CRS in and out, filter and feature counts for every input.

Every emitted feature carries the source it came from. If gisc cannot say where a geometry came from, it does not emit it. A missing CRS is a non-zero exit, never a guess. And a run either replaces the previous one completely or leaves it exactly as it was — the old run is moved aside, not deleted, and restored on any failure.

Reading a LandXML station equation in Python

This is the one worth knowing about even if you never use gisc. An alignment can be re-stationed, and when it is, the station of a point stops being the distance to it. A <StaEquation> declares that at one point the stationing jumps, so the drawing reads 14+00 Back = 20+00 Ahead.

Ignore it and nothing looks wrong. The geometry parses, the CRS resolves, the offsets are right — and every station past the equation is off by the size of the equation:

a reader that ignores StaEquation says   2000.0
Civil 3D says                            2600.0

The mapping itself is simple once stated. The raw station is staStart plus distance along. At each equation, with staInternal = S and staAhead = A, everything past S is displayed at raw + (A − S). Two details bite:

Some exporters omit staInternal and give only staBack; the raw station then follows from the equations already applied, which means file order matters. gisc reads both forms and says which it used.

Flattening a LandXML clothoid spiral

A <Spiral spiType="clothoid"> is a transition whose curvature ramps linearly, so its heading is exact in closed form and only the sine and cosine of it need integrating:

θ(s) = θ₀ + sign · ( k₁·s + (k₂ − k₁)·s² / 2L )      k = 1/R, 0 for a tangent end

One expression covers an entry spiral, an exit spiral, and a compound spiral between two finite radii. Two things are easy to get wrong:

gisc integrates it, then checks the result against the <End> the file declares and refuses a spiral that does not land there — naming a reversed rot as the likely cause when flipping it would close.

Ground feet, grid feet, and the buffer

A projected CRS measures grid distance; a design clearance is a ground distance. In EPSG:3857 near 35.6°N the factor is 1.230, so an uncorrected 15 ft buffer covers 12.2 ft of real ground. State plane runs the other way, slightly under 1. gisc measures the local scale factor empirically — a geodesic walk on WGS 84 — rather than trusting a projection's analytic factor, which is wrong for Web Mercator on an ellipsoid.

Two things quietly give that correction back, so gisc handles both. A buffer is a polygon inscribed in the true circle, and at the usual 8 segments per quadrant a 15 ft corridor is 14.93 ft at its ends. And the scale factor is a property of a point, so over a long alignment one buffer distance cannot be right everywhere; gisc measures the drift and reports what it costs in feet.

Stationing is measured in the alignment's native design CRS, because a station is a grid distance in the system the drawing was made in.

Autodesk Civil 3D COM notes

Undocumented behaviour found while validating gisc against Civil 3D 2023 over COM. Recorded here because none of it is written down anywhere obvious, and the first one produces a valid-looking result that is completely wrong.

AddFreeSCSGroup1's sixth argument is GreaterThan180. The signature is (PreviousElementId, NextElementId, Spiral1Length, Radius, Spiral2Length, GreaterThan180, spiralDefinition). Passing 1 succeeds, returns a valid object, and fits the 315° reflex curve instead of the 45° one — 2900 ft where 985 ft was expected, with nothing complaining. Check the fitted length against a hand computation before trusting the result. spiralDefinition must be 1 for the clothoid; 0 raises.

Other things that cost time

Checked against Civil 3D, not against itself

Two alignments were built inside Civil 3D over COM and Civil 3D was asked for its own answers. The LandXML gisc reads was written from the geometry read back out of Civil 3D, so gisc parses Civil 3D's coordinates rather than the design that was fed in.

QuantityCivil 3D vs gisc
tangent–arc–tangent, length−0.0025 ft / 1035 ft
spiral–curve–spiral, length−0.0020 ft / 985 ft
centre-line XY, 40 stationsworst 0.0091 ft
clothoid TotalX / TotalY~7 × 10⁻¹¹ ft
offset, 8 probesworst 0.0073 ft
station across a station equationworst 0.0300 ft

The clothoid row is the one that says something. Civil 3D carries the spiral analytically and publishes its own TotalX/TotalY; gisc integrates it from nothing but the length and the two radii. Agreement to a ten-billionth of a foot means whatever error remains is chording, not the mathematics — and chording lands at 0.0091 ft against an advertised 0.01.

363 tests, 100% line coverage enforced in CI, on Linux and Windows across Python 3.11 and 3.12. The validation artifacts are committed, so the Civil 3D checks run anywhere without Civil 3D.

What it does not do