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.

How to find relative coordinates for annotations

This example provides an interactive developer tool to find, calibrate, and copy model-relative annotation coordinates ({ x, z, y }) directly from any 3D Gaussian splat model.

Controls: Click inside the map and use your keyboard’s Arrow keys to move the target pin across the model (hold Shift for faster steps, PageUp/PageDown for height, and press Enter to copy coordinates). You can also click the 🎯 Coordinate Finder button in the top-left corner to open the on-screen panel.

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 map = new Map({
  apiKey: config.apiKey,
  container: "map",
  center: [3.16689, 39.76162],
  zoom: 19,
  pitch: 50,
  bearing: -15,
});

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

let pos = { x: 0, z: 0, y: 0 };
let targetPin;

function updateTargetUI() {
  const elX = document.getElementById("coordX");
  const elZ = document.getElementById("coordZ");
  const elY = document.getElementById("coordY");

  if (elX) elX.textContent = pos.x.toFixed(3);
  if (elZ) elZ.textContent = pos.z.toFixed(3);
  if (elY) elY.textContent = pos.y.toFixed(5);

  if (targetPin) {
    targetPin.setPosition(pos);
    targetPin.setLabel(`${pos.x.toFixed(2)}, ${pos.z.toFixed(2)}`);
  }
}

function getFormattedJSON() {
  const p = {
    x: +pos.x.toFixed(3),
    z: +pos.z.toFixed(3),
    ...(pos.y !== 0 ? { y: +pos.y.toFixed(6) } : {}),
  };
  return JSON.stringify(p);
}

// Collapsible toolbox toggle logic (starts collapsed)
const toolPanel = document.getElementById("toolPanel");
const panelToggle = document.getElementById("panelToggle");
if (panelToggle && toolPanel) {
  panelToggle.addEventListener("click", () => {
    toolPanel.classList.toggle("collapsed");
    panelToggle.setAttribute("aria-expanded", !toolPanel.classList.contains("collapsed"));
  });
}

map.on("splatmodel:load", () => {
  // 1. Enable visual yellow bounding box to see model volume
  splatModel.setBoundingBox(true, "yellow");

  // 2. Add reference markers along corners and edges
  const edgeMarkers = [
    { pos: { x: 1, z: 0 }, label: "+X (1, 0)" },
    { pos: { x: -1, z: 0 }, label: "-X (-1, 0)" },
    { pos: { x: 0, z: 1 }, label: "+Z (0, 1)" },
    { pos: { x: 0, z: -1 }, label: "-Z (0, -1)" },
    { pos: { x: 1, z: 1 }, label: "(1, 1)" },
    { pos: { x: -1, z: 1 }, label: "(-1, 1)" },
    { pos: { x: 1, z: -1 }, label: "(1, -1)" },
    { pos: { x: -1, z: -1 }, label: "(-1, -1)" },
  ];

  for (const m of edgeMarkers) {
    splatModel.addAnnotation({
      pos: m.pos,
      label: m.label,
      color: [100, 116, 139, 0.75],
    });
  }

  // 3. Create the interactive red target pin at { x: 0, z: 0, y: 0 }
  targetPin = splatModel.addAnnotation({
    pos,
    icon: "🎯",
    label: "0.00, 0.00",
    color: "#e63946",
    popup: `<h3>🎯 Target Pin</h3><p>Active pin for calibrating annotation coordinates.</p>`,
  });

  updateTargetUI();
});

// Auto-expand tool panel if user clicks the target pin directly
map.on("splatmodel:annotation:active", ({ annotation }) => {
  if (annotation === targetPin && toolPanel && toolPanel.classList.contains("collapsed")) {
    toolPanel.classList.remove("collapsed");
    panelToggle?.setAttribute("aria-expanded", "true");
  }
});

// Keyboard navigation listener
window.addEventListener("keydown", (e) => {
  const step = e.shiftKey ? 0.1 : 0.01;

  if (e.key === "ArrowRight") pos.x -= step;
  else if (e.key === "ArrowLeft") pos.x += step;
  else if (e.key === "ArrowUp") pos.z += step;
  else if (e.key === "ArrowDown") pos.z -= step;
  else if (e.key === "PageUp") pos.y += 0.00005;
  else if (e.key === "PageDown") pos.y -= 0.00005;
  else if (e.key === "Enter") {
    const jsonStr = getFormattedJSON();
    console.log("Copied annotation pos:", jsonStr);
    navigator.clipboard?.writeText(jsonStr);
    const btn = document.getElementById("copyBtn");
    if (btn) {
      btn.textContent = "Copied to Clipboard!";
      setTimeout(() => {
        btn.textContent = "Copy JSON Coordinate";
      }, 1500);
    }
    return;
  } else {
    return;
  }

  e.preventDefault();
  updateProbeUI();
});

// On-screen D-Pad buttons
document.getElementById("btnUp")?.addEventListener("click", () => {
  pos.z += 0.02;
  updateProbeUI();
});
document.getElementById("btnDown")?.addEventListener("click", () => {
  pos.z -= 0.02;
  updateProbeUI();
});
document.getElementById("btnLeft")?.addEventListener("click", () => {
  pos.x += 0.02;
  updateProbeUI();
});
document.getElementById("btnRight")?.addEventListener("click", () => {
  pos.x -= 0.02;
  updateProbeUI();
});
document.getElementById("btnCenter")?.addEventListener("click", () => {
  pos = { x: 0, z: 0, y: 0 };
  updateTargetUI();
});

// Copy button
document.getElementById("copyBtn")?.addEventListener("click", () => {
  const jsonStr = getFormattedJSON();
  console.log("Copied annotation pos:", jsonStr);
  navigator.clipboard?.writeText(jsonStr);
  const btn = document.getElementById("copyBtn");
  if (btn) {
    btn.textContent = "Copied to Clipboard!";
    setTimeout(() => {
      btn.textContent = "Copy JSON Coordinate";
    }, 1500);
  }
});

Why use model-relative coordinates

Model-relative coordinates (pos: { x, z, y }) are normalized to the 3D bounding box of the splat model:

  • { x: 0, z: 0 }: Represents the exact horizontal center of the model.
  • x axis: Ranges from -1.0 (western/left edge) to +1.0 (eastern/right edge).
  • z axis: Ranges from -1.0 (southern/front edge) to +1.0 (northern/back edge).
  • y: Optional vertical height offset above the collision mesh or terrain.

Using model-relative coordinates ensures that pins stay perfectly attached to architectural features even if the model is rotated, dragged, or repositioned on the map.

Enabling the model bounding box and corner pins

Visualizing the model’s bounding box and corner landmarks helps establish a reference grid:


map.on("splatmodel:load", () => {
  // Show yellow 3D bounding box wireframe
  splatModel.setBoundingBox(true, "yellow");

  // Place reference markers on corner extremes
  const corners = [
    { pos: { x: 1, z: 1 }, label: "(1, 1)" },
    { pos: { x: -1, z: 1 }, label: "(-1, 1)" },
    { pos: { x: 1, z: -1 }, label: "(1, -1)" },
    { pos: { x: -1, z: -1 }, label: "(-1, -1)" },
  ];

  corners.forEach((c) => splatModel.addAnnotation(c));
});

A target pin (SplatAnnotation) is placed at { x: 0, z: 0, y: 0 }. Listening to keyboard events lets you nudge the pin in real time using targetPin.setPosition(pos):


let pos = { x: 0, z: 0, y: 0 };
let targetPin = splatModel.addAnnotation({
  pos,
  icon: "🎯",
  color: "#e63946",
});

window.addEventListener("keydown", (e) => {
  const step = e.shiftKey ? 0.1 : 0.01;

  if (e.key === "ArrowRight") pos.x -= step;
  else if (e.key === "ArrowLeft") pos.x += step;
  else if (e.key === "ArrowUp") pos.z += step;
  else if (e.key === "ArrowDown") pos.z -= step;
  else if (e.key === "PageUp") pos.y += 0.00005;
  else if (e.key === "PageDown") pos.y -= 0.00005;
  else return;

  e.preventDefault();
  targetPin.setPosition(pos);
  targetPin.setLabel(`${pos.x.toFixed(2)}, ${pos.z.toFixed(2)}`);
});
  • Arrow keys: Nudge the target pin horizontally by 0.01 units.
  • Shift + Arrow keys: Nudge the target pin by larger 0.1 steps.
  • PageUp / PageDown: Fine-tune vertical height y in world units.

Copying coordinates into production code

Pressing Enter or clicking Copy JSON Coordinate formats the exact position into a JSON object ready to paste directly into your splatModel.addAnnotation() call:


{ "x": 0.24, "z": -0.15 }

Learn more

Check out the MapTiler GeoSplats SDK reference to explore the full set of methods for SplatAnnotation and SplatModel.

How to add annotations to a splat model

Add annotations

Examples

Add interactive labelled pins and popups to a splat model.

Position annotations relative to model or geography

Model vs geographic positioning

Examples

Place annotations using model-relative bounding box coordinates or geographic coordinates.

Customize annotation pins and colors

Pin styles and categories

Examples

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

Edit model in custom editor

Edit model in custom editor

Examples

Build a custom 3D editor to adjust the position, rotation, and scale of splat models and save their properties to a database.

Was this helpful?