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.

Track and update visited annotation states

This example demonstrates how to use the SplatAnnotation runtime methods setColor(), setIcon(), and event listeners to track user engagement and mark houses as visited across a 6-house residential development.

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: [-74.42418, 40.77947],
  zoom: 18.2,
  pitch: 52,
  bearing: -30,
});

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

const VISITED_COLOR = "#e69439";
const visitedSet = new Set();
const annotationsList = [];

const houseConfigs = [
  {
    id: "house-1",
    pos: { x: 0.05, z: -0.58 },
    icon: "🏑",
    label: "House 1",
    popup:
      "<h3>🏑 House 1</h3><p>Two-story colonial with private driveway and landscaped lawn.</p>",
  },
  {
    id: "house-2",
    pos: { x: 0.35, z: -0.35 },
    icon: "🏑",
    label: "House 2",
    popup: "<h3>🏑 House 2</h3><p>Modern family residence featuring an open-concept patio.</p>",
  },
  {
    id: "house-3",
    pos: { x: -0.4, z: 0.05 },
    icon: "🏑",
    label: "House 3",
    popup: "<h3>🏑 House 3</h3><p>Spacious 4-bedroom home with attached two-car garage.</p>",
  },
  {
    id: "house-4",
    pos: { x: 0.4, z: 0.05 },
    icon: "🏑",
    label: "House 4",
    popup: "<h3>🏑 House 4</h3><p>Contemporary suburban home with south-facing solar roof.</p>",
  },
  {
    id: "house-5",
    pos: { x: -0.3, z: 0.42 },
    icon: "🏑",
    label: "House 5",
    popup:
      "<h3>🏑 House 5</h3><p>Corner lot cottage with mature oak trees and fenced backyard.</p>",
  },
  {
    id: "house-6",
    pos: { x: -0.55, z: -0.35 },
    icon: "🏑",
    label: "House 6",
    popup:
      "<h3>🏑 House 6</h3><p>Executive villa with walk-out basement and scenic garden view.</p>",
  },
];

function updateCounter() {
  const counter = document.getElementById("visitedCount");
  if (counter) {
    counter.textContent = `${visitedSet.size} / ${houseConfigs.length}`;
  }
}

map.on("splatmodel:load", () => {
  for (const config of houseConfigs) {
    const annotation = splatModel.addAnnotation({
      pos: config.pos,
      icon: config.icon,
      label: config.label,
      popup: config.popup,
      data: { id: config.id, defaultIcon: config.icon },
    });
    if (annotation) {
      annotationsList.push(annotation);
    }
  }
});

// 1. Mark with visited color when clicked
map.on("splatmodel:annotation:click", ({ annotation }) => {
  annotation.setColor(VISITED_COLOR);
  const data = annotation.getData();
  if (data && data.id) {
    visitedSet.add(data.id);
    updateCounter();
  }
});

// 2. Change icon to checkmark once popup closes
map.on("splatmodel:annotation:inactive", ({ annotation }) => {
  const data = annotation.getData();
  if (data && visitedSet.has(data.id)) {
    annotation.setIcon("βœ”οΈ");
  }
});

// 3. Reset all visited annotations
document.getElementById("resetBtn")?.addEventListener("click", () => {
  visitedSet.clear();
  for (const ann of annotationsList) {
    const data = ann.getData();
    ann.setColor(undefined);
    if (data && data.defaultIcon) {
      ann.setIcon(data.defaultIcon);
    }
  }
  updateCounter();
});

Listening to annotation events

The GeoSplats SDK provides three key annotation lifecycle events:

  • "annotation:click": Fires when the user clicks a pin, right before its popup begins to open.
  • "annotation:active": Fires when the popup opens on screen.
  • "annotation:inactive": Fires when the popup closes (via the close button, clicking the map, or opening another popup).

Every event delivers the affected annotation handle in its payload.

Updating pin colors dynamically

When a user clicks a house annotation, call annotation.setColor() to change its background fill immediately to an in-progress color:


const VISITED_COLOR = "#e69439";

map.on("splatmodel:annotation:click", ({ annotation }) => {
  annotation.setColor(VISITED_COLOR);
});

The pin animates smoothly and maintains its new color even when the camera moves.

Changing icons when popups close

When the user finishes reading a popup and closes it, the "annotation:inactive" event fires. Use annotation.setIcon() to replace the pin’s house icon with a completion badge like a checkmark:


map.on("splatmodel:annotation:inactive", ({ annotation }) => {
  annotation.setIcon("βœ”οΈ");
});

The pin automatically recalculates its geometry and resizes with a brief squash-and-stretch animation.

Resetting annotation properties

Calling annotation.setColor(undefined) removes the custom color and restores the default MapTiler blue theme:


// Reset pin to default theme
annotation.setColor(undefined);

// Restore original icon from custom data
const data = annotation.getData();
if (data?.defaultIcon) {
  annotation.setIcon(data.defaultIcon);
}

Learn more

Check out the MapTiler GeoSplats SDK reference to learn more about the SplatAnnotation API and event parameters.

How to add annotations to a splat model

Add annotations

Examples

Add interactive labelled pins and popups to a splat model.

Customize annotation pins and colors

Pin styles and categories

Examples

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

Create a guided tour with annotations

Guided tour with annotations

Examples

Guide users through model annotations with next and previous buttons.

Filter annotations by category at runtime

Filter annotations by category

Examples

Filter annotations by category using visibility toggle or color dimming.

Was this helpful?