Terrain and ground clearance#

OpenOHL builds a ground surface from survey points and checks how far every solved wire is above it.

cargo run -p ohl-cli -- line examples/synthetic-line/synthetic-line.ohl.json --temperature 80 \
  --terrain examples/synthetic-line/synthetic-survey.csv --clearance 7 --html line.html
terrain: 2960 points, 5694 triangles — loaded and 10 spans checked in 7.7ms
  lowest vertical ground clearance 4.64 m: section S2 span 1 at (464.0, 6.7)
  spans below the required 7.00 m: 1
    section S2         span   1:   4.64 m at (464.0, 6.7, wire 512.92 m)
Option Meaning
--terrain FILE Survey points (below). Enables the clearance check; the viewer draws the ground.
--clearance M Required vertical ground clearance, m: spans below it are listed and marked in the viewer.
--rules FILE Clearance rules (.ohlr, below): vertical and radial clearances to the ground, LiDAR classes, other lines and other wires of this line. --clearance M adds a vertical ground rule.
--other-line FILE Another line (.ohl.json) for wire-to-wire clearances; repeatable. Solved in the same weather case with the same libraries.
--clearance-csv FILE Every clearance found: rule,measure,section,span,other_section,other_span,clearance_m,required_m,violations,wire_x,…,target_x,…,class.

Run it once per weather case that governs clearance (e.g. maximum operating temperature, ice), with --temperature, --ice-thickness and --wind-pressure. Blown-out wires are checked over the ground they are blown over.

Plan and profile drawings#

--drawings DIR writes, for every route, a plan and profile sheet ROUTE.svg and the profiles as ROUTE-profile.csv:

ohl line project.ohl --pointcloud project.ohl --temperature 80 --rules rules.ohlr \
  --clearance 7 --drawings sheets --side-offset -15 --side-offset 15
  • Profile: ground along the route centreline (exact TIN profile), side ground profiles at --side-offset (right positive), the required-clearance line (ground + --clearance), structures with their stations, the route's wires and jumpers coloured by phase, and violation markers.
  • Plan: straightened along the route (station × offset at true scale), aligned with the profile.
  • Scales --hscale 2000 --vscale 200 (1:2000 / 1:200 by default); the plan across the line --plan-scale 1000 (default: the horizontal scale — along the line the plan always keeps the horizontal scale to stay aligned with the profile); plan half width --plan-width 40.
  • Sheets: --page A1 (A0–A4 landscape, or WIDTHxHEIGHT in mm) splits each route at structures — as many whole spans as fit per sheet, the structure ending a sheet starting the next (match line) — into ROUTE-sheet-NN.svg, each with its own height range and a title block (line, route, sheet n/N, station range, structures, scales, format, weather case). A span longer than the sheet or a profile taller than it is drawn anyway and reported as a warning: change the scales or the format.
  • Logos: --logo company.svg:30 (SVG, PNG or JPEG, width in mm; repeatable) in the title block.

The sheet layout (title block, margins) is built in for now; company templates will come as a layout plugin. For DXF, PDF, PNG and XML, station ranges and the profile alone, see Drawings and profile exports (ohl drawing).

Clearance rules (.ohlr)#

Every rule names a target and the required vertical and/or radial clearance:

  • vertical: to the ground, the wire's height above it directly below (negative: the wire is below the ground); to points, the vertical separation from points within vertical_band (default 0.5 m) of the wire in plan, above or below it; to other wires, the vertical separation where the wires cross in plan.
  • radial: the shortest 3D distance between the wire and the target.
  • Bundles (sections with bundle_spacing): to the nearest subconductor — the lowest one for vertical clearances below the wire, the highest above it, each subconductor for radial clearances.
format = "ohl-clearance-rules"
version = 1
name = "Example rules"

[[rule]]
name = "Ground"
target = "ground"
vertical = "7 m"
radial = "6.5 m"

[[rule]]
name = "Vegetation"
target = "points"            # LiDAR points, by role and/or class number
roles = ["vegetation"]
classes = [99]
vertical = "5 m"
radial = "4 m"

[[rule]]
name = "Phase to phase"
target = "same_line"         # other wires of this line: same span and other spans
radial = "3 m"

[[rule]]
name = "Crossed lines"
target = "other_lines"       # wires of the lines given with --other-line
vertical = "4.5 m"
radial = "4.5 m"
Field Meaning
target ground, points, same_line or other_lines.
roles, classes Points rules: classes by role (resolved with --lidar-classes) and/or by number.
vertical, radial Required clearances (at least one): a distance, or depending on the voltage (below).
pair_voltage Wire rules: the voltage of a pair of wires is the higher of the two systems' nominal voltages (default), their sum, or the difference — the actual voltage between the two conductors from phase angles and DC polarities.
vertical_band Points count for vertical clearance within this horizontal distance of the wire (default 0.5 m). For the ground: the highest ground within this distance (default 0: straight below the wire).
vertical_depth Vertical clearances to points and the ground: the depth along the line of the area under the wire (vertical_band its half width across), e.g. "0.2 m". The clearance is the shortest vertical distance between the wire and a point of the area: a point is compared with the wire's lowest point within that depth around it (its highest for a point above the wire). Default 0: the wire straight above the point.
voltages Only the wires of these nominal voltages are checked, e.g. [220] (kV; 0 for earthed wires); default: all. Wire rules: the first wire of a pair.
wires Which wires are checked: "sections" (the spans), "jumpers", or "both" (default).
search_margin Targets are reported within the required distance plus this (default 5 m); crossings of other wires are always reported.
include_same_phase same_line: also check wires of the same circuit and phase (default: no).
circuits same_line: "different" checks only pairs of wires of different circuits (circuit to circuit, e.g. the two circuits of a double-circuit tower; a phase and an earth wire), "same" only pairs of the same circuit (phase to phase); default "any". Combine with case_pairs for one circuit iced and the other not.
cases Weather cases the rule applies to, by name (* ? wildcards); default: all cases run.
exclude_near_ends Exclusion zone: the wire within this horizontal distance of its ends (clamps) is not checked, e.g. "10 m" — where wires converge at supports by design.
exclude_spans_at Exclusion zone: spans ending at these structures are not checked, e.g. ["con_*"] for down-leads into cable terminations (wildcards).
case_pairs Wire rules: [["glaze ice", "max temperature"]] — the first wire in the first case, the second wire in the second (e.g. an iced upper wire sagging towards a hot lower one); checked after all cases are solved.

A rule file may hold a [wind] table. The desktop vertical and radial clearance windows and ohl zones read it (the windows write it); ohl line ignores it and checks each weather case as it is:

[wind]
steps = true        # every pressure from 0 up to the case's, in steps
step = "50 Pa"
sides = "both"      # both, left or right

and a [check] table — what those windows and ohl zones solve and read; ohl line ignores it too:

[check]
circuits = ["North", "South"]   # circuit names; none: every circuit
method = "fe"                   # every section by finite elements ("rs": ruling span); none: each section's own
search_distance = "30 m"        # LiDAR points farther from the wires are ignored

Design-code plugins may give such rule files as clearance presets for those windows (Design codes).

Clearances by voltage#

Each section belongs to an electrical system (systems: AC or DC, voltage, phase angles) through its conductor path, independent of the cable; its spans and jumpers are checked with the requirement for the system's nominal voltage. vertical and radial can be a distance, a table by voltage, or a formula:

[[rule]]
name = "Buildings"
target = "points"
roles = ["building"]
# Rows [up to kV, distance]: the first row whose voltage is not exceeded.
vertical = { by_voltage = [[0, "5 m"], [150, "6 m"], [245, "6.5 m"], [420, "7.5 m"]] }
# base + per_kv · (U − above_kv) above above_kv, else base.
radial = { base = "5 m", per_kv = "0.01 m", above_kv = 50 }

With one rule per group of LiDAR classes (vegetation, buildings, roads, railways…), each with its own voltage table, the requirement follows both the class and the voltage. Earthed sections (no system: earth wires, OPGW) are checked as 0 kV — give a 0 row for them. Between wires, pair_voltage = "difference" uses the voltage actually between the two conductors: two phases of a 380 kV system meet the 380 kV row, the same phase of two parallel systems the 0 kV row, a phase and an earth wire 219 kV. A voltage above a table's last row stops the run with an error naming the rule, before any check. The report prints the voltages found; the CSV's required_m and the envelope rows give the requirement of each wire or pair.

Envelopes over weather cases#

With several weather cases (--weather), each case is reported in turn, then the envelope: for every rule and every wire (or wire pair), the lowest vertical and radial clearance over all cases with the case that governs it, and the cases in which a requirement is not met:

envelope over 3 weather cases (glaze ice, max temperature, rime ice + wind) and 1 case pairs (glaze ice × max temperature):
  rule "Ground": lowest vertical 4.64 m (S2 span 1, max temperature); lowest radial 4.63 m (S2 span 1, max temperature)
    below the requirement in some case: 1
      S2 span 1: vertical 4.64 m (max temperature), radial 4.63 m (max temperature) — in: glaze ice, max temperature

The clearance CSV holds each case's rows and then the envelope rows (case = envelope, last column governing_case). The plan and profile sheets draw every case's wires — the first solid, the others dashed, dotted… (named in the title block) — the envelope of the wire positions.

Wires sharing an end point (adjacent spans, a jumper and the sections it joins) are connected and never checked against each other. Point rules are evaluated during the same LiDAR pass that collects the ground. examples/synthetic-line/clearance-rules.ohlr and the crossing line crossing-line.ohl.json next to it show all wire rules on the synthetic model. Rule values come from the design code or the utility; the desktop clearance windows edit rules per LiDAR class, and design-code plugins may give rule files as clearance presets.

Vertical and radial clearances without the window (ohl zones)#

ohl zones runs the check of the desktop vertical and radial clearance windows without the program: a rule file gives the rules, the zone, the wind and what is solved; the project gives the point cloud and the ground.

ohl zones project.ohl --rules vertical-clearances.ohlr
ohl zones examples/synthetic-line/synthetic-line.ohl.json --rules rules.ohlr \
  --weather examples/synthetic-line/weather-cases.ohlwc \
  --terrain examples/synthetic-line/synthetic-survey.csv --csv zones.csv
vertical clearances: Demo (rules.ohlr) — 1 rule row(s); zone ±0 m in slices of 0 m, wind stepped by 100 Pa from the left and the right, method fe
libraries:
  cables: libraries/cables (shipped)
  structures: examples/synthetic-line/Structures (next to the file)
  insulators: libraries/insulators (shipped)
weather: examples/synthetic-line/weather-cases.ohlwc — 5 case(s), 2 wind position(s)
ground: survey file examples/synthetic-line/synthetic-survey.csv
points: not needed
solved 3 section(s) in 2 wind position(s) in 283.8ms; checked in 10.0ms; 0 LiDAR points read
1 span below a rule
  status    rule      span        case & wind       target   found m required m    count
  violation Ground    S2 span 2   max temperature   ground      4.72       7.50 16 pieces

The rule file is read as the windows import it: its ground and point rules with a clearance of one measure — vertical or radial, the only one the file has (a file with both needs --measure vertical or --measure radial); a requirement by voltage gives one row per voltage of the line; roles become the classes of the class scheme; the first rule's vertical_band and vertical_depth are the zone's width and depth. The [wind] table sets the wind (default: the cases' own pressure from both sides), the [check] table the circuits, the calculation method and the search distance. A window's own rule file (Export…, or File › Export › Libraries…) and a design code's clearance preset work as they are.

Option Meaning
--rules FILE The rule file (.ohlr); for a project, also the name of one of its own rule files.
--measure vertical|radial The measure, for a rule file with both.
--weather FILE Weather cases (.ohlwc; for a project, also the name of one of its own). Default: the file of the same name as the rule file, next to it or in a weather folder beside it (a design code's preset), else the project's weather case file used last, else the project's first weather case file.
--case NAME Only these weather cases (wildcards; repeatable). Default: every case the rules name.
--cloud SPEC Point cloud: FILE.ohl, FILE.ohl#name or FILE.copc.laz. Default: the project's own point cloud, if it holds one. --no-cloud reads none.
--terrain FILE, --surface NAME The ground: survey points, or a surface stored in the project. Default: the project's surface of the checks, else its active surface (as set by an earlier build), else the ground classes of the point cloud along the spans (lowest point per square metre, triangles of up to 50 m) — as the program builds it.
--lidar-classes FILE Class scheme (.ohlc); default the point cloud's own, else ASPRS.
--library, --structures, --insulators As for ohl line: for a project, the libraries it keeps with its current line; else (and for a kind the project holds none of) the shipped wire and insulator libraries, the Structures folder next to the file.
--search-distance M, --circuits NAME, --method fe|rs Override the rule file's [check] (--circuits repeatable; "": the sections without a circuit).
--csv FILE Every finding: status (violation, then ok for each rule's lowest clearance that keeps it), rule, section, span, case, wind pressure and side, target, class, clearance and required distance, count, the wire and target points.
--time-limit S Stop after this many seconds (default 600): solving every wind position and reading the points.

The report says where each library came from (cables: the project's (2 files), --library, shipped, next to the file), lists the sections solved, the wind positions, the LiDAR points read and the time taken, then the findings as the window shows them: the violations, worst first, then each rule's lowest clearance. A rule of several voltages makes one row per voltage; the summary keeps them together: one line NAME: nothing within reach (220, 380 kV) per rule name, and one note no wire of 150, 125, 110 kV: 12 rule rows not used for the rows of voltages the line does not have (a note repeated is printed once with its count). The CSV keeps one row per finding. The exit status is 0 with no violation, 1 with violations, 2 on an error (a rule file with both measures, no weather case, the time limit) — for scripts and batch runs.

Survey files#

Plain text, one point per line, separated by commas, semicolons, tabs or spaces. Empty lines and lines starting with # are ignored. Coordinates are metres in the line's coordinate system.

Without a header the columns are x y z [code] [line]:

# x, y, z, feature code, breakline id
2600010.00,1200020.00,512.31,GND,
2600030.00,1200020.00,512.80,ROAD,road-left

With a header, columns are found by name, so other layouts work too:

Column Names
x (easting) x, e, east, easting
y (northing) y, n, north, northing
z (elevation) z, h, elev, elevation, height
feature code code, d, desc, description, feature
breakline id line, breakline, string

Other columns (e.g. a point number) are ignored, so a PNEZD export only needs the header p,n,e,z,d.

  • Feature codes are kept with each point (e.g. GND, ROAD, TREE). They will select what each clearance rule applies to; v1 uses all points as ground.
  • Breaklines: consecutive points (in file order) with the same non-empty breakline id are joined by edges the triangulation must keep — ridges, ditches, road edges, embankment toes. Without them a triangle may cut across a ditch. A breakline edge that would cross an earlier breakline is skipped and counted in the output.
  • Points with the same x and y are merged (the first one is kept).

Ground from LiDAR#

cargo run --release -p ohl-cli -- line LINE.ohl.json --lidar TILES_DIR \
  --lidar-classes SCHEME.ohlc --clearance 8
Option Meaning
--lidar PATH LAS / LAZ file or directory of tiles. (--pointcloud instead reads a project's point cloud or a COPC file, only the blocks or octree nodes of the corridor: Point clouds.)
--lidar-classes FILE Class scheme (below); default: ASPRS LAS 1.4 standard classes.
--ground-role ROLE Classes with this role make the ground surface (default ground).
--corridor M Only points within M of the spans (default 60 m). Every tile is read (the bounds in tile headers are not trusted); points flagged withheld are left out.
--max-edge M Longest triangle side (default 50 m): no ground is invented across larger gaps.
--thin M Optional: keep the lowest ground point per M × M cell.

ohl lidar info PATH --classes [--scheme FILE] lists the tiles and counts the points of every class, with the scheme's names and roles.

Class schemes (.ohlc)#

What a class number means depends on who classified the data — the ASPRS LAS standard, or a utility's or surveyor's own specification. A class scheme names every class and gives it roles; tools select classes by role, so any classification works. A class can have several roles, and roles are free words:

format = "ohl-lidar-classes"
version = 1
id = "my-survey"
name = "Survey company X, project Y"
source = "Survey specification v2"   # optional: where the scheme comes from

[[class]]
code = 1
name = "Terrain"
roles = ["ground"]

[[class]]
code = 5
name = "Asphalt road"
roles = ["ground", "road"]

The ASPRS LAS 1.4 scheme is in libraries/lidar-classes/asprs-las-1.4.ohlc (ground: 2, water 9, road surface 11). Used by OpenOHL: ground (the ground surface), vegetation (vegetation analysis), conductor and shield_wire (as-built wire fitting), and any role a clearance rule names; the application also colours points by role. Suggested: vegetation, building, water, road, railway, bridge, structure, insulator, conductor, shield_wire, load_object, other_line_structure, other_line_conductor, object, noise, temporary, unclassified.

On a real 25 km line (private data, checked locally), ground built this way from 99.6 million LiDAR points (11.3 million in the utility's ground classes, 22.6 million triangles, about one minute) gave span clearances matching those on the TIN of a commercial program to a median of 2 mm (94 % of 930 spans within 10 cm).

A class may have a display colour, color = "#rrggbb"; without one, its colour comes from its roles (ground brown, vegetation green, buildings red, water blue, wires yellow…). In the application, Terrain › Survey data › Class colours… edits them per class and saves the scheme to a file; the scheme used is each user's choice (for a project or every project), so the same point cloud can be read with another scheme (e.g. a utility's own class numbers stored as ASPRS).

The ground surface#

The points are triangulated (constrained Delaunay, the spade library) into a TIN. A finished mesh (an imported TIN, or the output of a terrain-building plugin) can be used as it is. The ground height anywhere inside the surveyed area (the convex hull of the points) is the linear interpolation on the triangle below. Outside it there is no ground: spans that are not fully over the survey are reported.

Backgrounds: orthophotos and drawings#

Terrain › Backgrounds keeps orthophotos and DXF drawings (e.g. the cadastre) in the project file, drawn under the line. Like point clouds and surfaces they belong to the project, not to the line: they are not logged and not undone.

  • Import orthophotos… — north-up images in the project's coordinate system: GeoTIFF (its tags give where it lies; a GeoTIFF naming another EPSG system than the project's is refused), or TIFF, JPEG, PNG with a world file next to them (.tfw, .jgw, .pgw, .wld, or the extension with w). Add files or a whole folder; they go into a layer (e.g. Orthophoto 2024) shown or hidden as one. Each image is cut into JPEG tiles of 256 pixels with a zoom pyramid (each level half the last, down to one tile; about a third more): the plan view reads only the tiles in sight at the level of the zoom (a coarse level first). Plain black or white tiles (no data) are not kept. Importing an image of the same name in the same layer replaces it. It runs in the background; the project opens again to show them. An image is decoded whole (3 bytes per pixel: 10 000 × 10 000 pixels need 300 MB).
  • ECW files are converted to GeoTIFF first by an external converter you install yourself: GDAL's gdal_translate with the ECW driver (ECW is a proprietary format whose decoder needs a licensed SDK; it is not part of OpenOHL, and GDAL's usual builds leave it out). Give it in the window (Find looks in the usual places: PATH, Homebrew, QGIS, OSGeo4W); it is remembered on this computer. Each .ecw is converted into a temporary GeoTIFF (its own georeferencing, else the .eww world file next to the ECW), imported, and the temporary file removed. Without such a GDAL, convert them once yourself (QGIS with ECW support, or the ECW vendor's free tools) to GeoTIFF — some mapping agencies publish their orthophotos as GeoTIFF directly.
  • Import DXF drawing… — an ASCII DXF file (any version; binary DXF is refused): lines, polylines (with their arcs), circles, arcs, ellipses, points and texts, by layer, in their colours (the DXF colour index or true colour; BYLAYER, BYBLOCK), at their own heights; blocks are inserted (scaled, turned, nested up to 8 deep) with their attributes. Hatches, dimensions, splines, faces and meshes are counted and not drawn (the import says which). Layers switched off in the file are not drawn. Coordinates are taken as they are (the project's system and units).
  • Remove backgrounds… — an orthophoto layer or a drawing, after a second click.

What is shown is yours (remembered on this computer, like the other view choices): Orthophotos and Drawings on or off, and each orthophoto layer and each drawing on its own — the menu stays open while you tick. Both are shown when a project opens.

  • Orthophotos are drawn in the plan view only.
  • Drawings are drawn in the plan view (texts from 6 pixels high, turned as in the file) and in the 3D view at their own heights — nothing is draped on the ground: a 2D drawing (heights 0) lies at height 0. Texts are not drawn in 3D.

Reproject project (Terrain › Coordinate system) moves the drawings with the project; orthophotos would need resampling, so they are left out of the new project: import them again in the new system.

Terrain surfaces in the project#

A project keeps any number of terrain surfaces (TINs, triangulated irregular networks), by name, compressed in the project file. You choose one as active: it is the ground — drawn, and used for clearances, profiles and placing structures. When a project with an active surface opens, the ground is read from it (a 12.7-million-triangle surface: about 1 s) instead of being built from the LiDAR every time. Other surfaces can be shown too, each in its colour (the Other surfaces layer, drawn in chunks with levels of detail like the ground).

Terrain › Terrain surfaces (TIN)… manages them:

  • Import… a file. Its format comes from the extension, or you choose it:

    Format Extension (no heading)
    LandXML .xml every TIN surface of the file (<Surface> with <Faces>; points are northing easting elevation)
    OBJ .obj a mesh (v x y z, f a b c …)
    Survey points .csv, .xyz triangulated, breaklines kept; option max_edge=50 leaves out longer triangles
    Terrain plugins as they say e.g. plugins/examples/esri-grid (ESRI ASCII grids, .asc)

    The first surface kept becomes the ground when there is none.

  • Build from LiDAR: tick the classes the ground is built from in the list of every LiDAR class of the model (code, name, points; it scrolls). At first the classes with the ground role are ticked; Ground classes, All and None set the ticks at once. Without LiDAR points the section is greyed out. Then the lowest point of each cell (default 1 m) of those classes, near the line or the whole cloud, is triangulated with a longest side (default 50 m) and kept under the surface name given.

  • Keep the ground shown as a surface: the ground built at opening (from the LiDAR or a survey file), kept so the next opening reads it.

  • Active (radio) and shown (tick, colour) are your own choice: remembered on this computer for this project, never written to the project nor sent to a server — everyone working on a hosted project decides what they show.

  • Checks (radio): the surface the checks (clearances) use, chosen for the project — one for everyone (a project setting; needs edit rights). None chosen: each user's ground. (The clearance checks will be reworked around the LiDAR points.)

  • Rename, Export… (LandXML .xml or OBJ .obj), Delete (confirmed; it cannot be undone — export first to keep a copy).

Reading, building and storing run in the background. With a project joined on a server, surfaces are imported and built on the hosting computer.

From the command line:

ohl surface list project.ohl
ohl surface import ground.xml --into project.ohl --user ann [--name N] [--format F] [--option max_edge=50] [--plugin DIR] [--replace]
ohl surface export project.ohl "ground" ground.xml
ohl surface rename project.ohl "ground" "survey 2026" --user ann
ohl surface delete project.ohl "ground" --user ann
ohl surface formats [--plugin DIR]

Ground profiles follow the TIN exactly: a profile point at every triangle edge crossed, with straight ground between them — no sampling step to choose and no ridge missed between samples.

Clearance#

For every span (and every jumper, reported as jumper:<id>) the vertical distance from the wire down to the ground is evaluated under every piece of the solved wire, at every TIN edge crossing, and the smallest value is reported with its location. The wire is a polyline of closely spaced points on the exact catenary; between those points it lies slightly below its chords, so clearances can be high by at most the chord sag of a piece — a few millimetres.

Several weather cases are checked together with envelopes; exclusion zones near the wire ends are rule fields (exclude_near_ends, exclude_spans_at); the plan and profile sheets draw the required-clearance line and the violations.

Example data#

examples/synthetic-line/synthetic-survey.csv is invented ground for the synthetic example line, generated by make_synthetic_survey.py: smooth ground through the structure bases, an 8 m hill under span T2–T3 and a road embankment surveyed as two breaklines across span T4–T5.

View this page's source on GitHub