Table of Contents

Class GeoUtils

Namespace
LansMap.Core.Geo

Geographic utilities for building map features: bearings, destinations, path math, bounds fitting and point-in-polygon.

public static class GeoUtils
Inheritance
object
GeoUtils

Remarks

Pure and allocation-free, all doubles, with distances delegated to GeoMath and therefore on a spherical earth. Two different models appear here deliberately: bearings, destinations and midpoints are great-circle, while the fitting and interpolation helpers work in mercator so that motion and padding are uniform on screen rather than in degrees.

Fields

DefaultFitMaxZoom

Deepest zoom the fitting helpers return unless told otherwise, 22.

public const double DefaultFitMaxZoom = 22

Field Value

double

Remarks

Matches the default maxNativeZoom on TileSource, so a fit cannot ask for a tile deeper than the default provider serves. Clamping to Max instead is what put a single marker at zoom 28: the mercator span of one point falls to the 1e-12 floor below, the logarithm blows up, and the clamp catches it at the storage ceiling six levels past any served tile. Pass a larger value for a provider that serves deeper.

Methods

BearingDeg(LatLon, LatLon)

Initial great-circle bearing from one coordinate toward another.

public static double BearingDeg(LatLon a, LatLon b)

Parameters

a LatLon

Start coordinate, degrees WGS84.

b LatLon

Target coordinate, degrees WGS84.

Returns

double

Bearing in degrees clockwise from true north, 0 inclusive to 360 exclusive. This is the initial bearing: along a great circle it changes continuously, so it does not equal the reverse of the bearing from b to a.

BoundsOf(IReadOnlyList<LatLon>)

Smallest axis-aligned latitude and longitude box containing every given point.

public static GeoBounds BoundsOf(IReadOnlyList<LatLon> points)

Parameters

points IReadOnlyList<LatLon>

Coordinates in degrees WGS84.

Returns

GeoBounds

Bounds in degrees WGS84.

Remarks

Longitudes are compared numerically, the smallest box only while the set fits in a half-turn; a straddling set gets the complement instead, which GeoBounds cannot hold either. Use CalculateZoomAndCenter(IReadOnlyList<LatLon>, ScreenSize, int, double, double), LongitudeArcOf(IReadOnlyList<LatLon>) directly, or split at 180.

Exceptions

ArgumentException

points is null or empty.

CalculateCenter(IReadOnlyList<LatLon>)

Geographic centroid of a set of coordinates.

public static LatLon CalculateCenter(IReadOnlyList<LatLon> points)

Parameters

points IReadOnlyList<LatLon>

Coordinates in degrees WGS84. Must contain at least one point.

Returns

LatLon

Centroid in degrees WGS84.

Remarks

Averages 3D unit vectors rather than latitudes and longitudes, which is what makes it correct across the antimeridian; a naive longitude average puts the centroid of two points either side of it a hemisphere away. Input whose vectors cancel exactly, such as an antipodal pair, has no meaningful centroid and yields (0,0).

Exceptions

ArgumentException

points is null or empty.

CalculateZoomAndCenter(IReadOnlyList<LatLon>, ScreenSize, int, double, double)

Center and zoom that fit a set of points on screen. The "zoom to fit markers" entry point.

public static (LatLon Center, double Zoom) CalculateZoomAndCenter(IReadOnlyList<LatLon> points, ScreenSize screen, int tileSizePx, double paddingPx = 0, double maxZoom = 22)

Parameters

points IReadOnlyList<LatLon>

Coordinates to fit, degrees WGS84.

screen ScreenSize

Viewport size in screen px.

tileSizePx int

Tile edge length in px, 256 or 512.

paddingPx double

Margin to leave on every side, in screen px.

maxZoom double

Deepest zoom to return. Defaults to DefaultFitMaxZoom.

Returns

(LatLon Center, double Zoom)

The center in degrees WGS84 and the fractional zoom that fits the points.

Remarks

Equivalent to BoundsOf(IReadOnlyList<LatLon>) followed by FitViewToBounds(GeoBounds, ScreenSize, int, double, double) whenever LongitudeArcOf(IReadOnlyList<LatLon>)'s arc is the raw numeric box (its west edge equals the raw numeric minimum longitude); otherwise delegates to FitViewToPoints(IReadOnlyList<LatLon>, double, ScreenSize, int, double, double) at rotation 0, which measures from that same arc's west edge. The two paths therefore always agree.

Exceptions

ArgumentException

points is null or empty, the padding leaves no room in screen, or maxZoom is out of range.

DestinationAfter(LatLon, double, double, double)

Dead reckoning: where something travelling at a constant speed and heading will be after a given time.

public static LatLon DestinationAfter(LatLon start, double speedMetersPerSecond, double headingDeg, double seconds)

Parameters

start LatLon

Start coordinate, degrees WGS84.

speedMetersPerSecond double

Ground speed in meters per second.

headingDeg double

Heading in degrees clockwise from true north, held constant as an initial great-circle bearing.

seconds double

Elapsed time in seconds.

Returns

LatLon

Predicted position in degrees WGS84.

DestinationPoint(LatLon, double, double)

The coordinate reached by travelling a distance along a bearing on a great circle.

public static LatLon DestinationPoint(LatLon start, double distanceMeters, double bearingDeg)

Parameters

start LatLon

Start coordinate, degrees WGS84.

distanceMeters double

Distance to travel in meters.

bearingDeg double

Initial bearing in degrees clockwise from true north.

Returns

LatLon

Destination in degrees WGS84, longitude normalized to -180 to 180 so crossing the antimeridian returns a valid coordinate.

FitViewToBounds(GeoBounds, ScreenSize, int, double, double)

Center and zoom that fit a bounding box on screen with padding.

public static (LatLon Center, double Zoom) FitViewToBounds(GeoBounds bounds, ScreenSize screen, int tileSizePx, double paddingPx = 0, double maxZoom = 22)

Parameters

bounds GeoBounds

Box to fit, degrees WGS84.

screen ScreenSize

Viewport size in screen px.

tileSizePx int

Tile edge length in px, 256 or 512.

paddingPx double

Margin to leave on every side, in screen px. Must be zero or greater, and must leave a positive area inside the viewport.

maxZoom double

Deepest zoom to return. Defaults to DefaultFitMaxZoom.

Returns

(LatLon Center, double Zoom)

The center in degrees WGS84 and the fractional zoom that fits the box. Zoom is clamped to 0 through maxZoom, and is the tighter of the horizontal and vertical fits so nothing is cropped.

Remarks

The center is the mercator midpoint, so the fit is symmetric on screen. The only screen this rejects is one the padding does not fit in: at padding 0 no screen is ever rejected, and a 1x1 viewport returns a fit for a 1 px view. A degenerate box drives the mercator span to the 1e-12 floor below and the fit to maxZoom. Forwards to the rotation overload at 0 degrees: cos and sin of 0 are exact under IEEE arithmetic, so the result is bit-identical.

Exceptions

ArgumentException

paddingPx is negative, the padding leaves no room in screen, or maxZoom is outside 0 through Max.

FitViewToBounds(GeoBounds, double, ScreenSize, int, double, double)

Center and zoom that fit a bounding box on screen with padding, under a given bearing.

public static (LatLon Center, double Zoom) FitViewToBounds(GeoBounds bounds, double rotationDeg, ScreenSize screen, int tileSizePx, double paddingPx = 0, double maxZoom = 22)

Parameters

bounds GeoBounds

Box to fit, degrees WGS84.

rotationDeg double

Bearing the fit is evaluated under, same convention as RotationDeg. Does not change any view's own bearing; it only picks which screen extents the fit sizes against.

screen ScreenSize

Viewport size in screen px.

tileSizePx int

Tile edge length in px, 256 or 512.

paddingPx double

Margin to leave on every side, in screen px.

maxZoom double

Deepest zoom to return. Defaults to DefaultFitMaxZoom.

Returns

(LatLon Center, double Zoom)

The center in degrees WGS84 and the fractional zoom that fits the box under the given bearing.

Remarks

A rotated rectangle's screen extent has a closed form: corners sit at (+-w/2, +-h/2) from the center, so one rotated span is |cos| w + |sin| h and the other |sin| w + |cos| h. Centrally symmetric, so un-rotating the extent's own midpoint recovers the box's mercator midpoint

  • no corner list, no loop, nothing to fold, since a GeoBounds cannot represent a seam-crossing span.

Exceptions

ArgumentException

paddingPx is negative, the padding leaves no room in screen, or maxZoom is outside 0 through Max.

FitViewToPoints(IReadOnlyList<LatLon>, double, ScreenSize, int, double, double)

Center and zoom that fit a set of points on screen under a given bearing, so a shape that is not axis-aligned on screen still fills the padded viewport instead of its larger north-up bounding box.

public static (LatLon Center, double Zoom) FitViewToPoints(IReadOnlyList<LatLon> points, double rotationDeg, ScreenSize screen, int tileSizePx, double paddingPx = 0, double maxZoom = 22)

Parameters

points IReadOnlyList<LatLon>

Coordinates to fit, degrees WGS84.

rotationDeg double

Bearing the fit is evaluated under, same convention as RotationDeg. Does not change any view's own bearing; it only picks which screen extents the fit sizes against.

screen ScreenSize

Viewport size in screen px.

tileSizePx int

Tile edge length in px, 256 or 512.

paddingPx double

Margin to leave on every side, in screen px.

maxZoom double

Deepest zoom to return. Defaults to DefaultFitMaxZoom.

Returns

(LatLon Center, double Zoom)

The center in degrees WGS84 and the fractional zoom that fits the points under the given bearing.

Remarks

Mirrors MercatorToScreen(MercatorPoint): rotates each point's mercator offset from LongitudeArcOf(IReadOnlyList<LatLon>)'s west edge by rotationDeg before measuring its extent. The arc is chosen from longitudes alone, before rotation touches a number, so a straddling or wide set fits its true smallest arc at every bearing. At rotation 0 this equals CalculateZoomAndCenter(IReadOnlyList<LatLon>, ScreenSize, int, double, double) for every input. Pitch is ignored.

Exceptions

ArgumentException

points is null or empty, the padding leaves no room in screen, or maxZoom is out of range.

IsInsidePolygon(LatLon, IReadOnlyList<LatLon>)

Whether a coordinate lies inside a polygon ring.

public static bool IsInsidePolygon(LatLon point, IReadOnlyList<LatLon> ring)

Parameters

point LatLon

Coordinate to test, degrees WGS84.

ring IReadOnlyList<LatLon>

Ring vertices in degrees WGS84, in either winding order. The ring closes implicitly, so do not repeat the first vertex.

Returns

bool

True when the point is inside. False when the ring is null or has fewer than three vertices. Points exactly on an edge are not guaranteed either way.

Remarks

Even-odd ray cast treating latitude and longitude as plane coordinates. Adequate for regions well away from the poles and the antimeridian, and wrong for rings that cross either.

LongitudeArcOf(IReadOnlyList<LatLon>)

The smallest arc of longitude containing every point's longitude.

public static (double WestLonDeg, double SpanDeg) LongitudeArcOf(IReadOnlyList<LatLon> points)

Parameters

points IReadOnlyList<LatLon>

Coordinates in degrees WGS84.

Returns

(double WestLonDeg, double SpanDeg)

The arc's west edge, in [-180, 180), and its width, in [0, 360). The arc crosses the antimeridian exactly when the west edge plus the width exceeds 180.

Remarks

The smallest arc containing longitudes is 360 minus the largest gap between neighbours on the circle. Inside a half-turn the numeric box is already that arc, so that case is sort-free; a wider set rents a sort buffer from ArrayPool<T> instead of a shared static one, since two concurrent fits on this pure static class could otherwise corrupt each other's buffer. Ties: the wrap gap wins over any interior gap; otherwise the earlier sorted gap wins.

Exceptions

ArgumentException

points is null or empty.

Midpoint(LatLon, LatLon)

Great-circle midpoint of two coordinates, the halfway point along the shortest surface path.

public static LatLon Midpoint(LatLon a, LatLon b)

Parameters

a LatLon

First coordinate, degrees WGS84.

b LatLon

Second coordinate, degrees WGS84.

Returns

LatLon

Midpoint in degrees WGS84. Not the average of the two latitudes and longitudes, and correct across the antimeridian.

MoveTowards(LatLon, LatLon, double)

Interpolates between two coordinates in mercator space, the building block for fly-to and cluster glide animations.

public static LatLon MoveTowards(LatLon from, LatLon to, double t)

Parameters

from LatLon

Start coordinate, degrees WGS84.

to LatLon

Target coordinate, degrees WGS84.

t double

Fraction of the way from start to target, clamped to 0 through 1.

Returns

LatLon

Interpolated coordinate in degrees WGS84. Longitude takes the short way around the antimeridian and stays in range; latitude is clamped to the mercator limit.

Remarks

Interpolating in mercator rather than in degrees is what makes the motion uniform on screen, which is the point: a latitude lerp visibly accelerates as it approaches the poles.

PathLengthMeters(IReadOnlyList<LatLon>)

Total length of a path, summed over its segments.

public static double PathLengthMeters(IReadOnlyList<LatLon> path)

Parameters

path IReadOnlyList<LatLon>

Ordered coordinates in degrees WGS84.

Returns

double

Length in meters, or 0 when the path is null or has fewer than two points.

PointAlongPath(IReadOnlyList<LatLon>, double)

The point a given fraction of the way along a path by distance, for animating something down a route.

public static LatLon PointAlongPath(IReadOnlyList<LatLon> path, double t)

Parameters

path IReadOnlyList<LatLon>

Ordered coordinates in degrees WGS84.

t double

Fraction of total length, clamped to 0 through 1. Fraction of distance, not of segment count, so evenly spaced t gives even speed regardless of how the path is subdivided.

Returns

LatLon

Position in degrees WGS84. A single-point path returns that point, and so does a path whose total length is zero.

Remarks

Interpolates latitude and longitude linearly inside a segment rather than along a great circle: accurate enough for route and track segments, and cheap enough to call every frame. Longitude takes the short way around the antimeridian, the same fold MoveTowards(LatLon, LatLon, double) applies in mercator, because the fraction comes from a haversine length that already took the short arc: a raw degree lerp against that fraction puts the midpoint of a 179 to -179 path on the antipode. The result is normalized through WrapUnit(double) so both helpers spell the seam identically, as -180.

Exceptions

ArgumentException

path is null or empty.