Summary
On Android the camera pitch cannot exceed 60°, and MapLibreMap.setMaxPitchPreference() cannot lift it. The value is enforced in three separate places, two of which ignore the configured maximum entirely and use a hardcoded literal.
MapLibre GL JS allows up to 85° (maxPitch), so the same style renders a horizon on web and cannot on Android.
Use case
A marine navigation app. Tilting to the horizon is the difference between a chart read from above and the view over the bow — at 60° the horizon is still off-screen, so the "helm view" cannot be built on Android while it works on web from the same style.
Where it is enforced
Decompiled from org.maplibre.gl:android-sdk-opengl:13.2.0 (also present in 13.4.1 and 13.5.1):
1. Transform.setMaxPitch(double) — validates and silently returns:
if (value < 0.0 || value > 60.0) {
Logger.e("Mbgl-Transform", "Not setting maxPitchPreference, value is in unsupported range: %s", value);
return;
}
nativeMap.setMaxPitch(value);
This is reachable behaviour: setMaxPitchPreference(85) logs and does nothing. It does not throw, so callers get no signal beyond a log line.
2. MapGestureDetector$ShoveGestureListener — the tilt gesture, which never reads the configured maximum:
pitch = currentPitch - (0.1f * deltaPixels);
pitch = MathUtils.clamp(pitch, 0.0, 60.0); // literal, not maxPitch
transform.setPitch(pitch);
3. CameraPosition$Builder — clamps programmatic camera positions to 60 as well, so easeTo/animateCamera cannot reach a higher pitch either.
What I verified
To check whether the restriction is in the Java layer or the renderer, I bypassed (1) by reflection and called NativeMap.setMaxPitch(85.0) directly. That succeeded — no exception, nothing logged — but the gesture still clamped at 60, because (2) does not consult the map's maximum. So the renderer appears willing; the Java wrapper is what refuses.
MapLibreConstants.MAXIMUM_PITCH is 60.0f in 13.2.0, 13.4.1 and 13.5.1, and the docs state it: "The default and upper bound for maximum Pitch is 60."
Suggested change
- Raise
MapLibreConstants.MAXIMUM_PITCH to 85 to match MapLibre GL JS, or make the bound configurable.
- Have
ShoveGestureListener clamp to transform.getMaxPitch() rather than the literal, so setMaxPitchPreference governs the gesture as its name implies.
- Same for
CameraPosition.Builder.
(2) is arguably a bug independent of (1): a configured maximum that the gesture ignores is surprising regardless of what the ceiling is.
Environment
org.maplibre.gl:android-sdk-opengl 13.2.0 (also checked 13.4.1, 13.5.1)
- via
@maplibre/maplibre-react-native 11.3.6
- Android 16, Samsung SM-S918B
Happy to open a PR for (2) and (3) if that direction is welcome.
Summary
On Android the camera pitch cannot exceed 60°, and
MapLibreMap.setMaxPitchPreference()cannot lift it. The value is enforced in three separate places, two of which ignore the configured maximum entirely and use a hardcoded literal.MapLibre GL JS allows up to 85° (
maxPitch), so the same style renders a horizon on web and cannot on Android.Use case
A marine navigation app. Tilting to the horizon is the difference between a chart read from above and the view over the bow — at 60° the horizon is still off-screen, so the "helm view" cannot be built on Android while it works on web from the same style.
Where it is enforced
Decompiled from
org.maplibre.gl:android-sdk-opengl:13.2.0(also present in 13.4.1 and 13.5.1):1.
Transform.setMaxPitch(double)— validates and silently returns:This is reachable behaviour:
setMaxPitchPreference(85)logs and does nothing. It does not throw, so callers get no signal beyond a log line.2.
MapGestureDetector$ShoveGestureListener— the tilt gesture, which never reads the configured maximum:3.
CameraPosition$Builder— clamps programmatic camera positions to 60 as well, soeaseTo/animateCameracannot reach a higher pitch either.What I verified
To check whether the restriction is in the Java layer or the renderer, I bypassed (1) by reflection and called
NativeMap.setMaxPitch(85.0)directly. That succeeded — no exception, nothing logged — but the gesture still clamped at 60, because (2) does not consult the map's maximum. So the renderer appears willing; the Java wrapper is what refuses.MapLibreConstants.MAXIMUM_PITCHis60.0fin 13.2.0, 13.4.1 and 13.5.1, and the docs state it: "The default and upper bound for maximum Pitch is 60."Suggested change
MapLibreConstants.MAXIMUM_PITCHto 85 to match MapLibre GL JS, or make the bound configurable.ShoveGestureListenerclamp totransform.getMaxPitch()rather than the literal, sosetMaxPitchPreferencegoverns the gesture as its name implies.CameraPosition.Builder.(2) is arguably a bug independent of (1): a configured maximum that the gesture ignores is surprising regardless of what the ceiling is.
Environment
org.maplibre.gl:android-sdk-opengl13.2.0 (also checked 13.4.1, 13.5.1)@maplibre/maplibre-react-native11.3.6Happy to open a PR for (2) and (3) if that direction is welcome.