The surface API
The SDK has two public faces. The object API is the classic one: a class per capability, a setter per option, and a per-language binding generated by SWIG — that is what the Javadoc and Jazzy reference documents.
The surface API is the one everything new is built on. An app names objects by id, describes them with JSON specs, and reads or writes anything through a dotted path. It is the same shape in every language, so an example reads the same in Kotlin, Swift and TypeScript, and it is what the NativeScript plugin, the C ABI and the coming React Native binding all sit on.
This page is the concept. Every spec type, property, method and event is in the API reference, generated from the SDK build. The design rationale is in the facade internals.
Five things, and that is the whole API
create(kind, id, spec) | build an object and register it under an id |
destroy(kind, id) | drop it |
set / get | write or read any property, by dotted path |
call(method, args) | run a method — route, search, fit bounds |
on(event) | subscribe |
Adding a feature to the SDK never adds a method here. It adds a row in a generated table, which is why the reference can be generated and cannot drift.
A map, from nothing
- Java / Kotlin
- Swift
- TypeScript (NativeScript)
- C
MassifMap map = MassifMap.attach(mapView);
map.addLayer("basemap", Spec.of("raster")
.set("source", Spec.of("http")
.set("url", "https://tile.openstreetmap.org/{z}/{x}/{y}.png")
.set("maxZoom", 19)));
map.camera().moveTo(new Position(6.8652, 45.8326), 11);
let map = MassifMap.attach(mapView)
map.addLayer("basemap", Spec("raster")
.set("source", Spec("http")
.set("url", "https://tile.openstreetmap.org/{z}/{x}/{y}.png")
.set("maxZoom", 19)))
map.camera().moveTo(Position(6.8652, 45.8326), zoom: 11)
const map = host.map;
map.addLayer('basemap', {
type: 'raster',
source: { type: 'http', url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', maxZoom: 19 }
});
map.camera().moveTo([6.8652, 45.8326], { zoom: 11 });
mm_handle layer;
mm_create(ctx, "layer", "basemap",
"{\"type\":\"raster\",\"source\":"
"{\"type\":\"http\",\"url\":\"https://tile.openstreetmap.org/{z}/{x}/{y}.png\","
"\"maxZoom\":19}}", &layer);
Positions are longitude first, matching GeoJSON and the wire format.
Kinds and ids
Every object belongs to a kind — the namespace its id lives in, and the set of spec types it can be built from. There are thirteen, one page each in the reference:
| Kind | What it holds |
|---|---|
layer | what is drawn |
source | where tiles come from |
style / styleset / assets | the decoder, its CartoCSS, and the package it is read from |
options | fog, sky, light, terrain |
element / elementstyle | markers, popups, lines, and how they look |
geometry / feature | shapes and shapes-with-properties |
search / routing / geocoding | services and their requests |
data | raw bytes |
An id is the app's own name — "basemap", "routes", "pin" — and is how one object reaches
another without holding a reference to it:
{"type": "vector", "source": "osm", "style": "outdoor"}
A string where a spec is expected is an id lookup. An object there is an anonymous child
built on the spot. Both are checked against the class the key needs, so a style id naming a
source is refused rather than cast.
A spec is a constructor plus properties
A factory only handles what the C++ constructor takes. Every other key is applied afterwards through the property table — so an option added to a class is usable here on the next build, with no factory change:
{"type": "persistent-cache",
"databasePath": "/data/.../osm.db",
"capacity": 268435456,
"source": {"type": "http", "url": "https://…/{z}/{x}/{y}.pbf", "maxZoom": 14}}
capacity is not a constructor argument. It reaches setCapacity through the table, the same way
opacity on a layer or rangeStart on the fog does.
Dotted paths
Anything readable or writable is one path, and the path traverses OBJECT properties:
- Java / Kotlin
- Swift
- TypeScript (NativeScript)
map.set("fog.rangeStart", 1.2);
map.set("terrain.exaggeration", 1.4);
map.fog().apply(Map.of("enabled", true, "rangeEnd", 4.0)); // one crossing, not three
double tilt = map.getDouble("camera.tilt");
map.set("fog.rangeStart", 1.2)
map.set("terrain.exaggeration", 1.4)
map.fog().apply(["enabled": true, "rangeEnd": 4.0])
let tilt = map.getDouble("camera.tilt")
map.set('fog.rangeStart', 1.2);
map.set('terrain.exaggeration', 1.4);
map.fog().apply({ enabled: true, rangeEnd: 4.0 });
const tilt = map.getDouble('camera.tilt');
A group (map.fog(), layer.style()) is only a path prefix — there are no 700 hand-written
accessors behind it, and apply hands the whole object over in one call rather than looping.
Events
map.onClick(e -> map.camera().animate(2).moveTo(e.position(), 14));
layer.onFeatureClick(e -> show(e.featureId(), e.get("name")));
Payloads are read lazily — a feature with a long geometry costs nothing unless the handler asks for the geometry — and an event object is valid only for the duration of the handler. The event names and their payloads are listed per class in the reference.
Bringing an existing app across
An app on the object API does not have to rebuild its map. adopt gives an object it already
built an id, and with it every property, method and event of the surface API:
MassifLayer base = map.adoptFirst("base", VectorTileLayer.class);
base.onFeatureClick(e -> …);
That also covers the reverse direction — an AssetPackage subclass written in Kotlin, Swift or
TypeScript (an app folder, an encrypted bundle) is adopted under an id, and any assets key then
names it. Writing a whole data source that way is the
extension guide.
What it does not do
- Register a new spec
type. The table is generated at build time, so an extension constructs its object and adopts it under an id instead. - Replace the object API. Both surfaces are supported; the surface API is the one that grows.
Every example in the gallery is written against it, on all three platforms.