Skip to main content

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.

A floorplan pinned over the basemap

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.

The SDK does not read georeferencing metadata

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.

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);
No spec factory yet

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.
  • zoomLevelBias compensates for the upsampling a high-DPI screen applies. Without it a floorplan looks blurry at every zoom.
  • TILE_SUBSTITUTION_POLICY_VISIBLE stops the layer standing in with a stretched neighbouring tile outside the overlay's own extent.
  • The overlay is an ordinary raster layer, so opacity and layer order work as usual — and it drapes onto 3D terrain like any other raster.