For AI agents: a documentation index is available at /llms.txt. Markdown versions of all pages are available by appending .md to any URL.
Create a guided tour with annotations
This example demonstrates how to build a guided tour by controlling annotations programmatically with openPopup() and closePopup().
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: [-80.27822, 25.74077],
zoom: 17.5,
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);
});
const tour = [];
let currentStep = 0;
function goTo(index) {
if (!tour.length) return;
currentStep = (index + tour.length) % tour.length;
const indicator = document.getElementById("stepIndicator");
if (indicator) {
indicator.textContent = `Step ${currentStep + 1} of ${tour.length}`;
}
tour[currentStep].openPopup();
}
map.on("splatmodel:load", () => {
const a1 = splatModel.addAnnotation({
pos: { x: -0.06, z: -0.26 },
icon: "🏢",
label: "1. Main Lobby",
popup: "<h3>1. Main Lobby</h3><p>Grand entrance hall and 24/7 concierge reception.</p>",
});
const a2 = splatModel.addAnnotation({
pos: { x: -0.4, z: -0.2 },
icon: "🏖️",
label: "2. Sun Pool",
popup: "<h3>2. Sun Pool</h3><p>Heated resort pool surrounded by palm groves and cabanas.</p>",
});
const a3 = splatModel.addAnnotation({
pos: { x: 0.6, z: 0.46 },
icon: "🎾",
label: "3. Tennis Club",
popup: "<h3>3. Tennis Club</h3><p>10 floodlit championship clay courts.</p>",
});
const a4 = splatModel.addAnnotation({
pos: { x: -0.6, z: 0.5 },
icon: "⛳",
label: "4. Golf Course",
popup:
"<h3>4. Golf Course</h3><p>18-hole championship green surrounding the resort grounds.</p>",
});
tour.push(a1, a2, a3, a4);
goTo(0);
});
document.getElementById("nextBtn")?.addEventListener("click", () => goTo(currentStep + 1));
document.getElementById("prevBtn")?.addEventListener("click", () => goTo(currentStep - 1));
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowRight") {
goTo(currentStep + 1);
} else if (e.key === "ArrowLeft") {
goTo(currentStep - 1);
} else if (e.key === "Escape") {
tour[currentStep]?.closePopup();
}
});
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="robots" content="noindex" />
<title>Create a guided tour with annotations</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="tour-control">
<button id="prevBtn">◀ Prev</button>
<span id="stepIndicator">Step 1 of 4</span>
<button id="nextBtn">Next ▶</button>
</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;
}
.tour-control {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(4px);
padding: 8px 16px;
border-radius: 30px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
gap: 12px;
z-index: 10;
}
.tour-control button {
background: #3174ff;
color: #fff;
border: none;
padding: 6px 14px;
border-radius: 16px;
font-weight: 600;
font-size: 13px;
cursor: pointer;
}
.tour-control button:hover {
background: #1b5ce5;
}
.tour-control span {
font-size: 14px;
font-weight: 600;
color: #333;
min-width: 90px;
text-align: center;
}
Opening popups programmatically
While popups typically open on user clicks, you can trigger them directly in JavaScript using the SplatAnnotation instance handle.
Calling annotation.openPopup() opens the annotation’s popup and automatically closes whichever popup was previously open:
const tour = []; // SplatAnnotation handles
function goTo(stepIndex) {
tour[stepIndex].openPopup();
}
Unlike user clicks, openPopup() does not fire "annotation:click", but it fires "annotation:active" on the model and "splatmodel:annotation:active" on the map.
Navigating between tour steps
Store the returned SplatAnnotation handles in an array when adding annotations during splatmodel:load. You can then step forward or backward using external UI buttons:
let currentStep = 0;
document.getElementById("nextBtn").addEventListener("click", () => {
currentStep = (currentStep + 1) % tour.length;
tour[currentStep].openPopup();
});
When a popup opens, the active pin bounces and a pulse ring ripples across the model surface, directing the viewer’s attention to the active point of interest.
Keyboard shortcuts and escape handling
You can close any open popup programmatically with closePopup(). This makes it simple to bind keyboard shortcuts like Escape or the left and right arrow keys:
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
tour[currentStep]?.closePopup();
}
});
Learn more
Check out the MapTiler GeoSplats SDK reference to explore the SplatAnnotation interface.
Related examples
Pin styles and categories
ExamplesStyle annotations with colors, icons, numbers, and custom popup placement.
Filter annotations by category
ExamplesFilter annotations by category using visibility toggle or color dimming.
Sync annotations with sidebar
ExamplesConnect 3D model annotations bidirectionally with an external sidebar list.