Table of Contents

Class MapViewTransform

Namespace
LansMap.Core.View

Converts map positions and view angles into screen coordinates and back. The single seam between map state and the screen: an immutable snapshot of one view (center, zoom, bearing, pitch) that answers every projection question the renderer and the public API ask.

public sealed class MapViewTransform
Inheritance
object
MapViewTransform

Remarks

Every consumer derives from this one type: the renderer affine, the perspective matrix rows, ScreenToLatLon(ScreenPoint) and footprint selection. That is deliberate, so the shader and the API can never disagree about where a coordinate lands. The terrain model arrived as the optional sampler field below rather than as a new type, so that guarantee still holds.

Conventions, which differ between the three spaces and are the usual source of sign errors. Screen px have their origin at the top left with Y increasing downward, which is not Unity's input convention; ScreenMapView flips Y at the MonoBehaviour boundary. Normalized mercator is the unit square with Y also increasing downward, southward, so the two axes align at rotation 0. Clip space has Y increasing upward, and the flip lives inside the two Build methods.

Immutable and cheap: construct a new one per frame rather than mutating. All world coordinates are double, never float, because a float32 mercator coordinate cannot resolve a screen pixel at high zoom. Narrowing to float happens only at the GPU boundary, against an anchor near the view center; see NeedsReanchor(MercatorPoint).

Constructors

MapViewTransform(MapViewState, ScreenSize, int)

Builds a transform at true altitude scale.

public MapViewTransform(MapViewState state, ScreenSize screen, int tileSizePx)

Parameters

state MapViewState

Center, zoom, bearing and pitch of the view.

screen ScreenSize

Viewport size in screen px.

tileSizePx int

Tile edge length in px; must be 256 or 512.

Exceptions

ArgumentOutOfRangeException

tileSizePx is neither 256 nor 512.

MapViewTransform(MapViewState, ScreenSize, int, double)

Builds a transform with an explicit marker altitude scale.

public MapViewTransform(MapViewState state, ScreenSize screen, int tileSizePx, double altitudeScale)

Parameters

state MapViewState

Center, zoom, bearing and pitch of the view.

screen ScreenSize

Viewport size in screen px.

tileSizePx int

Tile edge length in px; must be 256 or 512.

altitudeScale double

Unitless multiplier on marker altitudes, 1 for true scale. Not validated.

Exceptions

ArgumentOutOfRangeException

tileSizePx is neither 256 nor 512.

MapViewTransform(MapViewState, ScreenSize, int, double, IElevationSampler)

Builds a terrain-aware transform: the forward projections displace by real ground height sampled from terrain.

public MapViewTransform(MapViewState state, ScreenSize screen, int tileSizePx, double altitudeScale, IElevationSampler terrain)

Parameters

state MapViewState

Center, zoom, bearing and pitch of the view.

screen ScreenSize

Viewport size in screen px.

tileSizePx int

Tile edge length in px; must be 256 or 512.

altitudeScale double

Unitless multiplier on marker altitudes and terrain height alike, 1 for true scale. Not validated.

terrain IElevationSampler

Ground height source for MercatorToScreen(MercatorPoint) and LatLonToScreen(LatLon). Passing null is equivalent to the flat constructor overloads.

Exceptions

ArgumentOutOfRangeException

tileSizePx is neither 256 nor 512.

MapViewTransform(MapViewState, ScreenSize, int, double, IElevationSampler, double)

Builds a terrain-aware transform with an explicit maximum view distance.

public MapViewTransform(MapViewState state, ScreenSize screen, int tileSizePx, double altitudeScale, IElevationSampler terrain, double maxViewDistanceFactor)

Parameters

state MapViewState

Center, zoom, bearing and pitch of the view.

screen ScreenSize

Viewport size in screen px.

tileSizePx int

Tile edge length in px; must be 256 or 512.

altitudeScale double

Unitless multiplier on marker altitudes and terrain height alike, 1 for true scale. Not validated.

terrain IElevationSampler

Ground height source for the forward projections. Null is equivalent to the flat overloads.

maxViewDistanceFactor double

How far to draw ground, in multiples of the camera distance. See MaxViewDistanceFactor; clamped, never rejected.

Exceptions

ArgumentOutOfRangeException

tileSizePx is neither 256 nor 512.

MapViewTransform(MapViewState, ScreenSize, int, double, IElevationSampler, double, double, double)

Builds a terrain-aware transform with explicit view distance and display density.

public MapViewTransform(MapViewState state, ScreenSize screen, int tileSizePx, double altitudeScale, IElevationSampler terrain, double maxViewDistanceFactor, double screenPixelScale, double maxTerrainElevationMeters = 0)

Parameters

state MapViewState

Center, zoom, bearing and pitch of the view.

screen ScreenSize

Viewport size in device pixels.

tileSizePx int

Source tile-scheme edge, 256 or 512.

altitudeScale double

Unitless marker and terrain altitude multiplier.

terrain IElevationSampler

Ground-height source; null keeps the map flat.

maxViewDistanceFactor double

Maximum ground reach in camera-distance multiples.

screenPixelScale double

Positive device pixels per logical map pixel.

maxTerrainElevationMeters double

See MaxTerrainElevationMeters. Zero, negative or NaN leave the margin off even with a sampler set; not validated beyond that.

MapViewTransform(MapViewState, ScreenSize, int, double, double)

Builds a transform with an explicit maximum view distance.

public MapViewTransform(MapViewState state, ScreenSize screen, int tileSizePx, double altitudeScale, double maxViewDistanceFactor)

Parameters

state MapViewState

Center, zoom, bearing and pitch of the view.

screen ScreenSize

Viewport size in screen px.

tileSizePx int

Tile edge length in px; must be 256 or 512.

altitudeScale double

Unitless multiplier on marker altitudes, 1 for true scale. Not validated.

maxViewDistanceFactor double

How far to draw ground, in multiples of the camera distance. See MaxViewDistanceFactor; clamped, never rejected.

Exceptions

ArgumentOutOfRangeException

tileSizePx is neither 256 nor 512.

MapViewTransform(MapViewState, ScreenSize, int, double, double, double)

Builds a transform with explicit view distance and display density.

public MapViewTransform(MapViewState state, ScreenSize screen, int tileSizePx, double altitudeScale, double maxViewDistanceFactor, double screenPixelScale)

Parameters

state MapViewState

Center, zoom, bearing and pitch of the view.

screen ScreenSize

Viewport size in device pixels.

tileSizePx int

Source tile-scheme edge, 256 or 512.

altitudeScale double

Unitless marker and terrain altitude multiplier.

maxViewDistanceFactor double

Maximum ground reach in camera-distance multiples.

screenPixelScale double

Positive device pixels per logical map pixel.

Exceptions

ArgumentOutOfRangeException

tileSizePx is neither 256 nor 512, or screenPixelScale is not finite and positive.

Fields

DefaultMaxTerrainElevationMeters

Default MaxTerrainElevationMeters for a terrain constructor overload with a non-null sampler: Everest plus headroom.

public const double DefaultMaxTerrainElevationMeters = 8900

Field Value

double

DefaultMaxViewDistanceFactor

Default MaxViewDistanceFactor: 2.366, the ground distance the top screen edge reaches at pitch 60, in multiples of the camera distance.

public const double DefaultMaxViewDistanceFactor = 2.366025403784439

Field Value

double

Remarks

Chosen so the cap is dormant at every pitch up to 60 - a view at or below 60 already stops there - and selection at those pitches is byte-identical to a build with no cap at all.

MaxViewDistanceFactorCeiling

Largest accepted MaxViewDistanceFactor. Values above it clamp down to it.

public const double MaxViewDistanceFactorCeiling = 4

Field Value

double

Properties

AltitudeScale

Unitless multiplier applied to every marker altitude, where 1 is true scale. Exaggerating height is a display choice, so this is a map setting rather than a per-marker one, and it is applied identically on the GPU and CPU paths so picking never disagrees with what is drawn.

public double AltitudeScale { get; }

Property Value

double

Remarks

Not validated, unlike every other input to this type: zero, negative and NaN are all accepted and propagate into the projections.

CenterMercator

The view center in normalized mercator, projected from State.Center. The snap below can push it outside the unit square by up to half a device pixel worth of mercator when the state center sits at the latitude clamp or the antimeridian.

public MercatorPoint CenterMercator { get; }

Property Value

MercatorPoint

Remarks

On a flat unrotated view (pitch 0, rotation 0) this is the pixel-snapped projection center and can differ from the direct projection of State.Center by up to half a device pixel per axis; it is the center every projection here actually uses, so it stays consistent with everything drawn.

MaxTerrainElevationMeters

Worst-case ground height above the flat plane, in meters, that NearEdgeTerrainMarginPx(double) and selection account for. Zero on every sampler-less constructor.

public double MaxTerrainElevationMeters { get; }

Property Value

double

MaxViewDistanceFactor

How far the map draws ground, in multiples of the camera distance.

public double MaxViewDistanceFactor { get; }

Property Value

double

Remarks

Ground beyond this distance is not selected, not drawn and not projected. Default DefaultMaxViewDistanceFactor, which is exactly the reach of the top screen edge at pitch 60, so the cap changes nothing at or below that tilt. Past it the cap is what keeps the tile count finite: without it the visible ground diverges at pitch 71.565 under this camera model.

Raising this fetches more distant tiles, and the cost grows with the square of it. Clamped into 1 to MaxViewDistanceFactorCeiling; NaN falls back to the default.

MaxViewDistanceScreenY

Screen Y of the line above which ground is further away than MaxViewDistanceFactor allows, or 0 when the cap does not reach into the viewport.

public double MaxViewDistanceScreenY { get; }

Property Value

double

Remarks

0 means "nothing to clamp": the whole viewport is within the cap, which is the case at every pitch up to 60 with the default factor and at every pitch at all when the view is flat. A caller that unprojects a screen position it did not get from a real on-screen pixel should push it down to this row first - see ClampToMaxViewDistance(ScreenPoint).

Screen

Viewport size in screen px.

public ScreenSize Screen { get; }

Property Value

ScreenSize

ScreenPixelScale

Device pixels per logical map pixel. One on fixed-resolution targets; greater than one on high-density screens.

public double ScreenPixelScale { get; }

Property Value

double

Remarks

Changes only how large a source tile appears. It never changes the source zoom, tile coordinate, decoded texture size or pixel ratio.

State

The view this transform was built from.

public MapViewState State { get; }

Property Value

MapViewState

TileSizePx

Tile edge length in px, 256 or 512. Sets the mapping from zoom to WorldSizePx, so a 512 px source at zoom z covers the same ground as a 256 px source at z+1.

public int TileSizePx { get; }

Property Value

int

WorldSizePx

Screen px spanned by the whole world at this zoom, that is TileSizePx times 2 to the power of the zoom. The scale factor between mercator and screen distances.

public double WorldSizePx { get; }

Property Value

double

Remarks

Uses the fractional zoom, so this is the continuous projection scale and not the integer level whose tiles are drawn.

Methods

AltitudeCoefficients()

The clip-space coefficients that let the vertex shader apply marker altitude itself, matching TryProjectWithAltitude(MercatorPoint, double, out ScreenPoint).

public (double y, double w) AltitudeCoefficients()

Returns

(double y, double w)

Two coefficients per unit of raw altitude in mercator units, with AltitudeScale and WorldSizePx already folded in, so the shader multiplies by the unscaled value. The shader adds y * altMercator to clip Y and w * altMercator to W.

Remarks

There is deliberately no X coefficient: altitude never moves a marker horizontally in clip space, only through the changed perspective divide.

AnchorRelativeX(double, double)

public double AnchorRelativeX(double mercX, double anchorX)

Parameters

mercX double
anchorX double

Returns

double

BuildRenderAffine()

Builds the 2x3 affine from absolute mercator to clip space. The pitch 0 fast path.

public RenderAffine BuildRenderAffine()

Returns

RenderAffine

An affine mapping normalized mercator directly to clip space, where X and Y run -1 to 1 and Y increases upward.

Remarks

Expresses the same mapping as LatLonToScreen(LatLon) in clip space, with one difference worth knowing: this applies no world wrap, whereas MercatorToScreen(MercatorPoint) folds X to the nearest world copy. The two therefore diverge for points more than half a world from the center. Takes absolute mercator, so unlike BuildRenderMatrixRows(MercatorPoint) it offers no protection against float32 precision loss and is unsuitable for handing to a shader at high zoom.

Exceptions

InvalidOperationException

The view has a non-zero pitch. Use BuildRenderMatrixRows(MercatorPoint) instead; despite the general name, this overload is the flat fast path only.

BuildRenderMatrixRows(MercatorPoint)

Builds the full perspective rows over anchor-relative mercator. The path that handles any pitch, and the one the shader uses.

public RenderMatrixRows BuildRenderMatrixRows(MercatorPoint anchor)

Parameters

anchor MercatorPoint

Origin the shader's coordinates are relative to, in normalized mercator, normally the corner of a tile near the view center. Vertex positions are supplied as offsets from it so they stay small enough to survive float32; see NeedsReanchor(MercatorPoint) for when to move it.

Returns

RenderMatrixRows

Three homogeneous rows mapping anchor-relative mercator to clip X, clip Y and W. The GPU divides by W, so W is a perspective term and is exactly 1 at pitch 0.

Remarks

Derived from the same scalar formulas as MercatorToScreen(MercatorPoint) and tested equal to it, so the CPU API and the shader cannot drift apart. Applies no world wrap: the caller supplies already-wrapped offsets.

ClampToMaxViewDistance(ScreenPoint)

Moves a screen position down to the cap line if it sits above it, so unprojecting it lands on ground the map actually draws.

public ScreenPoint ClampToMaxViewDistance(ScreenPoint s)

Parameters

s ScreenPoint

Screen position in px, origin top left.

Returns

ScreenPoint

s unchanged when the cap does not bind, which is every view at pitch 60 or below with the default factor.

Remarks

The guard the enumerated unprojection sites use. ScreenToMercator(ScreenPoint) deliberately stays unguarded - the footprint sweep needs its exact off-world values - so the clamp belongs at the call sites that unproject a position a user pointed at rather than one the sweep computed.

HasChanged(MapViewTransform, MapViewTransform)

Whether any field a projection depends on differs between two transforms: the five MapViewState fields, viewport size, AltitudeScale, TileSizePx and ScreenPixelScale.

public static bool HasChanged(MapViewTransform a, MapViewTransform b)

Parameters

a MapViewTransform
b MapViewTransform

Returns

bool

Remarks

The canonical dirty check for a per-frame projection cache keyed on "did the view move". AltitudeScale, TileSizePx and ScreenPixelScale are not part of State or Screen but all three feed TryProjectWithAltitude(MercatorPoint, double, out ScreenPoint) (hPx = altMercator * AltitudeScale * worldSizePx, while TileSizePx and ScreenPixelScale set worldSizePx itself) - a compare over only the five state fields plus screen size missed them, and since ScreenMapView.AltitudeScale is a live public field a consumer can change with no other view field moving, two independent per-tier caches (WorldMarkerManager's projection cache and MarkerHoverLabel's hover pick) each shipped that same gap and each needed the identical one-line fix. This is the one place that comparison lives now, so a future field that changes a projection only needs adding here for every cache keyed on view motion to pick it up. The terrain sampler is compared by reference, not value: slice 1 only ever swaps it at pipeline build, the one time it can change, so a reference compare is exact and needs no sampler-level equality contract.

LatLonToScreen(LatLon)

Projects a geographic coordinate to its position on screen.

public ScreenPoint LatLonToScreen(LatLon ll)

Parameters

ll LatLon

Coordinate in degrees WGS84.

Returns

ScreenPoint

Position in screen px, origin top left, Y increasing downward. May legitimately lie outside the viewport. Latitude is clamped into the Web Mercator range first, so the poles land on the map edge.

Remarks

Screen Y and mercator Y both increase downward, so the axes align at rotation 0. Longitude resolves to the nearest world copy, matching the wrapped draw. Carries the same unguarded horizon precondition as MercatorToScreen(MercatorPoint): at a non-zero pitch a coordinate far enough behind the camera returns a finite but mirrored point rather than failing.

MercatorToScreen(MercatorPoint)

Projects a point to screen px: on the ground plane when this transform has no terrain sampler, or at the sampled ground height when it does (see the terrain constructor overload).

public ScreenPoint MercatorToScreen(MercatorPoint m)

Parameters

m MercatorPoint

Position in normalized mercator. X is folded to the nearest world copy, so values outside [0,1) project as the equivalent wrapped position; Y is used as given, because mercator is not periodic in Y.

Returns

ScreenPoint

Position in screen px, origin top left, Y increasing downward.

Remarks

Pitch tips the top of the map away from the viewer, and the mapping reduces exactly to the 2D affine at pitch 0. Folding X to the nearest world copy matches how tiles and markers are actually drawn, and is the identity for anything within half a world of the center.

Precondition, unguarded: the point must be in front of the camera. The perspective divide is not checked, so at a non-zero pitch a point beyond the horizon produces a finite, plausible looking, mirrored result instead of an error. There is no Try variant of this method; TryProjectWithAltitude(MercatorPoint, double, out ScreenPoint) is the only projection here that reports the failure. That stays true with a terrain sampler set: a terrain point at or behind the camera projects the same finite, mirrored way rather than failing.

With a terrain sampler set this method's inverse, ScreenToMercator(ScreenPoint) (and ScreenToLatLon(ScreenPoint), TryGroundBounds(double, out MercatorBounds)), still solves against the flat ground plane, not the sampled terrain - a named gap, not an oversight, closed by terrain unprojection in a later slice.

NearEdgeTerrainMarginPx(double)

Extra screen px the swept rect's near (bottom) edge must reach past Height so that a ground point too close to appear on the flat plane, but tall enough that its true elevation projects it back inside the screen, still has its tile selected.

public double NearEdgeTerrainMarginPx(double maxElevationMeters)

Parameters

maxElevationMeters double

Worst-case ground height above the flat plane, in meters. Zero, negative or NaN return 0: no elevation, no margin, byte-identical to today.

Returns

double

Zero at pitch 0 (elevation never moves screen Y there) and zero whenever the margin computes negative (the flat near edge already covers the elevated point). Never negative.

Remarks

Closed form, not a solve over the footprint: inverts the same scalar projection MercatorToScreenTerrain(MercatorPoint) uses, asking "how far past the flat near edge would a ground point have to sit for its ELEVATED projection to land exactly on the near edge", then reports how far past the edge the FLAT projection of that same point sits. LansMap.Core.View.MapViewTransform.GroundDepthFloorMin floors the denominator, the same backstop NeedsReanchorCore(MercatorPoint, double, bool) uses for the same reason: without it a pitch approaching the horizon can drive the ratio arbitrarily large rather than merely large. This is a bound over one ground point (the geometric worst case at screen center), not a per-tile computation, and is meant to be recomputed once per sweep or clamp evaluation.

NeedsReanchor(MercatorPoint)

Whether the GPU anchor is far enough from the view center that narrowing to float32 could shift the rendered map by more than a quarter of a screen pixel, meaning the caller should re-anchor.

public bool NeedsReanchor(MercatorPoint anchor)

Parameters

anchor MercatorPoint

The anchor currently in use, in normalized mercator, as passed to BuildRenderMatrixRows(MercatorPoint).

Returns

bool

True when the anchor should be moved back toward the view center.

Remarks

The predicate is one-sided and conservative, so read it as "re-anchor no later than this", not as "the error is exactly at budget now". It is a function of zoom, screen diagonal and pitch, not of zoom alone, and it is a pure query about the anchor passed in: nothing is mutated and no re-anchoring happens here.

What the budget covers. The bounded quantity is the worst displacement, in screen px and in either screen axis, of any corner of any tile the footprint currently selects, once the nine matrix rows and the anchor-relative tile rect are narrowed to float32 and pushed through the shader's dot products and perspective divide. It is a bound over the whole footprint, not just the view center, and the budget is 0.25 screen px.

Strength of the guarantee. The supported claim is that the predicate permits no error above 0.25 px anywhere in the swept configurations, not that the error is provably bounded everywhere. The measurement is an external reference rather than the renderer checking itself: the test replays the real narrowing path in strict float32 against the same algebra in full double.

This overload models flat content only. Content raised off the ground plane loses precision faster than this and must ask the two-argument overload. Two layers are raised today - markers, and polylines, whose segments carry a per-endpoint altitude - and both pass their store's tallest value. Polygons carry no altitude at all, and the label layer re-derives its anchor from the view center every rebuild so it never accumulates an offset to judge; both say so at the call site.

NeedsReanchor(MercatorPoint, double)

The same question for content that is raised above the ground plane, such as markers with altitude.

public bool NeedsReanchor(MercatorPoint anchor, double maxAltitudeMercator)

Parameters

anchor MercatorPoint

The anchor currently in use, in normalized mercator, as passed to BuildRenderMatrixRows(MercatorPoint).

maxAltitudeMercator double

The largest raw altitude any instance drawn against this anchor carries, in normalized mercator units before AltitudeScale, which is the same unit MarkerInstanceData.AltitudeMercator stores. Zero, negative and NaN all mean flat, and a flat call is the exact expression the single-argument overload has always evaluated.

Returns

bool

True when the anchor should be moved back toward the view center.

Remarks

Height costs precision twice, and both halves are one-sided, so this overload can only ever fire earlier than the flat one, never later. First, the altitude reaches the shader as its own float32 value through its own float32 coefficients (AltitudeCoefficients()), so it rounds on top of everything the flat model already counted; that is an additive term, and its two coefficients are the shader's own, not new fitted numbers. Second, raising a point shortens its distance to the camera, and every error at that point is divided by that shorter distance, so the flat error is multiplied by 1/(1-f), where f is the height as a share of the shallowest camera distance anything drawn can have. Once that multiplier would pass 10 the answer is "re-anchor now" rather than an extrapolation.

The reach convention is the flat one: the bounded quantity is the displacement of drawn content within the footprint the view selects, not of an instance arbitrarily far outside it. A point the shader culls for being at or behind the camera is not covered, because it is not drawn, and neither is one whose projection lands more than a viewport outside the screen.

Strength of the guarantee, on the same terms as the flat half: the claim is "no measured error above 0.25 px across the swept configurations", not a proof.

In plain terms: ordinary markers a few meters up cost nothing until past z20, and it is aircraft-scale altitudes at high zoom that make the map re-anchor on every frame. For MarkerLayer and PolylineLayer that means a full instance-buffer re-upload per frame.

NeedsReanchorWrapSlack(MercatorPoint)

The wrap-slack variant of NeedsReanchor(MercatorPoint) for flat content, single-argument form. See NeedsReanchorWrapSlack(MercatorPoint, double) for the full reasoning; this is that overload with no altitude term, for callers (PolygonLayer) that carry none.

public bool NeedsReanchorWrapSlack(MercatorPoint anchor)

Parameters

anchor MercatorPoint

Returns

bool

NeedsReanchorWrapSlack(MercatorPoint, double)

The wrap-slack variant of NeedsReanchor(MercatorPoint, double), for the instanced overlay layers only (MarkerLayer, PolylineLayer, PolygonLayer).

public bool NeedsReanchorWrapSlack(MercatorPoint anchor, double maxAltitudeMercator)

Parameters

anchor MercatorPoint
maxAltitudeMercator double

Returns

bool

Remarks

This overload only changes the ANSWER inside that already- degenerate radius >= 0.5 regime: instead of firing unconditionally, it fires when Math.Abs(dx) >= WrapSlack (an eighth of the world), then falls through to the unchanged precision (errPx) evaluation below - it never returns false early. The radius < 0.5 case, and both single/dual-argument overloads, are byte-for-byte identical to today: this method is additive, not an edit to the fitted precision model (NarrowingFactorFlat/Pitch/Width, the footprint radii, and the altitude terms are untouched).

Precision margin in the gated regime: radius >= 0.5 forces worldSizePx <= 2 * diagPx * (1 + 2.2 sinP), which caps errPx at fire-suppressed offsets well under the 0.25 px budget - about 0.005 px at 1920x1080 z5 pitch 60, about 0.05 px at the worst swept corner (3840x1080 pitch 60 at the regime boundary), both at WrapSlack = 0.125. Pinned by a sweep assertion, not asserted from this comment alone.

Behavior change, named honestly: between fires, far-field markers near the world-wrap seam keep the world copy chosen at the last re-anchor for up to WrapSlack of world-X pan, instead of re-folding every frame. Both positions are true periodic copies of the same marker; the lag only affects views whose footprint already spans more than half the world (z <= 5-6 tilted, z <= 4 flat), in the far horizon band, since today's every-frame refresh cannot remove the seam from the footprint either (radius > 0.5 means it cannot) - it only re-picks copies continuously. This is a copy-choice policy at world-spanning zooms, not a precision regression.

ProjectWithAltitude(MercatorPoint, double)

Projects a point above the ground plane to screen px, for markers with altitude.

public ScreenPoint ProjectWithAltitude(MercatorPoint m, double altMercator)

Parameters

m MercatorPoint

Ground position in normalized mercator.

altMercator double

Height above the ground in normalized mercator units at the local latitude scale, not in meters. Convert with AltitudeToMercator(double, double). Scaled by AltitudeScale here.

Returns

ScreenPoint

Position in screen px, or a point whose components are both NaN when the projection fails.

Remarks

Prefer TryProjectWithAltitude(MercatorPoint, double, out ScreenPoint). This overload calls it and discards the result flag, so failure is reported only as a NaN sentinel that will propagate silently into any arithmetic downstream. At pitch 0 raising a marker leaves its screen Y unchanged and only shortens its distance to the camera, which is the correct top-down perspective.

ScreenToLatLon(ScreenPoint)

Unprojects a screen position to a geographic coordinate. The public boundary version of ScreenToMercator(ScreenPoint).

public LatLon ScreenToLatLon(ScreenPoint s)

Parameters

s ScreenPoint

Position in screen px, origin top left, Y increasing downward.

Returns

LatLon

Coordinate in degrees WGS84, always in range. Longitude wraps into the canonical world, so panning across the antimeridian keeps returning valid values; latitude clamps at the mercator limit of 85.05112878 degrees.

Remarks

Wrapping and clamping here is the documented boundary behaviour: this method never returns an out-of-range coordinate, which is why it and not ScreenToMercator(ScreenPoint) is the one on the public API. A position above the far edge of the drawn ground resolves to the coordinate at that edge rather than to a point mirrored through the horizon; see ClampToMaxViewDistance(ScreenPoint).

ScreenToMercator(ScreenPoint)

Unprojects a screen position to normalized mercator by intersecting the camera ray with the ground plane.

public MercatorPoint ScreenToMercator(ScreenPoint s)

Parameters

s ScreenPoint

Position in screen px, origin top left, Y increasing downward.

Returns

MercatorPoint

Normalized mercator, deliberately neither wrapped nor clamped, so X may leave [0,1) and Y may leave [0,1]. Footprint selection needs the exact off-world value; ScreenToLatLon(ScreenPoint) is the clamped boundary version.

Remarks

The denominator stays positive for every position actually inside the viewport, because pitch plus half the vertical field of view stays below 90 degrees and so the horizon is never on screen. It is not checked, though, and the parameter accepts any value: for a point far enough above the viewport at a high pitch the sign flips and the result is silently reflected through the horizon.

ScreenToMercatorAtAltitude(ScreenPoint, double)

Inverse of TryProjectWithAltitude(MercatorPoint, double, out ScreenPoint) at a fixed altitude: the ground position whose altitude-projected point lands on a given screen pixel. Used to drag a raised marker.

public MercatorPoint ScreenToMercatorAtAltitude(ScreenPoint s, double altMercator)

Parameters

s ScreenPoint

Position in screen px, origin top left, Y increasing downward.

altMercator double

Altitude to solve at, in normalized mercator units at the local latitude scale. Scaled by AltitudeScale.

Returns

MercatorPoint

Ground position in normalized mercator, neither wrapped nor clamped.

Remarks

Exactly inverts the forward projection in double, but is asymmetric with it in two ways. It has no depth guard, so for a screen position whose implied depth is at or behind the camera it returns a finite wrong answer where the forward method returns false. And the forward path folds X to the nearest world copy while this does not unwrap, so a round trip through both returns the nearest-copy longitude rather than the original beyond half a world.

Pass the screen point through ClampToMaxViewDistance(ScreenPoint) first unless it is already known to sit on drawn ground. This divides by the same quantity ScreenToMercator(ScreenPoint) does and is unguarded for the same reason, so a point above the far edge of the drawn ground returns a finite value mirrored through the horizon rather than failing. At the steepest tilt that band is a large share of the viewport, so it is easy to reach with a pointer.

TryGroundBounds(double, out MercatorBounds)

Conservative mercator bounds of the ground the viewport can see, expanded by marginPx screen pixels on every side: the axis-aligned rect over the four unprojected corners.

public bool TryGroundBounds(double marginPx, out MercatorBounds bounds)

Parameters

marginPx double
bounds MercatorBounds

Returns

bool

False when the expanded top edge reaches the horizon, where ScreenToMercator(ScreenPoint) silently reflects instead of diverging; callers must then treat the visible ground as unbounded. On-screen points can never refuse (pitch clamping keeps the horizon off screen), so only a margin large relative to the screen can.

Remarks

The ground projection of the screen rect is a homography image, a convex quad whose vertices are the unprojected corners, so the rect over the corners contains everything the padded viewport sees under any bearing and pitch. A superset, not a fit: at high pitch the quad is a thin trapezoid and the rect over it is much larger.

TryGroundBoundsOfRect(ScreenPoint, ScreenPoint, out MercatorBounds)

TryGroundBounds(double, out MercatorBounds) for an arbitrary screen rect - for example the square around a pick cursor - with the same guarantee and the same horizon refusal. min must be the top-left corner, max the bottom-right.

public bool TryGroundBoundsOfRect(ScreenPoint min, ScreenPoint max, out MercatorBounds bounds)

Parameters

min ScreenPoint
max ScreenPoint
bounds MercatorBounds

Returns

bool

TryProjectWithAltitude(MercatorPoint, double, out ScreenPoint)

Projects a point above the ground plane to screen px, reporting whether the point is in front of the camera.

public bool TryProjectWithAltitude(MercatorPoint m, double altMercator, out ScreenPoint screen)

Parameters

m MercatorPoint

Ground position in normalized mercator. X is folded to the nearest world copy, matching MercatorToScreen(MercatorPoint).

altMercator double

Height above the ground in normalized mercator units at the local latitude scale, not in meters. Scaled by AltitudeScale.

screen ScreenPoint

Position in screen px on success; both components NaN on failure.

Returns

bool

False when the point is at or behind the camera, which a large altitude at high zoom can easily reach, and false for a point further away than MaxViewDistanceFactor allows on a view steep enough for that cap to bind. Such points must neither render nor be pickable, so callers must check this rather than using the position regardless.

ViewDistanceAtScreenY(double)

Perspective depth of the ground at a screen row, in multiples of the camera distance: 1 at the view center, larger toward the top of a tilted screen.

public double ViewDistanceAtScreenY(double screenY)

Parameters

screenY double

Screen Y in px, origin top left. Values outside the viewport are accepted.

Returns

double

Positive infinity at or beyond the horizon row, where the ground plane and the camera ray no longer meet in front of the camera.

Remarks

Exactly the w the shader divides by, so a fog term keyed on this and one keyed on the interpolated clip w agree by construction rather than by matching two derivations.