2.30.7.13. pycsamt.map.geometry#

Real-geometry line-layout helpers for 3-D survey maps.

Multi-line 3-D views (fence, block, depth-slice) need two things per survey line: an along-line (“profile distance”) axis and a line-to-line (“offset”) position. Placing lines with a synthetic index * spacing stack ignores where the lines actually sit on the ground, which staggers panels that should be parallel and evenly spaced.

This module derives both from real station coordinates instead: every station is projected onto a shared local (u, v) frame — u along the survey’s dominant strike, v across it — so parallel lines stay parallel and the line-to-line spacing reflects their true geographic separation.

Pure numpy, no Plotly/Dash/pycsamt-app dependencies, so it is safe to import from both pycsamt.map (the code-first / mapview 3-D builder) and pycsamt.app.web (the legacy 3-D map callbacks) — whichever data structure each caller uses (MapData stations, or plain per-line dicts) is adapted to plain id/lat/lon/line arrays before calling in here.

Functions

equirect_xy(lat, lon)

Local planar metres for lat/lon, equirectangular approx.

fit_strike(points, groups)

Return the shared along-line unit direction for points.

normalize_offsets(offsets)

Shift offsets so the smallest value is zero.

resolve_offset(name, idx, real_offsets, ...)

Cross-strike placement for one line, in metres.

survey_frame(lat, lon, line)

Fit the shared local frame used by survey_uv().

survey_uv(ids, lat, lon, line)

Project every station onto a shared local (u, v) frame, metres.

Classes

SurveyFrame(mean_lat_deg, origin_xy, strike, ...)

A survey's local along/cross-strike frame, reusable for any point.

pycsamt.map.geometry.equirect_xy(lat, lon)[source]

Local planar metres for lat/lon, equirectangular approx.

Centred implicitly at the mean latitude of lat.

Parameters:
Return type:

tuple[ndarray, ndarray]

pycsamt.map.geometry.fit_strike(points, groups)[source]

Return the shared along-line unit direction for points.

Fits each group’s (line’s) own best-fit axis — SVD on that group’s centred points — then averages them, sign-aligned and weighted by point count. This reflects “which way each line actually runs” rather than the aggregate shape of the whole point cloud, which can pick the wrong axis when a survey’s footprint is roughly as wide (line-to-line) as it is long (along-line), e.g. a short/dense grid.

Returns None when no direction can be fit (fewer than two points overall, or a degenerate/singular point cloud).

Parameters:
Return type:

ndarray | None

pycsamt.map.geometry.survey_uv(ids, lat, lon, line)[source]

Project every station onto a shared local (u, v) frame, metres.

u is the along-strike distance, v the cross-strike offset, both measured from the combined centroid of every station passed in. ids, lat, lon, line must be the same length and aligned (one entry per station).

Returns {} when there are fewer than two stations or no strike direction could be fit (e.g. all stations coincide).

Parameters:
Return type:

dict[str, tuple[float, float]]

class pycsamt.map.geometry.SurveyFrame(mean_lat_deg, origin_xy, strike, perp)[source]

Bases: object

A survey’s local along/cross-strike frame, reusable for any point.

survey_uv() returns only the station projections; this exposes the frame itself so an arbitrary geographic point (a borehole collar, a target) can be placed in the same local metres the 3-D map builders use. project mirrors survey_uv() exactly: equirectangular metres about the survey’s mean latitude, then a shift to the station centroid and a rotation onto the fitted strike.

Parameters:
mean_lat_deg: float
origin_xy: tuple[float, float]
strike: tuple[float, float]
perp: tuple[float, float]
project(lat, lon)[source]

Return (u, v) local metres for one WGS84 lat/lon.

Parameters:
Return type:

tuple[float, float]

pycsamt.map.geometry.survey_frame(lat, lon, line)[source]

Fit the shared local frame used by survey_uv().

Returns None under the same conditions as survey_uv() (fewer than two stations, or no strike direction could be fit).

Parameters:
Return type:

SurveyFrame | None

pycsamt.map.geometry.normalize_offsets(offsets)[source]

Shift offsets so the smallest value is zero.

Returns None (signalling “no real geometry, fall back to a synthetic layout”) when offsets is empty or any value is missing/non-finite — mixing real and synthetic placement in the same plot would look inconsistent, so it is all-or-nothing.

Parameters:

offsets (dict[str, float | None])

Return type:

dict[str, float] | None

pycsamt.map.geometry.resolve_offset(name, idx, real_offsets, fallback_unit, spacing)[source]

Cross-strike placement for one line, in metres.

Uses the survey’s true line-to-line spacing (scaled by spacing, e.g. a UI “line spacing” multiplier where 1.0 means true spacing) when real_offsets is available, otherwise falls back to a synthetic idx * spacing * fallback_unit stack.

Parameters:
Return type:

float