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.

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";
  }
});

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.

How to add annotations to a splat model

Add annotations

Examples

Add interactive labelled pins and popups to a splat model.

Create rich HTML popups in annotations

Rich HTML popups

Examples

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

Filter annotations by category at runtime

Filter annotations by category

Examples

Filter annotations by category using visibility toggle or color dimming.

Synchronize annotations with a sidebar list

Sync annotations with sidebar

Examples

Connect 3D model annotations bidirectionally with an external sidebar list.

Was this helpful?