Class MapViewTransform
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
-
objectMapViewTransform
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
stateMapViewStateCenter, zoom, bearing and pitch of the view.
screenScreenSizeViewport size in screen px.
tileSizePxintTile edge length in px; must be 256 or 512.
Exceptions
- ArgumentOutOfRangeException
tileSizePxis 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
stateMapViewStateCenter, zoom, bearing and pitch of the view.
screenScreenSizeViewport size in screen px.
tileSizePxintTile edge length in px; must be 256 or 512.
altitudeScaledoubleUnitless multiplier on marker altitudes, 1 for true scale. Not validated.
Exceptions
- ArgumentOutOfRangeException
tileSizePxis 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
stateMapViewStateCenter, zoom, bearing and pitch of the view.
screenScreenSizeViewport size in screen px.
tileSizePxintTile edge length in px; must be 256 or 512.
altitudeScaledoubleUnitless multiplier on marker altitudes and terrain height alike, 1 for true scale. Not validated.
terrainIElevationSamplerGround height source for MercatorToScreen(MercatorPoint) and LatLonToScreen(LatLon). Passing null is equivalent to the flat constructor overloads.
Exceptions
- ArgumentOutOfRangeException
tileSizePxis 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
stateMapViewStateCenter, zoom, bearing and pitch of the view.
screenScreenSizeViewport size in screen px.
tileSizePxintTile edge length in px; must be 256 or 512.
altitudeScaledoubleUnitless multiplier on marker altitudes and terrain height alike, 1 for true scale. Not validated.
terrainIElevationSamplerGround height source for the forward projections. Null is equivalent to the flat overloads.
maxViewDistanceFactordoubleHow far to draw ground, in multiples of the camera distance. See MaxViewDistanceFactor; clamped, never rejected.
Exceptions
- ArgumentOutOfRangeException
tileSizePxis 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
stateMapViewStateCenter, zoom, bearing and pitch of the view.
screenScreenSizeViewport size in device pixels.
tileSizePxintSource tile-scheme edge, 256 or 512.
altitudeScaledoubleUnitless marker and terrain altitude multiplier.
terrainIElevationSamplerGround-height source; null keeps the map flat.
maxViewDistanceFactordoubleMaximum ground reach in camera-distance multiples.
screenPixelScaledoublePositive device pixels per logical map pixel.
maxTerrainElevationMetersdoubleSee 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
stateMapViewStateCenter, zoom, bearing and pitch of the view.
screenScreenSizeViewport size in screen px.
tileSizePxintTile edge length in px; must be 256 or 512.
altitudeScaledoubleUnitless multiplier on marker altitudes, 1 for true scale. Not validated.
maxViewDistanceFactordoubleHow far to draw ground, in multiples of the camera distance. See MaxViewDistanceFactor; clamped, never rejected.
Exceptions
- ArgumentOutOfRangeException
tileSizePxis 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
stateMapViewStateCenter, zoom, bearing and pitch of the view.
screenScreenSizeViewport size in device pixels.
tileSizePxintSource tile-scheme edge, 256 or 512.
altitudeScaledoubleUnitless marker and terrain altitude multiplier.
maxViewDistanceFactordoubleMaximum ground reach in camera-distance multiples.
screenPixelScaledoublePositive device pixels per logical map pixel.
Exceptions
- ArgumentOutOfRangeException
tileSizePxis neither 256 nor 512, orscreenPixelScaleis 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
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
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
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
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
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
MaxViewDistanceFactor
How far the map draws ground, in multiples of the camera distance.
public double MaxViewDistanceFactor { get; }
Property Value
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
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
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
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
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
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
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 * altMercatorto clip Y andw * altMercatorto 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
Returns
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
anchorMercatorPointOrigin 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
sScreenPointScreen position in px, origin top left.
Returns
- ScreenPoint
sunchanged 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
Returns
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
llLatLonCoordinate 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
mMercatorPointPosition 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
maxElevationMetersdoubleWorst-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
anchorMercatorPointThe 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
anchorMercatorPointThe anchor currently in use, in normalized mercator, as passed to BuildRenderMatrixRows(MercatorPoint).
maxAltitudeMercatordoubleThe largest raw altitude any instance drawn against this anchor carries, in normalized mercator units before AltitudeScale, which is the same unit
MarkerInstanceData.AltitudeMercatorstores. 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
anchorMercatorPoint
Returns
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
anchorMercatorPointmaxAltitudeMercatordouble
Returns
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
mMercatorPointGround position in normalized mercator.
altMercatordoubleHeight 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
sScreenPointPosition 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
sScreenPointPosition 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
sScreenPointPosition in screen px, origin top left, Y increasing downward.
altMercatordoubleAltitude 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
marginPxdoubleboundsMercatorBounds
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
minScreenPointmaxScreenPointboundsMercatorBounds
Returns
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
mMercatorPointGround position in normalized mercator. X is folded to the nearest world copy, matching MercatorToScreen(MercatorPoint).
altMercatordoubleHeight above the ground in normalized mercator units at the local latitude scale, not in meters. Scaled by AltitudeScale.
screenScreenPointPosition 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
screenYdoubleScreen 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.