Join us online - 19th of November
Save your spot
For AI agents: a documentation index is available at /llms.txt. Markdown versions of all pages are available by appending .md to any URL.

Position annotations relative to model or geography

This example explains the two coordinate systems supported by annotations in the MapTiler GeoSplats SDK: model-relative bounding box coordinates and geographic coordinates.

NPM module setup


npm install --save @maptiler/geosplats
import { SplatModel, Map, config } from "@maptiler/geosplats";
import "@maptiler/geosplats/dist/maptiler-geosplats.css";

config.apiKey = "YOUR_MAPTILER_API_KEY_HERE";

const HOTEL = { lng: -80.27822, lat: 25.74077 };

const map = new Map({
  apiKey: config.apiKey,
  container: "map",
  center: [HOTEL.lng, HOTEL.lat],
  zoom: 17.5,
  pitch: 76,
  bearing: 100,
});

const TILE_SIZE_AT_EQUATOR_M = 40_075_016.686 / 2 ** 14;
function metersToY(meters, latitude) {
  return meters / (TILE_SIZE_AT_EQUATOR_M * Math.cos((latitude * Math.PI) / 180));
}

let splatModel;
map.on("load", () => {
  splatModel = new SplatModel({
    model: "YOUR_MAPTILER_SPLAT_MODEL_ID_HERE",
    revealParams: { enabled: true },
  });
  map.addSplatModel(splatModel);
});

map.on("splatmodel:load", () => {
  // 1. Model-relative position: { x, z } normalized to the model bounding box [-1, 1]
  splatModel.addAnnotation({
    pos: { x: 0, z: 0 },
    icon: "🎯",
    label: "Model Center (0, 0)",
    popup:
      "<h3>Model Center</h3><p>Positioned at <code>{ x: 0, z: 0 }</code>, the normalized center of the model bounding box. Moves and rotates with the model.</p>",
  });

  // 2. Model-relative with height offset (y): hovers above the model collision surface
  splatModel.addAnnotation({
    pos: { x: -0.123, z: -0.123, y: metersToY(5, HOTEL.lat) },
    icon: "🗼",
    label: "Tower Rooftop (+5m)",
    color: "#2bb673",
    popup:
      "<h3>Tower Rooftop</h3><p>Elevated 5 meters above the surface using <code>y: metersToY(5, lat)</code>.</p>",
  });

  // 3. Geographic position: { lng, lat } in WGS84 degrees
  splatModel.addAnnotation({
    pos: { lng: -80.279382, lat: 25.741509, y: metersToY(10, HOTEL.lat) },
    icon: "🚏",
    label: "Bus Stop (WGS84) (+10m)",
    color: "#e69439",
    popup:
      "<h3>Bus Stop</h3><p>Fixed geographic coordinates <code>{ lng, lat }</code>. Anchored to the map and independent of model transforms.</p>",
  });

  // 4. Geographic position using { lon, lat }
  splatModel.addAnnotation({
    pos: { lon: -80.278575, lat: 25.741053 },
    icon: "🚕",
    label: "Taxi Stand (WGS84)",
    color: "#e69439",
    popup:
      "<h3>Taxi Stand</h3><p>Real-world POI defined using <code>{ lon, lat }</code> coordinates.</p>",
  });
});

Model-relative coordinates

A model-relative position uses { x, z } coordinates normalized to the model’s bounding box:

  • { x: 0, z: 0 } is the horizontal center of the bounding box.
  • Values of -1 and 1 represent the bounding edges (-1 is left/bottom, 1 is right/top).
  • Values outside [-1, 1] place the pin outside the bounding box.

splatModel.addAnnotation({
  pos: { x: 0, z: 0 },
  label: "Model Center",
});

Model-relative annotations automatically move, rotate, and scale whenever the model is repositioned, rotated, or dragged. Use them for architectural points of interest, entrances, or equipment attached to the structure.

Geographic coordinates

A geographic position uses WGS84 coordinates defined by { lng, lat } (or { lon, lat }):


splatModel.addAnnotation({
  pos: { lng: -80.27857, lat: 25.74079 },
  label: "Bus stop",
});

Geographic annotations remain fixed at their geographic coordinates on the map surface, even if the 3D splat model moves. Use them for permanent surrounding landmarks, public transit stops, parking facilities, or road connections.

Vertical height offset

By default, an annotation rests directly on whatever surface lies beneath it (either the model’s collision mesh or the base terrain).

The optional y offset lifts or lowers the pin relative to that surface. It is measured in SDK tile-relative world units, where 1 unit corresponds to the dimension of a zoom-14 tile (~2,446 m at the equator). To position an annotation by a specific height in meters, convert meters using the model’s latitude:


const TILE_SIZE_AT_EQUATOR_M = 40_075_016.686 / 2 ** 14;

function metersToY(meters, latitude) {
  return meters / (TILE_SIZE_AT_EQUATOR_M * Math.cos((latitude * Math.PI) / 180));
}

// Hover 5 meters above the surface
splatModel.addAnnotation({
  pos: { x: 0.05, z: -0.1, y: metersToY(5, 25.74) },
  label: "Tower Rooftop (+5m)",
});

Learn more

Check out the MapTiler GeoSplats SDK reference for full type specifications of AnnotationPosition, ModelAnnotationPosition, and GeoAnnotationPosition.

How to add annotations to a splat model

Add annotations

Examples

Add interactive labelled pins and popups to a splat model.

Customize annotation pins and colors

Pin styles and categories

Examples

Style annotations with colors, icons, numbers, and custom popup placement.

Create rich HTML popups in annotations

Rich HTML popups

Examples

Display custom HTML cards in annotation popups with images, tables, and custom widths.

How to add a splat model to your map

Add splat model

Examples

Learn how to load a map using the GeoSplats SDK and add a photorealistic splat model. A splat model offer a modern alternative to 3D meshes, perfect for complex scenes.

Was this helpful?