Docs / Map view / Camera

Camera — the hardest part of the abstraction

The camera is both the hardest and the most valuable thing MapConductor unifies. One model — position, zoom, bearing, tilt, visibleRegion — carries the same meaning across more than ten map engines. Hand the same MapCameraPosition to any provider and you get the same view.

POSITION
lat / lng
ZOOM
0 – 22
BEARING
0 – 360°
TILT
−60 – 60°

01 · Why unifying it matters

Every engine has its own camera model: some speak in zoom, some in camera altitude, some orbit a ground target, some move the eye itself. Use the native SDKs directly and swapping providers gives you a different view from identical numbers — and a rewrite of your screen logic.

MapConductor confines those differences to the adapter layer and exposes a single camera model. A camera position becomes a value you can carry between providers. The Camera Sync page in the sample apps pushes one position into every provider at once, so you can see them line up on the same place, scale and tilt.

DIFFERENCE 01

Zoom or altitude

2D tile engines express scale as a zoom level; 3D engines as camera altitude or range. Different units, different baselines.
DIFFERENCE 02

Target or eye

Some engines orbit a point on the ground; others are defined by the eye position and where it looks.
DIFFERENCE 03

How far it tilts

Maximum pitch differs per engine — and not one of them can pitch upward at all.
Figure · how one moveCameraTo becomes the same view on every engine
val camera = MapCameraPosition(
    position = geoPoint,
    zoom = 18.0,
    tilt = 70.0,
)
moveCameraTo(camera)
MapConductor
Unified camera model
position / zoom · bearing / tilt
Per-vendor
map SDK adapter
Per-vendor
map SDK adapter
Adapters translate into each native camera representation. Zoom-based, altitude-based and no-upward-pitch differences are absorbed here.
Zoom-based engines (Google Maps · Mapbox · MapLibre …)
Altitude-based engines (ArcGIS · Cesium · MapKit …)
One entry point; different native expressions on the way out — but the same place, scale and tilt on screen.
Sample video · Camera Sync (one camera position across every provider)

02 · The unified camera model

A camera is one MapCameraPosition. There is no per-provider type: build the value and pass it to moveCameraTo.

Property
Description
position
The ground point at the centre of the view.
zoom
Scale, normalised across providers against Google Maps zoom levels.
bearing
Heading, in degrees clockwise from true north.
tilt
Tilt: 0 looks straight down, positive leans forward, negative looks above the horizon.
paddings
Insets covered by your UI. Centring and fitting account for them.
visibleRegion
The ground actually on screen — a bounding box plus the four screen corners.

copy() replaces single fields, and comparison uses tolerances of 0.01 on zoom and tilt so that tiny drift reported by an engine is not mistaken for movement.

Platform

03 · Zoom level and altitude as one concept

Internally zoom is the same thing as camera height. A converter in core maps 2D zoom levels to and from 3D altitude and range, so every provider value is normalised to one shared zoom scale — the Google Maps one — before it reaches your code.

That baseline was not derived on paper. A calibration tool in the sample apps measures the area each engine actually shows at a given zoom, and the reference altitude was tuned until the areas matched. Zoom 14 therefore covers the same ground everywhere.

Figure · zoom levels and the scale they show (roughly how many km fit across the screen)
0
≈ 40,000 km
Whole globe
The entire world in one view — continent outlines only.
3
≈ 5,000 km
Continent
Continents and oceans; borders carry no meaning yet.
6
≈ 600 km
Country
A whole country and where its cities sit.
10
≈ 40 km
Metro area
Built-up areas and arterial roads — a common initial view.
14
≈ 2.5 km
District
The street grid is readable; right for a list of store pins.
17
≈ 300 m
Block
Intersections and building footprints — walking navigation.
20
≈ 40 m
Building
Entrances and pinpoint positions.
Each step doubles the scale. Figures are for the equator at 256 px per screen width; actual ground distance shifts with latitude.
LATITUDE

The same zoom needs a different height by latitude

Web Mercator magnifies the map towards the poles, so the camera altitude for one zoom level differs several-fold between the equator and high latitudes. The zoom ⇄ altitude conversion accounts for it.

ZOOM 10 · near the equator
≈ 5,200 m
ZOOM 10 · around 70°N
≈ 1,700 m
NATIVE
Aligning native values to this scale
±0.0
Google Maps · Mapbox 2D — Taken as the reference. The unified zoom scale follows this family.
+1.0
MapLibre · MapTiler · TomTom — Native zoom sits one step lower, so a +1.0 offset is applied.
m
MapKit · ArcGIS · Cesium · Google 3D — Converted between zoom and altitude/range, accounting for latitude and tilt.
ZOOM RANGE
0.0 – 22.0
ZOOM FACTOR
2.0
ZOOM 0 ALTITUDE
171,319,879 m
MAPLIBRE OFFSET
+1.0

The only constants are the doubling-per-step relation and the altitude at zoom 0. Latitude is part of the conversion, so scale does not drift between Tokyo and Honolulu.

04 · Negative tilt, emulated

Tilt runs from −60 to 60 degrees. Positive is the familiar oblique view; negative looks above the horizon. No map engine can express that directly — every SDK accepts a pitch of zero or more only.

So MapConductor keeps the eye position and direction and rewrites the request as a positive pitch. From your code negative tilt just works, and reading the camera back gives you the negative value again.

tilt = 0
A plan view straight down: the ground rectangle is the screen rectangle.
tilt > 0
Leaning forward: the ground becomes a trapezoid, narrow near, wide far.
tilt < 0
Looking above the horizon — a view no engine offers natively.
How the emulation works
01
The eye position and height stay put; only the ground target moves forward along the bearing, by an amount derived from altitude and tilt angle.
02
The engine is then given abs(tilt) as a downward pitch. Because the eye and its direction match, the picture equals the upward view. MapLibre-family engines get fixed correction factors on the shift and zoom so the framing matches too.
03
Reading the camera back runs the same maths in reverse, undoing the forward shift and zoom offset to return the logical negative tilt and the original centre. Round trips are stable.
Negative tilt support
Android · all providers
OK
iOS · all providers
OK
React · all but Google Maps 2D
OK
React · Google Maps 2D
N/A

The one exception is Google Maps 2D on React — the raster JavaScript API map. Its tilt depends on browser and tile conditions, so the altitude and orientation the emulation relies on cannot be controlled. Google Maps 3D (Map3DElement) does support negative tilt.

TiltMapPageViewModel.kt
// −60〜60 のスライダーをそのまま渡すだけ。負の値でも同じ呼び出し
currentPosition = currentPosition.copy(tilt = tilt)
mapViewState.value?.moveCameraTo(currentPosition)
Sample video · Tilt (sweeping −60 to 60)

05 · VisibleRegion — what is really on screen

Tilt the camera and the screen rectangle becomes a trapezoid on the ground. A south-west / north-east box cannot express how much narrower the near edge is. VisibleRegion carries that box plus the ground coordinates of the four screen corners.

Near is the bottom edge of the screen, closest to the camera; far is the top edge. Left and right are screen-relative, so rotating with bearing does not change their meaning. With all four points you can scope data queries and attribution correctly even on a tilted map.

farLeft
farRight
nearLeft
nearRight
screen rectangle → trapezoid on the ground
The far edge is wider, the near edge narrower. Bounds is the box around this trapezoid.
bounds
The south-west / north-east box around the trapezoid, for coarse checks and existing APIs.
nearLeft / nearRight
Ground coordinates of the bottom screen corners — the near side.
farLeft / farRight
Ground coordinates of the top screen corners — the far side, spreading wider as you tilt.
Used for
Fetching data for the visible area, fitBounds, and switching tile attribution rules.
VisibleRegionViewModel.kt
mapViewState.cameraPosition.visibleRegion?.let { region ->
    val sw = region.bounds.southWest
    val ne = region.bounds.northEast
    // 傾いた画面の四隅。台形として扱える
    val corners = listOf(region.nearLeft, region.nearRight, region.farLeft, region.farRight)
}

Related pages