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);
}
});
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="robots" content="noindex" />
<title>How to find relative coordinates for annotations</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="tool-panel collapsed" id="toolPanel">
<div
class="panel-header"
id="panelToggle"
role="button"
aria-expanded="false"
title="Toggle coordinate finder"
>
<div class="header-title">
<span>🎯</span>
<h3>Coordinate Finder</h3>
</div>
<button class="toggle-btn" id="toggleBtn" aria-label="Toggle panel">▲</button>
</div>
<div class="panel-content" id="panelContent">
<div class="help-text">
Use <strong>Arrow Keys</strong> to move the target pin.<br />
Hold <strong>Shift</strong> for faster steps.<br />
<strong>PageUp/Down</strong> for vertical height.
</div>
<div class="coord-display">
<div><span>x:</span><span id="coordX">0.000</span></div>
<div><span>z:</span><span id="coordZ">0.000</span></div>
<div><span>y:</span><span id="coordY">0.00000</span></div>
</div>
<button class="btn-copy" id="copyBtn">Copy JSON Coordinate</button>
<div class="dpad">
<div></div>
<button id="btnUp" title="Move North (-Z)">▲</button>
<div></div>
<button id="btnLeft" title="Move West (-X)">◀</button>
<button id="btnCenter" title="Center (0,0)">•</button>
<button id="btnRight" title="Move East (+X)">▶</button>
<div></div>
<button id="btnDown" title="Move South (+Z)">▼</button>
<div></div>
</div>
</div>
</div>
<div id="map"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
body {
font-family: Inter, sans-serif;
margin: 0;
overflow: hidden;
}
#map {
width: 100%;
height: 100vh;
}
.tool-panel {
position: absolute;
top: 12px;
left: 12px;
width: 250px;
max-width: calc(100% - 24px);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
border-radius: 10px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.16);
z-index: 10;
overflow: hidden;
transition: box-shadow 0.25s ease;
user-select: none;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
cursor: pointer;
background: transparent;
transition: background 0.15s ease;
}
.panel-header:hover {
background: rgba(0, 0, 0, 0.04);
}
.header-title {
display: flex;
align-items: center;
gap: 6px;
}
.tool-panel h3 {
font-size: 13.5px;
font-weight: 700;
margin: 0;
color: #222;
white-space: nowrap;
}
.toggle-btn {
background: transparent;
border: none;
font-size: 10px;
color: #666;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
transition: transform 0.25s ease;
}
.tool-panel.collapsed .toggle-btn {
transform: rotate(180deg);
}
.panel-content {
padding: 0 12px 12px 12px;
max-height: 280px;
overflow-y: auto;
transition:
max-height 0.25s ease,
opacity 0.2s ease,
padding 0.25s ease;
}
.tool-panel.collapsed .panel-content {
max-height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
pointer-events: none;
}
.coord-display {
background: #f1f5f9;
border: 1px solid #cbd5e1;
border-radius: 6px;
padding: 8px 10px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
margin: 8px 0;
color: #0f172a;
}
.coord-display div {
display: flex;
justify-content: space-between;
}
.help-text {
font-size: 11px;
color: #64748b;
line-height: 1.4;
margin-bottom: 8px;
}
.btn-copy {
background: #e63946;
color: #fff;
border: none;
width: 100%;
padding: 6px 12px;
border-radius: 6px;
font-weight: 600;
font-size: 12px;
cursor: pointer;
transition: background 0.15s ease;
}
.btn-copy:hover {
background: #d62828;
}
.dpad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
margin-top: 8px;
}
.dpad button {
background: #f8f9fa;
border: 1px solid #ced4da;
padding: 4px 0;
font-size: 11px;
font-weight: 600;
border-radius: 4px;
cursor: pointer;
}
.dpad button:hover {
background: #e2e8f0;
}
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.xaxis: Ranges from-1.0(western/left edge) to+1.0(eastern/right edge).zaxis: 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));
});
Navigating the interactive target pin
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.01units. - Shift + Arrow keys: Nudge the target pin by larger
0.1steps. - PageUp / PageDown: Fine-tune vertical height
yin 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.
Related examples
Model vs geographic positioning
ExamplesPlace annotations using model-relative bounding box coordinates or geographic coordinates.
Pin styles and categories
ExamplesStyle annotations with colors, icons, numbers, and custom popup placement.
Edit model in custom editor
ExamplesBuild a custom 3D editor to adjust the position, rotation, and scale of splat models and save their properties to a database.