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.
What this 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, drawn by the Routing module instead. The base map underneath belongs to the Styling Foundations module; markers given no 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.
Two features of the module have a guide of their own:
| Guide | Covers |
|---|---|
| Place styling | Marker types and clusters, colour, labels, icons, extra properties, layer positioning and the knob catalogue |
| Connections and entry points | Labelled 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, with no fields to map first:
search, geocoding, reverse geocoding, place details and EV availability results, or your own GeoJSON
points. Three places services return something else:
| Result | Not showable because | Show instead |
|---|---|---|
getSearchSuggestions |
Suggestions to choose from, without geometry | A place suggestion's resolve(), or a brand or category suggestion's refine() |
geometryData |
Polygons | The Geometries module |
evChargingStationsAvailability |
Availability alone, no place | getPlacesWithEVAvailability, over the stations a search found |
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);
- Every property of the place is kept.
idis copied intoproperties, andtitle,iconID, the extra properties and, underbase-mapandpin-clustered,categoryandgroupare added. buildPlaceTitleis the default label, the POI name else the address, to fall back on in alabel.titlefunction of your own.toPlacesturns any of the three shapesshow()accepts into one collection, such as the one ashown-featureshandler receives.- An
iconIDnaming an image the module adds, 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
Your own GeoJSON points go to show() as they are, and draw like the places a search returns. Each
keeps its own properties, which label.title, layers overrides and click handlers read back. Name
their shape on create to have the label.title and icon.mapping callbacks typed for it:
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 a layer of your own fed by
preparePlacesForDisplay. The module adds the images its icons name, and brings the events, clustering and restoration after a style change with it. icon.mappinggives 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 point it returnsundefinedfor keeps the icon it would have without a mapping. Route waypoints take the samemappingandlabel.title.- The default label is
poi.name, elseaddress.freeformAddress. Points carrying neither need alabel.titlefunction.
Showing and hiding
setVisible hides and shows everything the module draws — places, connections
and entry points — while keeping its data. visible: false at creation starts it hidden.
const placesModule = await PlacesModule.create(map, { visible: false });
await placesModule.show(searchResults); // loaded, not drawn
placesModule.setVisible(true);
placesModule.isVisible(); // true
show()andclear()never change it, and it survives a style change.- Showing the module leaves hidden the layers its marker type does not use, and any layer you hid
through
layers.*.layout.visibility, such asmainin 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— the pins alone, typed asPlace, which is what you want in TypeScript.events.connections— the lines and labels drawn byshowConnections.events.entryPoints— the entry points.
placesModule.events.places.on('click', (place) => console.log(place.properties.poi?.name));
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: 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. What it shows, how it looks and how it closes are your app's,
so the SDK leaves them to it: a MapLibre
Popup, or a panel of your own beside the map. - A place already has its anchor. The click hands over the place, whose coordinates a
Popupsits on. The Routing module draws its own route summary bubbles because a route has no single point to anchor one to: placing them along the line takes the module. - 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 carry 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
What happens to the shown places, their event states and their connections when you show again, clear or change the style:
| Call | Places | Event states | Connections |
|---|---|---|---|
show(results) |
Replaced, never merged | Dropped, even for a place shown again under the same id | Dropped |
show(placesModule.getShown().places) |
Shown again | Kept: they travel on each feature's properties.eventState |
Dropped |
clear() |
Removed | Removed | Dropped |
applyConfig, updateConfig, setKnob, resetKnob |
Kept | Kept | Kept, ids resolved again |
setStyle |
Kept | Kept | Kept, ids resolved again |
setStyle(style, { resetState: true }) |
Removed | Removed | Removed |
So in a search, select, search again loop, 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 places shown each time the
connections are drawn; one that matches no shown place is skipped, yet stays in
getShown().connections. - Since
show()andclear()drop the connections, none outlives the places it referred to, and none comes back when those places are shown again: callshowConnections()aftershow().
API reference
PlacesModule lists every method, and
PlacesModuleConfig every setting.
Related guides and examples
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
Related examples
- Search playground - Search for places and display results interactively
- Pin interaction - Click events on places, with popups of your own
- Places in geometry - Places searched inside an area's outline
- Places by category in words - A category named in words, in five languages, searched inside a city outline
- Bring your own data: Yorkshire heatmap - An open-data GeoJSON file drawn as base-map places, with
icon.mappingand alabel.titleof its own
Related map modules
- 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