Skip to main content

Migrating from the CARTO Mobile SDK

Massif Maps is the CARTO Mobile SDK, forked and kept alive after CARTO stopped maintaining it. The 4.x concepts, class shapes and CartoCSS styles all still apply — what changed in 6.0 is the name, in every namespace, package and debug token.

Every rename below is a hard break except the CartoCSS ones, which keep parsing the old spelling and log a deprecation warning once per stylesheet.

Most apps are a find-and-replace

An Android app that only used com.carto.* classes migrates with one search-and-replace and a dependency line. The part with no automatic path is the CARTO online services, which the fork removed entirely — see What was removed.

Do this first

1 · Change the dependency.

// before
implementation 'com.carto:carto-mobile-sdk:4.4.+'
// after — JitPack overrides the declared groupId, so the coordinate is the GitHub path
implementation 'com.github.massif-maps:MassifMaps-android-aar:v6.0.0'

On iOS the CocoaPod is replaced by a Swift package: https://github.com/massif-maps/MassifMaps-ios-swift.

2 · Rename the imports.

# Android / Java / Kotlin
grep -rl 'com\.carto\.' src/ | xargs sed -i '' 's/com\.carto\./com.massifmaps./g'

# iOS / Obj-C / Swift — class prefix NT -> MSF
grep -rl 'NT[A-Z]' Sources/ | xargs sed -i '' -E 's/\bNT([A-Z][A-Za-z0-9]*)/MSF\1/g'

3 · Replace anything that talked to CARTO's servers — see What was removed.

4 · Styles need no change. nuti:: still parses. Rename it when convenient.

Bindings

PlatformBeforeAfter
Java / Kotlincom.carto.*com.massifmaps.*
Java (routing-lib)com.akylas.routing.*com.massifmaps.routing.*
Objective-C / SwiftNTMapView, NTVectorTileLayer, …MSFMapView, MSFVectorTileLayer, …
.NETCarto.Ui, Carto.Layers, …Massif.Ui, Massif.Layers, …
Native librarycarto_mobile_sdk (libcarto_mobile_sdk.so)massif (libmassif.so)
Gradle artifactcom.carto:carto-mobile-sdkcom.massifmaps:massif

On JitPack the resolvable coordinate is com.github.massif-maps:MassifMaps-android-aar:<tag> — JitPack overrides the declared groupId, so the package name and the coordinate differ by design.

C++ (only if you build from source or embed the core)

BeforeAfter
carto::massif::
_CARTO_*_SUPPORT build defines_MASSIF_*_SUPPORT
CARTO_VT_RENDER_STATS, CARTO_FRAME_PROFILERMASSIF_VT_RENDER_STATS, MASSIF_FRAME_PROFILER
mvt::NutiParameter*mvt::StyleParameter*
CartoGeocodingProxyMassifGeocodingProxy

CartoCSS — the old spelling still works

Both spellings parse. CartoCSSMapLoader logs one warning per deprecated token per stylesheet; the old spelling will be removed in a later release.

BeforeAfterWhat it means
[nuti::x][param::x]app-supplied runtime parameter, joining mapnik:: and view::
"nutiparameters" (project.json)"styleparameters"the block declaring those parameters
text-placement: nutibillboardbillboardfully screen-aligned
text-placement: nutibillboardlinebillboard-line-repeatscreen-aligned, repeated along a line
text-placement: nutipointflatflat in the placement plane, no camera facing
text-placement: nuticalloutcalloutscreen-aligned with a leader line

point (upright on the ground normal, swivelling to face the camera) is unchanged. See Live style parameters for what param:: can do now.

text-placement: line lies flat again (behaviour change in 6.0.1)

6.0.0 laid every line label out on the camera axes, so the text stayed upright at any tilt instead of lying on the map — the 5.x behaviour, and what the same text drawn with text-clip still did. line is flat again, and the upright run moved to billboard-line, which now follows the line instead of placing one upright label per text-spacing step.

You wantUse
the 5.x look, text in the ground plane (route distances, contour heights)text-placement: line — no change needed
the 6.0.0 look, text upright and readable at any tilttext-placement: billboard-line
one upright label per text-spacing step, not turning with the line (road shields)text-placement: billboard-line-repeat

See Labels for the three layouts.

shield-placement defaults to billboard-line-repeat (breaking change after 6.0.1)

billboard-line-repeat is new: it repeats along the line at shield-spacing like line does, but lays no glyph run on it, so each repeat stays an upright camera-facing box. That is what a road shield is, and until now no placement gave both — billboard-line turns the shield with the road, billboard draws one per line. It is now the default for shields, so a style that never set shield-placement changes twice:

FeatureBefore (point)Now
lineone shield, flat on the ground at the line's midpointone per shield-spacing step, facing the camera
point / polygonflat on the ground, foreshortening with the tiltfacing the camera

shield-placement: point restores the old default exactly. text-placement is unchanged — text still defaults to point.

text-clip / shield-clip no longer follow allow-overlap (behaviour change in 6.0.1)

clip used to default to whatever allow-overlap was, so text-allow-overlap: true alone moved the text off the label pipeline onto the tile geometry: no culler, no text-min-distance, no run following the line, one copy at the midpoint of every segment, and glyphs cut at the tile border. Both now default to false, which is their own declared default.

  • A style that wanted overlapping labels needs no change and gets correctly placed ones.
  • A style that relied on the clipped geometry path (usually for its cost, on very dense text) must now say text-clip: true explicitly.
  • marker-clip is unchanged — marker-allow-overlap: true still takes the geometry path.

Debug knobs

BeforeAfter
logcat tag carto-mobile-sdkmassif
adb shell setprop debug.carto.*debug.massif.*
demo app com.akylas.cartotestcom.massifmaps.MassifDemo
demo --es style nuti--es style project (--es demo nuti still accepted)
demo --es nutiInterval--es paramInterval

What was removed

The fork dropped everything that depended on CARTO's hosted services — they were switched off with the product. There is no renamed equivalent; bring your own source.

GoneReplace with
CartoOnlineVectorTileLayer and the hosted basemapany TileDataSource + a CartoCSS style — see the source reference
CARTO offline map packages and their PackageManager endpointsyour own package server, MBTiles or PMTiles
CARTO hosted routing and geocoding endpointsthe embedded Valhalla / SGRE engines, or any HTTP service
CARTO API keys and the mobile app registration flownothing — there is no key to set

Guides carried over from the CARTO documentation that still use these classes carry a warning banner at the top.

Breaking changes after 6.0.0

Map camera events carry a reason, and onMapStable fires once per movement

MapEventListener.onMapMoved and onMapStable take a MapMoveReason argument, so every app that overrides them must change the signature. The C++ compiles either way — a missed override just stops being called — so this is a silent break unless you build with an override check.

// before
public void onMapMoved() { ... }
public void onMapStable() { ... }

// after
public void onMapMoved(int reason) { ... }
public void onMapStable(int reason) {
if (reason == MapMoveReason.MAP_MOVE_REASON_GESTURE) { ... }
}
// before
- (void)onMapMoved;
// after
- (void)onMapMoved:(enum MSFMapMoveReason)reason;

The reason is MAP_MOVE_REASON_GESTURE, _ANIMATION or _API. On the facade, map.moved and map.stable now carry a MapMoveInfo payload with a reason property, where they carried none.

onMapStable also changes behaviour: it is edge-triggered. It fires once, at the end of a movement, and a touch that did not move the camera does not raise it at all. It used to be a poll — it fired for a tap on a motionless map, and could fire repeatedly while at rest. An app that kept its own "did the map actually move" flag can drop it, and one that relied on a tap raising stable must listen for the click instead.

See Map events for the full model.

3D buildings cast a contact shadow by default

building-ao-ground-radius used to default to 0, i.e. off, so nothing changed until a style asked. It now defaults to 4 metres with building-ao-intensity 0.2, and an app that upgrades gets the shadow without changing anything.

An extrusion with no contact shadow reads as pasted onto the map rather than standing on it, which is why it is on. It is not free — around 3 ms of GPU frame at a city camera where there is no drape to bake it into, and nothing where there is (see the performance log).

building-ao-ground-attenuation also changes meaning: it is now the exponent k of occlusion = (1 - d)^k, not mapbox's 1 - pow(1 - d, k). Higher keeps the shadow tighter to the wall; the old formula read far too strong across the whole band.

BeforeAfter
building-ao-ground-radius0 (off)4
building-ao-intensity0.50.2
building-ao-ground-attenuation0.69, mapbox's curve1.75, exponent of (1 - d)

To keep the old look, set building-ao-ground-radius: 0; in the style's Map block.

OpenGL ES 3.0 is required

The SDK no longer creates or accepts an OpenGL ES 2.0 context. No API changed — this is a device break, not a code one.

BeforeAfter
Android manifestuses-feature glEsVersion 0x000200000x00030000
Android contextES 3.0 if the device reported it, else ES 2.0ES 3.0, no fallback
iOS contextES 3.0, falling back to ES 2.0ES 3.0, no fallback
UWPEGL_CONTEXT_CLIENT_VERSION 23

If your app ships its own AndroidManifest.xml with a glEsVersion line, raise it to 0x00030000 — the merged manifest takes the highest value, so a stale 0x00020000 in your app is harmless, but leaving it there advertises support you no longer have.

Devices lost are pre-2013 GPUs: Mali-400, Adreno 200/305, Tegra 3, PowerVR SGX. At minSdk 21 and an iOS 13 floor — where every device is A7 or newer — that is a rounding error. On desktop the equivalent floor is D3D feature level 10_1 (Sandy Bridge, 2011), which Windows 11 already exceeds.

Shaders moved to GLSL ES 3.00 — and your shaders still work

The SDK's shaders now compile as #version 300 es. This is not a breaking change for application GLSL. If you pass a shader to SkyOptions.shaderSource, FogOptions.shaderSource, TerrainOptions.surfaceShaderSource, CustomRasterTileLayer.shaderSource or PostProcessEffect, keep writing it exactly as before — attribute, varying, texture2D and gl_FragColor all still work.

They keep working because the SDK prepends tangram's compatibility preamble, which maps the ESSL 1.00 spellings onto their 3.00 equivalents, including #define gl_FragColor. That is legal: ESSL reserves the GL_ prefix for macro names, and gl_FragColor is a built-in variable that ESSL 3.00 does not declare.

You may now also use ESSL 3.00 features directly (in/out, texture(), integer operations) — but do not declare your own #version line, and do not declare a fragment output named TANGRAM_FragColor, which the preamble already provides at location 0.

LightOptions.shadowDistance is a factor, not metres

BeforeAfter
lightOptions.shadowDistance = 20000 (metres)lightOptions.shadowDistance = 4.5 (multiples of the camera-to-focus distance)
style shadow-distance in metresstyle shadow-distance in the same multiples

Same unit as fogOptions.rangeStart / rangeEnd, and the same reason: the camera-to-focus distance is a function of the zoom alone, so one value holds from a city to a massif where a metric radius has to be retuned per zoom. The name did not change, so an app that set metres will not fail to compile — it will ask for thousands of times the view. Drop the setting and let the default (0, i.e. 4.5) apply, or scale from there; do not convert.

Fog moved to its own FogOptions

Fog was three fields on TerrainOptions and two on SkyOptions. It is not a terrain feature — it fogs a plain 2D map too — so it is now one object on Options, modelled on the Mapbox fog style property. See Sky, Sun & Shadows.

BeforeAfter
terrainOptions.fogColorfogOptions.color
terrainOptions.fogStartDistance (metres)fogOptions.rangeStart (multiples of the camera-to-focus distance)
terrainOptions.fogDistance (metres)fogOptions.rangeEnd (same unit)
skyOptions.fogBlend (degrees)fogOptions.horizonBlend (fraction of a quarter turn: degrees / 90)
style fog-start-distance / fog-distancestyle fog-range-start / fog-range-end

The range unit changed, so the numbers do not carry over. Metres had to be retuned for every zoom; the camera-to-focus distance is a function of the zoom alone, so one setting now holds everywhere. Start from the defaults (0.8 to 8) rather than converting.

New with it: enabled (a real switch — no more toggling fog by driving a distance to 0), highColor, spaceColor, starIntensity (Mapbox high-color / space-color / star-intensity), and shaderSource for a custom fog blend across map and sky.

The fog and the sky are one model now — and both are breaking

The fog had four implementations that agreed only by luck, which is why haze on the ground could meet clean sky along the skyline. The ground now takes the same angular term the sky takes (Mapbox's fog_horizon_blending), so the two are continuous by construction at any tilt and any zoom. Details in 08-lighting-sky-fog.md.

BeforeAfter
fogOptions.horizonAngleremoved — the shared horizon term is terrain-correct without it
fogOptions.horizonBlend (fraction of a quarter turn, linear cubed falloff)same range 0..1, now exp(-3 · (sin θ / blend)²) — retune, do not convert
distance ramp clamp((d − start)/(end − start))(1 − exp(−6t))³, Mapbox's own — the same range reads softer at the near end
fogOptions.verticalRangeStart / verticalRangeEnd (metres, Mapbox vertical-range): summits stand clear of a valley haze
style fog-vertical-range-start / fog-vertical-range-end

SkyOptions now defaults to a physical atmosphere. SkyOptions.type is SKY_TYPE_ATMOSPHERE: Rayleigh and Mie scattering, so the sky reddens at a low sun on its own. Set SKY_TYPE_GRADIENT to get the previous two-colour ramp back, unchanged. New with it: quality, atmosphereSunIntensity, atmosphereColor, haloColor, atmosphereLuminance, and the style properties sky-type, sky-atmosphere-sun-intensity, sky-atmosphere-color, sky-atmosphere-halo-color, sky-atmosphere-luminance.

Every custom shader has to be rewritten. FogOptions.shaderSource now replaces three entry points instead of one, and both SkyOptions.shaderSource and TerrainOptions.surfaceShaderSource changed with it:

// before
vec4 applyFog(vec4 color, float amount, float dist) {
return vec4(mix(color.rgb, uFogColor.rgb * color.a, amount), color.a);
}

// after - dir is the world-space view ray, heightM the fragment's altitude in metres.
// fogOpacity / fogHorizonBlend / fogVertical are supplied; use them or ignore them.
vec4 applyFog(vec4 color, vec3 dir, float dist, float heightM) {
float amount = fogOpacity(fogRange(dist)) * fogHorizonBlend(dir);
return vec4(mix(color.rgb, uFogColor.rgb * color.a, amount * (1.0 - fogVertical(heightM))), color.a);
}
vec4 skyFog(vec4 color, vec3 dir) {
return vec4(mix(color.rgb, uFogColor.rgb * color.a, uFogColor.a * fogHorizonBlend(dir)), color.a);
}
float fogLabelFade() { return 1.0; } // 0 hides a label the fog has swallowed
  • A custom sky shader must no longer call fogAmount(rayDir) or mix u_fogColor itself — the SDK applies skyFog once to whatever skyColor returns, so a custom fog shader reaches a custom sky too. u_fogBlend and u_fogHorizon are gone; the fog block's own uniforms are declared instead.
  • A custom terrain surface shader must no longer call fogAmount(v_dist) or read u_fogColor / u_fogRange, both removed. Return the unfogged colour; the SDK fogs it with everything else.

3D buildings are lit by the sun, whatever the terrain is doing

Tile extrusions had two lighting models, picked by whether terrain lighting was on. With it off, walls used Options.MainLightDirection / MainLightColor / AmbientLightColor and roofs were shaded by the view direction — so buildings changed shape when terrain lighting was toggled, and never answered to the hour of day. There is now one model, the same normalised Lambert the terrain surface uses, and it is always on.

BeforeAfter
options.mainLightDirection (3D buildings)lightOptions.sunAzimuth / sunAltitude
options.mainLightColor (3D buildings)lightOptions.sunColor
options.ambientLightColor (3D buildings)style building-ambient (see below)

The look changes on upgrade even if you set nothing. The old default light pointed down and slightly north-east (0.35, 0.35, -0.87); the sun defaults to azimuth 315 / altitude 45. Buildings are also tinted by sunColor now, which is what makes a facade go warm at dusk with the slope behind it.

The three Options properties still exist and still drive the normal-map illumination default and the spherical-mode 2D lighting; they simply no longer reach tile extrusions. Polygon3DLayer elements are unaffected — they have their own renderer and their own lighting.

Styles keep both overrides: building-light-intensity and building-ambient in the Map block still win over the sun, so a style can tune walls without moving the terrain sun.

Note the asymmetry: buildings take the sun's direction, colour and intensity, but keep their own ambient. lightOptions.ambientIntensity moves the ground, not the facades — at ambient 1 a shared value would flatten every building, and an extrusion with no side shading does not read as 3D.

3D buildings follow mapbox's fill-extrusion model

The normalised Lambert above was replaced by mapbox's model, so a facade shades the way the same building does in mapbox at the same hour. Details in internals/rendering/08-lighting-sky-fog.md.

The look changes on upgrade even if you set nothing, in three ways:

BeforeAfter
light sumambient + sun * (1 - ambient), in sRGBambient + sun, summed linear, returned to sRGB
building-ambient default0.350.5
building-light-intensity default1.00.5
building-vertical-gradient default0.650 — mapbox has no facade gradient

The two 0.5s sum to exactly 1 in direct sun, so a lit facade keeps its own colour at any hour. To keep something close to the old look, set building-vertical-gradient: 0.65 and building-light-intensity: 1 in the style's Map block; the linear sum has no opt-out.

Roofs now read lighter than walls in daylight and darker at dawn, as mapbox does — it falls out of N.L and is not tunable. A 60° floor on the building sun's altitude, present only in unreleased 6.1 builds, was removed: it was compensating for a shadow bug and it suppressed that inversion.

Buildings also gain rounded edges (building-edge-radius, metres, 0 = off as before — it rounds both the roof edge and the vertical corner where two walls meet), a building-roof-shade knob, and pitched roofs from OSM roof:shape / roof:height where the tiles carry them. All default to the previous flat-capped, sharp-edged geometry.

Java enums are int constants (Android only)

The 31 generated Java enum classes are now classes of int constants, annotated with @IntDef — the shape Android's own APIs use. iOS and .NET are unchanged: Objective-C already emitted typedef NS_ENUM, and C# enums cost nothing to wrap.

// before // after
public enum PanningMode { public final class PanningMode {
PANNING_MODE_FREE, public final static int PANNING_MODE_FREE = 0;
PANNING_MODE_STICKY, public final static int PANNING_MODE_STICKY = 1;
... ...
} @IntDef({ ... }) public @interface Value {}
}

Signatures carry the annotation, so Android Studio still autocompletes only the valid constants and lint flags a wrong int:

public void setPanningMode(@PanningMode.Value int panningMode);

Most call sites do not change. The class name and the constant names are identical, so options.setPanningMode(PanningMode.PANNING_MODE_STICKY) compiles as before. What breaks:

BeforeAfter
holding onePanningMode m = options.getPanningMode();int m = options.getPanningMode();
parsing a namePanningMode.valueOf(name)your own switch on the string
numeric valuem.swigValue()m is the value
from a numberPanningMode.swigToEnum(i)i is the constant
iteratingPanningMode.values()no equivalent — list the constants you need
switchcase PANNING_MODE_FREE:unchanged

switch keeps working because the constants are now compile-time literals rather than a JNI call per constant at class load — which is also what lets them be @IntDef members.

The Android artifact gains one dependency, androidx.annotation, for the annotation itself.

The facade sugar has its own Position, Bounds, ScreenPoint, ScreenRect

Facade sugar only — MapPos, MapBounds, ScreenPos and ScreenBounds are untouched and stay with the object API.

WasNow
com.massifmaps.core.MapPoscom.massifmaps.api.Position (lng, lat, alt)
com.massifmaps.core.MapBoundscom.massifmaps.api.Bounds (min, max)
com.massifmaps.core.ScreenPoscom.massifmaps.api.ScreenPoint (x, y)
com.massifmaps.core.ScreenBoundscom.massifmaps.api.ScreenRect (min, max)
MSFMapPos / MSFMapBounds / MSFScreenPos / MSFScreenBoundsMSFPosition / MSFBounds / MSFScreenPoint / MSFScreenRect
map.camera().moveTo(new MapPos(5.7245, 45.1885), 13.5f); // was
map.camera().moveTo(new Position(5.7245, 45.1885), 13.5f); // now

Fields, not getters: pos.lng rather than pos.getX(). MSFPosition uses properties (pos.lng). Longitude is still FIRST, as MapPos.getX() was.

Also changed, on the camera:

  • MapCamera.fitBounds(bounds) is now fitBounds(bounds, width, height); the ObjC fitBounds:screenBounds:integerZoom: is fitBounds:screenRect:integerZoom:. The camera no longer holds the view, so it cannot measure it for you. An overload taking resetRotation and resetTilt is new, as is camera().climb(height) for an arched flight.
  • MassifMap.screenToMap returns a Position and mapToScreen a ScreenPoint.
  • New: MapCamera.progress() / MSFMapCamera.progress, 0 to 1 through the current flight.

Why: a MapPos is a SWIG proxy over a C++ object, so every position a click handler reads costs a JNI allocation and a finalizer to carry two doubles. These are plain objects, and they are what lets the sugar name no SWIG type at all.

A facade position is WGS84 by default

Facade API only — the object API is untouched, and MapPos still means whatever its owner says.

A position read through the facade with no projection named used to come back in the object's own projection (EPSG:3857 in practice). It now comes back in EPSG:4326 — degrees:

MassifApi.getPos(payload, "featurePos"); // was [641267, 5660048], now [5.7606, 45.2442]
MassifApi.getPos(payload, "featurePos", "EPSG:3857"); // the old behaviour, asked for by name

This is the change most likely to pass a compiler and fail at runtime, so check every facade getPos / mm_get_string / mm_get_position call that does not name a projection. A per-read name still wins, and a per-subscription default still applies to the reads inside its handler; only "nobody said" changed.

Writes moved with it — setString on a position property (Options.panBounds, GeocodingRequest.location) is now taken as WGS84 too, so a value read and written back is unchanged. That is the whole reason both sides moved at once.

Unchanged: a Mercator conversion that produces a non-finite number (the poles) still fails with RESULT_UNSUPPORTED_TYPE rather than emitting JSON that will not parse, and an object with no known projection is still left alone rather than guessed at.

adopt and the event bridges moved to MassifInterop

Facade API only, and a straight rename — same arguments, same behaviour:

WasNow
MassifApi.adopt(kind, id, …)MassifInterop.adopt(kind, id, …)
MassifApi.getSource / getLayerMassifInterop.getSource / getLayer
MassifApi.createEventBridgeMassifInterop.createEventBridge
MassifApi.createVectorTileEventBridgeMassifInterop.createVectorTileEventBridge
MassifApi.createVectorElementEventBridgeMassifInterop.createVectorElementEventBridge

On iOS the class is MSFMassifInterop; MassifMaps.h already imports it.

Those are the only facade methods that name an SDK class in their signature, so they are the only ones a hand-written binding cannot carry. Splitting them out is what makes "MassifApi is SWIG-free" a checkable property rather than an intention — see api-facade.md. The typed sugar (Massif, MassifMap, MassifLayer, MSFMassif…) is unaffected; it already calls this for you.

TileDataSource.encoding became a general meta data map

Applies to 6.1.0 and later. In 6.0.0 and 6.0.1, setEncoding is the only spelling — setMetaDataElement does not exist there. 6.0.2 backports getMetaDataElement / setMetaDataElement / getContainerMetaData, alongside setEncoding, but not the map accessors or the per-tile resolution below.

setEncoding / getEncoding were a DEM-only setting on the base class of every data source, and the name already collided: getMetaData("encoding") meant the tile format (mvt / mlt) on the same object. They are replaced by the meta data bag VectorElement and Layer already use, and the DEM key is renamed:

WasNow
source.setEncoding("terrarium")source.setMetaDataElement("dem_encoding", Variant("terrarium"))
source.getEncoding()source.getMetaDataElement("dem_encoding") (a Variant)
source.getMetaData(key)Stringsource.getContainerMetaData(key)String
source.getMetaData() / setMetaData(map) — the whole map, StringVariant
MBTilesTileDataSource.getMetaData()getContainerMetaData()

getMetaDataElement falls back to getContainerMetaData, so a tileset that declares dem_encoding in its own MBTiles/PMTiles metadata needs no application code at all.

The decoder is now resolved per TILE, from the map that tile carries. Two DEM sources of different encodings can therefore sit behind one OrderedTileDataSource — see 3D terrain. Two consequences worth knowing:

  • ContourTileDataSource's default changed from Terrarium to MapBox, which is what the terrain, the hillshade and the composite layer already defaulted to. A Terrarium source that relied on the contour default must now declare dem_encoding.
  • The PersistentCacheTileDataSource schema gained a metaData column. An existing cache database is dropped and rebuilt on first open, as with any past schema change.

TileData::getMetadata / setMetadata were renamed to getMetaDataElement / setMetaDataElement too; they were never exposed to bindings, so only C++ embedders see it.

Deliberately NOT renamed

These name data or upstream work, not this SDK:

  • On-disk formats. NUTi is the 4-byte magic of the compressed bitmap format; nutikeysha1 is a row in the metadata table of downloaded offline packages; .nutigraph / .nutigeodb are package file extensions; __Nuti_pkgmgr_ is a local filename prefix. Renaming any of them would orphan data users already have on disk.
  • cartodb_id, an MVT feature field.
  • CartoCSS — the style language, MapBox's and CARTO's, which this SDK implements rather than owns.
  • The CartoDB copyright headers and LICENSE attribution.

Default option values (2026-08-23)

The SDK now ships the values every bench and every example screenshot in this repo was actually made with, instead of values nothing was tuned at. They live in scripts/android-dev/.../demo/DemoConfig.java, which is where the tuning was done.

An app that already sets one of these explicitly is unaffected. A map that took the defaults will look and perform differently — mostly better, but the terrain ones change framing enough to invalidate a stored camera.

OptionWasNowWhy
Options.zoomGesturesoffondouble-tap, two-finger tap and double-tap-drag zoom. Every other map SDK does these; an app that had to ask just looked broken
Options.tileThreadPoolSize12tangram's numTileWorkers. One made tiles arrive late enough to be seen arriving
Options.tileLODFactor1.00.5half a nominal tile of screen area per level
TerrainOptions.meshResolution3264tangram's value. 32 leaves draped content visibly floating; 128 cost 8.5 fps against 15.2
TerrainOptions.cameraClearance200 m60 m200 stops the camera short of the surface, so a close approach swings into the nearest hillside
TerrainOptions.billboardOcclusionTolerance0.020a label goes out when its anchor goes behind the relief
LightOptions.ambientIntensity0.351.0both of these only apply once terrain lighting is on
LightOptions.shadowStrength00.3
LightOptions.shadowBias0.251.00.25 leaves acne on a lit slope at 3 cascades
HillshadeRasterTileLayer.heightScale1.00.05at 1.0 real DEM relief saturates and the shading reads as a stencil
HillshadeRasterTileLayer.hillshadeMethodSTANDARDIGORkeeps slopes readable under imagery
HillshadeRasterTileLayer.illuminationMapRotationEnabledtruefalseturning the map should not relight the terrain
ContourTileDataSource.seamlessEdgesoffonwithout it a traced line stops dead at every tile border
ContourTileDataSource.minVisibleZoom125the interval ladder already coarsens a regional view
ContourTileDataSource.simplifyTolerance1.01.5

Not moved, and why. The demo's sun position (azimuth 355°, altitude 9°) and hillshade illumination azimuth are a raking test light for judging relief, not a default any app wants; the same goes for its camera, its tile URLs and its per-source cache sizes.

TerrainOptions.maxTileZoomCoarsening was raised to the demo's 8 and put back to 3. It only pays for itself next to the demo's fixed 170 km viewDistance, where what it coarsens is the far horizon. On the default view distance it coarsens tiles that are still large on screen: measured on the iOS terrain example, a blurred band with a hard tile edge down the middle of the view. The two are a pair — raise both or neither.