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.
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.
Zoom or altitude
Target or eye
How far it tilts
val camera = MapCameraPosition(
position = geoPoint,
zoom = 18.0,
tilt = 70.0,
)
moveCameraTo(camera)02 · The unified camera model
A camera is one MapCameraPosition. There is no per-provider type: build the value and pass it to moveCameraTo.
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.
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.
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.
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.
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.
// −60〜60 のスライダーをそのまま渡すだけ。負の値でも同じ呼び出し currentPosition = currentPosition.copy(tilt = tilt) mapViewState.value?.moveCameraTo(currentPosition)
func setTilt(_ angle: Double, state: any MapViewStateProtocol) {
currentPosition = currentPosition.copy(tilt: angle)
state.moveCameraTo(cameraPosition: currentPosition)
}<SliderControl
label="Tilt" value={tilt} min={-60} max={60} step={1}
onChange={value => {
setTilt(value);
const next = cameraPositionRef.current.copy({ tilt: value });
cameraPositionRef.current = next;
mapViewState.moveCameraTo(next, 400);
}}
/>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.
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)
}if let region = cameraPosition.visibleRegion {
let bounds = region.bounds
let corners = [region.nearLeft, region.nearRight, region.farLeft, region.farRight]
}<MapViewContainer initialCamera={INIT_CAMERA} onCameraMove={setCameraPosition}>
{/* cameraPosition.visibleRegion の中身を並べて表示する */}
</MapViewContainer>
const visibleRegion = cameraPosition?.visibleRegion ?? null;
visibleRegion?.nearLeft?.toUrlValue(5);
visibleRegion?.farRight?.toUrlValue(5);