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:
| 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 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:
| 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 |
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
idintoproperties. It addstitle,iconIDand the extra properties, pluscategoryandgroupunderbase-mapandpin-clustered. buildPlaceTitlebuilds the default label (the POI name, otherwise the address). Fall back on it in your ownlabel.titlefunction.toPlacesturns any of the three shapesshow()accepts into one collection, such as the one ashown-featureshandler receives.- An
iconIDfor 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.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. AcategoryIconsentry that matches that category draws its icon instead. 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, otherwiseaddress.freeformAddress. Points with neither need alabel.titlefunction.
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()andclear()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 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- Only the pins, typed asPlace, which is what you want in TypeScript.events.connections- The lines and labels drawn byshowConnections.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
Popupsits 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:
| 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, because 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 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()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
- Search places within 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
- Show your own GeoJSON as places - An open-data GeoJSON file drawn as base-map places, with
icon.mappingand alabel.titleof its own - Store locator - Your own stores as places, the nearest to an address ranked by drive time and selected with
setEventState
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