Places module

The Places module draws places, whether search results or your own GeoJSON points, as markers on the map, and reports the clicks and hovers on them.

The module owns the place markers and labels it draws, and how they look (Place styling). A place that joins a route becomes one of the route’s waypoints, which the Routing module draws instead. The base map underneath belongs to the Styling Foundations module. Markers without a colour take its accent, and the marker types that borrow the look of the style’s POIs follow the knobs that restyle them. Who owns what compares every module.

The code below searches for parking and charging around a geocoded place and draws the results. The search response goes to show unchanged:

import { PlacesModule, TomTomMap } from '@tomtom-org/maps-sdk/map';
import { discoverPlaces, geocodeOne } from '@tomtom-org/maps-sdk/services';
const location = await geocodeOne('London Covent Garden');
const map = new TomTomMap({ mapLibre: { container: 'map', bounds: location.bbox } });
const parkingSpots = await discoverPlaces({
filters: { poiCategories: ['PARKING_GARAGE', 'OPEN_CAR_PARKING_AREA', 'CHARGING_LOCATION'] },
geoBias: { position: location },
limit: 50,
});
const placesModule = await PlacesModule.create(map);
await placesModule.show(parkingSpots);

The demo below runs it:

Two features of the module have their own guide:

GuideCovers
Place stylingMarker types and clusters, colour, labels, icons, extra properties, layer positioning and the knob catalogue
Connections and entry pointsLabelled lines between places, and the entrances a place’s data carries

Initialization

import { TomTomMap, PlacesModule } from '@tomtom-org/maps-sdk/map';
const map = new TomTomMap({ mapLibre: { container: 'map' } });
const placesModule = await PlacesModule.create(map);

PlacesModule.create makes a new instance on each call. See Multiple module instances.

Displaying places

import { discoverPlaces } from '@tomtom-org/maps-sdk/services';
// One place
await placesModule.show({
id: 'custom-place-1',
type: 'Feature',
geometry: { type: 'Point', coordinates: [4.9041, 52.3676] },
properties: {
type: 'POI',
address: { freeformAddress: 'Custom Place 1, Amsterdam' },
poi: { name: 'Custom Place' }
}
});
// Several places
await placesModule.show([place1, place2, place3]);
// Search results, as the service returns them
const searchResults = await discoverPlaces({
query: 'restaurants',
geoBias: { position: [4.9041, 52.3676] },
limit: 10
});
await placesModule.show(searchResults);
// Remove every place
await placesModule.clear();

What show() takes

show() takes a place, an array of places or a Places collection from search, geocoding, reverse geocoding or place details, or your own GeoJSON points, with no fields to map first. Two places services return something else:

ResultNot showable becauseShow instead
getSearchSuggestionsSuggestions to choose from, without geometryA place suggestion’s resolve(), or a brand or category suggestion’s refine()
geometryDataPolygonsThe Geometries module

preparePlacesForDisplay is what show() does to each place before drawing it. Call it to see the properties that layers overrides, extra properties and event handlers read:

import { preparePlacesForDisplay } from '@tomtom-org/maps-sdk/map';
const [first] = preparePlacesForDisplay(searchResults, { markerType: 'base-map' }).features;
console.log(first.properties.title, first.properties.iconID, first.properties.category);
  • It keeps every property of the place and copies id into properties. It adds title, iconID and the extra properties, plus category and group under base-map and pin-clustered, and baseMapIconID under pin-clustered.
  • buildPlaceTitle builds the default label (the POI name, otherwise the address). Fall back on it in your own label.title function.
  • toPlaces turns any of the three shapes show() accepts into one collection, such as the one a shown-features handler receives.
  • An iconID for an image the module adds, such as the default pin or a custom icon, is the first module’s name for it. getShown() returns what a given module drew.

Your own data

Pass your own GeoJSON points to show() as they are. They draw like the places a search returns. Each keeps its own properties, which label.title, layers overrides and click handlers read back. To type the label.title and icon.mapping callbacks, name your points’ shape on create:

import type { CommonPlaceProps, Places } from '@tomtom-org/maps-sdk/core';
import { PlacesModule } from '@tomtom-org/maps-sdk/map';
type Store = CommonPlaceProps & { storeName: string };
const stores: Places<Store> = await (await fetch('https://your-api.com/stores.json')).json();
const placesModule = await PlacesModule.create<Store>(map, {
markerType: 'base-map', // drawn like the map style's own POIs
icon: { mapping: { to: 'poiCategory', fn: () => 'COFFEE_SHOP' } },
label: { title: (place) => place.properties.storeName },
});
await placesModule.show(stores);
  • Draw them through the module, not through your own layer fed by preparePlacesForDisplay. The module adds the images its icons name, and brings events, clustering and restoration after a style change.
  • icon.mapping gives every point the base map’s icon and label colour for the POI category it returns, so your points sit among the style’s POIs as if they were its own. A categoryIcons entry that matches that category draws its icon instead. A point it returns undefined for keeps the icon it would have without a mapping. Route waypoints take the same mapping and label.title.
  • The default label is poi.name, otherwise address.freeformAddress. Points with neither need a label.title function.

Showing and hiding

setVisible hides and shows everything the module draws (places, connections and entry points) and keeps its data. To start it hidden, pass visible: false at creation.

const placesModule = await PlacesModule.create(map, { visible: false });
await placesModule.show(searchResults); // loaded, not drawn
placesModule.setVisible(true);
placesModule.isVisible(); // true
  • show() and clear() never change it, and it survives a style change.
  • Showing the module keeps hidden the layers its marker type does not use, and any layer you hid through layers.*.layout.visibility, such as main in micro-only rendering.

Events

User events

const stopListening = placesModule.events.on('click', (feature, lngLat) => {
console.log(feature.properties.title, lngLat);
});
placesModule.events.on('hover', (feature) => console.log(feature.properties.poi?.name));
stopListening(); // removes this handler alone
placesModule.events.off('hover'); // removes every hover handler

events covers every surface the module draws, so its handlers receive the raw MapLibre feature. Three named scopes narrow it:

  • events.places - Only the pins, typed as Place, which is what you want in TypeScript.
  • events.connections - The lines and labels drawn by showConnections.
  • events.entryPoints - The entry points.

Note that a cluster of the pin-clustered marker type isn’t a place, so events.places leaves it out. Its handlers don’t run for it, and the pointer keeps the map cursor over it. events receives it as MapLibre renders it, with cluster: true, cluster_id and point_count in its properties.

placesModule.events.places.on('click', (place) => console.log(place.properties.poi?.name));
placesModule.events.where((feature) => feature.properties.cluster === true).on('click', (cluster) => {
console.log(`${cluster.properties.point_count} places here`);
});
placesModule.events.connections.on('click', () => console.log('connection line clicked'));
placesModule.events.entryPoints.on('click', (entryPoint) => {
// A point at the entrance, whether its dot or its line was hit, with its place attached
console.log(entryPoint.properties.type, entryPoint.properties.place?.properties.title);
// It is a valid route location
calculateRoute({ locations: [origin, entryPoint] });
});

Result cards and popups

The module tells you which place was clicked or hovered, and does the hit-testing behind it. It draws no result card, popup or info window for a place, so open your own from the event.

import { Popup } from 'maplibre-gl';
const popup = new Popup({ closeButton: false, offset: 35 });
placesModule.events.places.on('click', (place) => {
popup
.setLngLat(place.geometry.coordinates)
.setText(place.properties.poi?.name ?? place.properties.address.freeformAddress)
.addTo(map.mapLibreMap);
});
// The popup is yours, so close it when the places change
popup.remove();
await placesModule.show(newResults);
  • A card is application chrome. Your app decides what it shows, how it looks and how it closes, so the SDK leaves it to you. Use a MapLibre Popup, or your own panel beside the map.
  • A place already has its anchor. The click passes the place, and a Popup sits on its coordinates. The Routing module draws its own route summary bubbles. A route has no single point to anchor one to, so the module places them along the line.
  • For the full details of a clicked place, such as opening hours, fetch them in the handler with getPlaceDetails.

Programmatic event states

Put, clean and read the event states of places as on every module that draws your data. They act on the places, whichever marker type draws them, clustered or not. events.connections and events.entryPoints have their own.

// Highlight the first place as if clicked
placesModule.setEventState({ index: 0, state: 'click' });
const { click: [selectedID] = [], hover = [] } = placesModule.getEventStates();
placesModule.clearEventStates({ states: ['hover'] });

A pin draws its state, and so do its entry points. A show() of new results drops the states. To keep a selection across a re-show, read it first and put it back:

const [selectedID] = placesModule.getEventStates().click ?? [];
await placesModule.show(morePlaces);
if (selectedID) placesModule.setEventState({ id: selectedID, state: 'click' });

Multiple module instances

Each PlacesModule.create call returns an independent instance with its own configuration, data, events and layers, so different sets of places can look and behave differently:

const restaurantPlaces = await PlacesModule.create(map, {
markerType: 'pin',
icon: {
categoryIcons: [{ id: 'RESTAURANT', image: '/icons/restaurant.png', pixelRatio: 2 }]
}
});
const hotelPlaces = await PlacesModule.create(map, {
markerType: 'circle-icon',
icon: {
categoryIcons: [{ id: 'HOTEL_OR_MOTEL', image: '/icons/hotel.png', pixelRatio: 2 }]
}
});
await restaurantPlaces.show(restaurantResults);
await hotelPlaces.show(hotelResults);

Reading shown data

getShown() returns the places passed to show() as drawn, each with its display properties (title, iconID…) and current eventState, and the connections passed to showConnections():

const shown = placesModule.getShown();
console.log(`Showing ${shown.places.features.length} places`);

Identity across show, clear and style changes

The table shows what happens to the shown places, their event states and their connections when you show again, clear or change the style:

CallPlacesEvent statesConnections
show(results)Replaced, never mergedDropped, even for a place shown again under the same idDropped
show(placesModule.getShown().places)Shown againKept, because they travel on each feature’s properties.eventStateDropped
clear()RemovedRemovedDropped
applyConfig, updateConfig, setKnob, resetKnobKeptKeptKept, ids resolved again
setStyleKeptKeptKept, ids resolved again
setStyle(style, { resetState: true })RemovedRemovedRemoved

So when your app searches, selects and searches again, put the selection back by id after the new show():

// `selectedID` is the id your app kept when the place was clicked
await placesModule.show(await discoverPlaces({ query: 'coffee', geoBias: { position } }));
placesModule.setEventState({ id: selectedID, state: 'click' }); // does nothing if the id is gone
  • A connection endpoint given as a place id resolves against the shown places each time the connections are drawn. The module skips one that matches no shown place, but it stays in getShown().connections.
  • Since show() and clear() drop the connections, none outlives the places it referred to, and none comes back when those places are shown again. Call showConnections() after show().

API reference

PlacesModule lists every method, and PlacesModuleConfig every setting.

Services integration

  • Search - Fuzzy, geometry and along-route search, whose results show() takes as they are
  • Geocoding - Addresses to places
  • Reverse geocoding - A position to the address there
  • Place details - Opening hours and more for a clicked place
  • POIs - Filter the map’s own POIs by category, and draw a category with an icon of your own
  • User interaction events - Event types, cursors and event states across modules