For AI agents: a documentation index is available at /llms.txt. Markdown versions of all pages are available by appending .md to any URL.
Update annotations with live on-demand data
This example demonstrates how to load popup content asynchronously on demand and stream live telemetry updates directly to 3D Gaussian splat annotation labels, colors, and open popups.
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);
});
let parkingAnnotation;
let poolAnnotation;
let isStreaming = true;
let timerId = null;
let parkingOccupancy = 68;
let poolCapacity = 42;
function getColorForPercent(pct) {
if (pct >= 90) return "red";
if (pct >= 60) return "#e69439";
return "#2bb673";
}
function generateParkingPopup(pct) {
const free = Math.max(0, Math.round(240 * (1 - pct / 100)));
return `<h3>🚘 Parking Occupancy</h3>
<p><strong>${pct}% occupied</strong> (${free} bays available)</p>
<div style="background:#eee;border-radius:4px;height:8px;overflow:hidden;margin-top:6px;">
<div style="background:${getColorForPercent(pct)};width:${pct}%;height:100%;"></div>
</div>`;
}
function generatePoolPopup(pct) {
return `<h3>🏖️ Sun Pool Capacity</h3>
<p><strong>${pct}% capacity</strong></p>
<p style="font-size:12px;color:#666;">Water Temp: 28°C · Sunbeds free: ${Math.round(50 * (1 - pct / 100))}</p>`;
}
map.on("splatmodel:load", () => {
// 1. Parking annotation with live occupancy
parkingAnnotation = splatModel.addAnnotation({
pos: { x: 0.1, z: 0.2 },
icon: "🚘",
label: `Parking: ${parkingOccupancy}%`,
color: getColorForPercent(parkingOccupancy),
data: { id: "parking", loaded: false },
});
// 2. Pool annotation with on-demand data fetch
poolAnnotation = splatModel.addAnnotation({
pos: { x: -0.4, z: -0.2 },
icon: "🏖️",
label: `Pool: ${poolCapacity}%`,
color: getColorForPercent(poolCapacity),
data: { id: "pool", loaded: false },
});
startStream();
});
// On-demand popup content loading on click
map.on("splatmodel:annotation:click", ({ annotation }) => {
const data = annotation.getData();
if (!data) return;
if (!data.loaded) {
// Show loading placeholder immediately before popup opens
annotation.setPopup(
'<div style="padding:12px;text-align:center;"><em>Fetching live telemetry…</em></div>',
);
// Simulate async backend fetch
setTimeout(() => {
data.loaded = true;
if (data.id === "parking") {
annotation.setPopup(generateParkingPopup(parkingOccupancy));
} else if (data.id === "pool") {
annotation.setPopup(generatePoolPopup(poolCapacity));
}
}, 500);
}
});
function updateTelemetry() {
if (!isStreaming) return;
// Random jitter
parkingOccupancy = Math.min(
100,
Math.max(10, parkingOccupancy + Math.floor(Math.random() * 9) - 4),
);
poolCapacity = Math.min(100, Math.max(5, poolCapacity + Math.floor(Math.random() * 11) - 5));
if (parkingAnnotation) {
parkingAnnotation.setLabel(`Parking: ${parkingOccupancy}%`);
parkingAnnotation.setColor(getColorForPercent(parkingOccupancy));
if (parkingAnnotation.isActive()) {
parkingAnnotation.setPopup(generateParkingPopup(parkingOccupancy));
}
}
if (poolAnnotation) {
poolAnnotation.setLabel(`Pool: ${poolCapacity}%`);
poolAnnotation.setColor(getColorForPercent(poolCapacity));
if (poolAnnotation.isActive()) {
poolAnnotation.setPopup(generatePoolPopup(poolCapacity));
}
}
}
function startStream() {
if (timerId) clearInterval(timerId);
timerId = setInterval(updateTelemetry, 3000);
}
document.getElementById("toggleStreamBtn")?.addEventListener("click", () => {
isStreaming = !isStreaming;
const btn = document.getElementById("toggleStreamBtn");
const status = document.getElementById("streamStatus");
const dot = document.getElementById("pulseDot");
if (!btn || !status || !dot) return;
if (isStreaming) {
btn.textContent = "Pause Stream";
status.textContent = "Receiving updates every 3s";
dot.style.background = "#2bb673";
} else {
btn.textContent = "Resume Stream";
status.textContent = "Stream paused";
dot.style.background = "#999";
}
});
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="robots" content="noindex" />
<title>Update annotations with live on-demand data</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="telemetry-panel">
<div class="title"><span class="status-badge" id="pulseDot"></span>Live Sensor Stream</div>
<div style="color: #666; font-size: 12px" id="streamStatus">Receiving updates every 3s</div>
<button id="toggleStreamBtn">Pause Stream</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;
}
.telemetry-panel {
position: absolute;
top: 16px;
left: 16px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(4px);
padding: 12px 18px;
border-radius: 10px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
z-index: 10;
font-size: 13px;
}
.telemetry-panel .title {
font-weight: 700;
color: #333;
margin-bottom: 4px;
}
.status-badge {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #2bb673;
margin-right: 6px;
}
.telemetry-panel button {
background: #3174ff;
color: #fff;
border: none;
padding: 5px 12px;
border-radius: 6px;
cursor: pointer;
width: 100%;
font-size: 12px;
font-weight: 600;
margin-top: 6px;
}
Loading popup content on demand
Because the "annotation:click" event fires before the popup opens, you can assign an HTML popup inside your click listener. This enables showing an immediate loading placeholder while fetching real-time data from an API:
splatModel.on("annotation:click", async ({ annotation }) => {
const data = annotation.getData();
if (data.loaded) return;
// Placeholder opens immediately on the current click
annotation.setPopup("<p>Loading live telemetry…</p>");
const response = await fetch(`/api/sensors/${data.id}`);
const details = await response.json();
data.loaded = true;
// Updates the card in place
annotation.setPopup(`<h3>${details.title}</h3><p>${details.reading}</p>`);
});
Streaming live telemetry updates
You can update annotation properties as frequently as needed (such as from WebSocket events or periodic timers). Calling setLabel() and setColor() recalculates the pin dimensions with smooth transitions:
function onTelemetry(occupancyRate) {
annotation.setLabel(`Parking: ${occupancyRate}%`);
const color = occupancyRate >= 90 ? "red" : occupancyRate >= 60 ? "#e69439" : "#2bb673";
annotation.setColor(color);
}
The SDK avoids unnecessary re-renders if the provided string or color is identical to the current value.
Updating active popups in place
If an annotation’s popup is currently open when new data arrives, calling annotation.setPopup(newHtml) updates the card in place without closing it:
if (annotation.isActive()) {
annotation.setPopup(`<h3>🚘 Parking</h3><p>${freeSpaces} bays available.</p>`);
}
Learn more
Check out the MapTiler GeoSplats SDK reference to explore the SplatAnnotation runtime methods.
Related examples
Rich HTML popups
ExamplesDisplay custom HTML cards in annotation popups with images, tables, and custom widths.
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.