Ground overlays
A ground overlay pins a bitmap (PNG, JPG) to real-world coordinates and draws it as a raster layer — an indoor floorplan over a building, a scanned map sheet, a site plan.

The pinning is done with ground control points: pixel positions in the bitmap paired with map positions. Three points give a linear affine transform (position, size, rotation); four give a perspective one. More than four is not supported — pick the four best.
Control points come from your code. A GeoTIFF, ESRI World File, MapInfo TAB or geospatial PDF carries its own referencing, but reading it means a GIS library — that belongs in an extension, not in the SDK.
The whole bitmap has to fit in RAM. Around 2000×2000 pixels is a realistic ceiling on a phone. For anything larger, tile it — PMTiles or MBTiles built once beats a giant bitmap loaded every launch.
Pinning a floorplan
The building below is known to be 220 m north–south and 200 m east–west and to face north, so one coordinate plus the size gives all four corners.
- Java
- Swift
Bitmap overlay = BitmapUtils.loadBitmapFromAssets("ground-floor.jpg");
// Corners of the building, in the layer's projection.
MapPos pos = proj.fromWgs84(new MapPos(-77.004590, 38.888702));
double sizeNS = 110, sizeWE = 100;
MapPosVector mapPoses = new MapPosVector();
mapPoses.add(new MapPos(pos.getX() - sizeWE, pos.getY() + sizeNS));
mapPoses.add(new MapPos(pos.getX() + sizeWE, pos.getY() + sizeNS));
mapPoses.add(new MapPos(pos.getX() + sizeWE, pos.getY() - sizeNS));
mapPoses.add(new MapPos(pos.getX() - sizeWE, pos.getY() - sizeNS));
// The same four corners, in bitmap pixels — same order, or the image comes out mirrored.
ScreenPosVector bitmapPoses = new ScreenPosVector();
bitmapPoses.add(new ScreenPos(0, 0));
bitmapPoses.add(new ScreenPos(0, overlay.getHeight()));
bitmapPoses.add(new ScreenPos(overlay.getWidth(), overlay.getHeight()));
bitmapPoses.add(new ScreenPos(overlay.getWidth(), 0));
BitmapOverlayRasterTileDataSource source =
new BitmapOverlayRasterTileDataSource(0, 20, overlay, proj, mapPoses, bitmapPoses);
RasterTileLayer layer = new RasterTileLayer(source);
mapView.getLayers().add(layer);
// Bitmaps are upsampled on a high-DPI screen; bias the zoom back down or the image goes soft.
float bias = (float) (Math.log(mapView.getOptions().getDPI() / 160.0f) / Math.log(2));
layer.setZoomLevelBias(bias * 0.75f);
layer.setTileSubstitutionPolicy(TileSubstitutionPolicy.TILE_SUBSTITUTION_POLICY_VISIBLE);
let overlay = MSFBitmapUtils.createBitmap(from: UIImage(named: "ground-floor.jpg"))
let pos = projection!.fromWgs84(MSFMapPos(x: -77.004590, y: 38.888702))!
let sizeNS = 110.0, sizeWE = 100.0
let mapPoses = MSFMapPosVector()
mapPoses?.add(MSFMapPos(x: pos.getX() - sizeWE, y: pos.getY() + sizeNS))
mapPoses?.add(MSFMapPos(x: pos.getX() + sizeWE, y: pos.getY() + sizeNS))
mapPoses?.add(MSFMapPos(x: pos.getX() + sizeWE, y: pos.getY() - sizeNS))
mapPoses?.add(MSFMapPos(x: pos.getX() - sizeWE, y: pos.getY() - sizeNS))
let bitmapPoses = MSFScreenPosVector()
bitmapPoses?.add(MSFScreenPos(x: 0, y: 0))
bitmapPoses?.add(MSFScreenPos(x: 0, y: Float(overlay!.getHeight())))
bitmapPoses?.add(MSFScreenPos(x: Float(overlay!.getWidth()), y: Float(overlay!.getHeight())))
bitmapPoses?.add(MSFScreenPos(x: Float(overlay!.getWidth()), y: 0))
let source = MSFBitmapOverlayRasterTileDataSource(
minZoom: 0, maxZoom: 20, bitmap: overlay, projection: projection,
mapPoses: mapPoses, bitmapPoses: bitmapPoses)
let layer = MSFRasterTileLayer(dataSource: source)
mapView?.getLayers()?.add(layer)
let bias = log(mapView!.getOptions().getDPI() / 160.0) / log(2.0)
layer?.setZoomLevelBias(bias * 0.75)
layer?.setTileSubstitutionPolicy(.TILE_SUBSTITUTION_POLICY_VISIBLE)
BitmapOverlayRasterTileDataSource is object-API only — it takes two parallel position lists,
which a JSON spec has no form for. Build it, then
adopt it under an id to reach it from the
surface API:
Massif.adopt("source", "floorplan", source);
map.addLayer("floorplan", Spec.of("raster").set("source", "floorplan"));
Getting it right
- Point order must match between the two lists — corner n in map space is corner n in pixels. A mirrored overlay is almost always this.
zoomLevelBiascompensates for the upsampling a high-DPI screen applies. Without it a floorplan looks blurry at every zoom.TILE_SUBSTITUTION_POLICY_VISIBLEstops the layer standing in with a stretched neighbouring tile outside the overlay's own extent.- The overlay is an ordinary raster layer, so
opacityand layer order work as usual — and it drapes onto 3D terrain like any other raster.