Maps SDK for JavaScript
Back to all examples
Map ev charging stations playground
EV charging station discovery with availability data
import { type ChargingParkWithAvailability, type ConnectorAvailability, type EVChargingStationPlaceProps, geographyTypes, type Place, type PolygonFeatures, TomTomConfig, } from '@tomtom-org/maps-sdk/core'; import { GeometriesModule, PlacesModule, PlacesModuleConfig, POIsModule, TomTomMap } from '@tomtom-org/maps-sdk/map'; import { buildPlacesWithEVAvailability, buildPlaceWithEVAvailability, geometryData, search, } from '@tomtom-org/maps-sdk/services'; import { bboxPolygon, difference } from '@turf/turf'; import { isEmpty, without } from 'lodash-es'; import { type LngLatBoundsLike, NavigationControl, Popup } from 'maplibre-gl'; import { connectorIcons } from './connectorIcons'; import { connectorNames } from './connectorNames'; import genericIcon from './ic-generic-24.svg?raw'; import './style.css'; import { API_KEY } from './config'; // (Set your own API key when working in your own environment) TomTomConfig.instance.put({ apiKey: API_KEY, language: 'en-GB' }); (async () => { const evBrandTextBox = document.querySelector('#sdk-example-evBrandTextBox') as HTMLInputElement; const areaTextBox = document.querySelector('#sdk-example-areaTextBox') as HTMLInputElement; const fitBoundsOptions = { padding: 50 }; const popUp = new Popup({ closeButton: false, offset: 35, className: 'maps-sdk-js-popup', }); const map = new TomTomMap({ container: 'sdk-map', center: [2.3597, 48.85167], zoom: 11, fitBoundsOptions, }); const mapBasePOIs = await POIsModule.get(map, { filters: { categories: { show: 'all_except', values: ['ELECTRIC_VEHICLE_STATION'] }, }, }); const mapEVStations = await PlacesModule.get(map, { theme: 'base-map' }); const buildAvailabilityText = (place: Place<EVChargingStationPlaceProps>): string => { const availability = getChargingPointAvailability(place); return availability ? `${availability.availableCount}/${availability.totalCount}` : ''; }; const evStationPinConfig: PlacesModuleConfig = { extraFeatureProps: { availabilityText: buildAvailabilityText, availabilityRatio: (place: Place) => getChargingPointAvailability(place)?.ratio ?? 0, }, text: { title: [ 'format', ['get', 'title'], {}, '\n', {}, ['get', 'availabilityText'], { 'font-scale': 1.1, 'text-color': [ 'case', ['>=', ['get', 'availabilityRatio'], 0.25], 'green', ['>', ['get', 'availabilityRatio'], 0], 'orange', 'red', ], }, ], }, }; const mapSearchedEVStationsModule = await PlacesModule.get(map, evStationPinConfig); const selectedEVStationModule = await PlacesModule.get(map, evStationPinConfig); const mapGeometryModule = await GeometriesModule.get(map); let minPowerKWMapEVStations = 50; let minPowerKWSearchedEVStations = 0; const connectorsHTML = (chargingPark: ChargingParkWithAvailability): string => { const availability = chargingPark?.availability; const connectorAvailabilities = availability ? availability.connectorAvailabilities : chargingPark.connectorCounts; return `<ul class="sdk-example-connector-ul"> ${connectorAvailabilities .map((connectorAvailability) => { const statusCounts = (connectorAvailability as ConnectorAvailability).statusCounts; const hasStatuses = !isEmpty(statusCounts); const availableCount = statusCounts.Available ?? 0; const connectorType = connectorAvailability.connector.type; const connectorName = connectorNames[connectorType] ?? connectorType; return ` <li class="sdk-example-connector-li"> <div class="sdk-example-connectorIcon">${connectorIcons[connectorType] ?? genericIcon}</div> <label class="sdk-example-connectorName sdk-example-label">${connectorName ?? ''}</label> <label class="sdk-example-connectorPower sdk-example-label"> | ${connectorAvailability.connector.ratedPowerKW} KW</label> <label class="sdk-example-label ${ hasStatuses ? availableCount ? 'sdk-example-available' : 'sdk-example-unavailable' : 'sdk-example-noStatus' }">${hasStatuses ? `${availableCount} / ` : ''}${connectorAvailability.count}</label> </li>`; }) .join('')} </ul>`; }; const showPopup = (evStation: Place<EVChargingStationPlaceProps>) => { const { address, poi, chargingPark } = evStation.properties; popUp .setHTML( ` <h3>${poi?.name}</h3> <label class="sdk-example-address sdk-example-label">${address.freeformAddress}</label> <br/><br/> ${connectorsHTML(chargingPark as ChargingParkWithAvailability)} `, ) .setLngLat(evStation.geometry.coordinates as [number, number]) .addTo(map.mapLibreMap); }; const updateMapEVStations = async () => { const zoom = map.mapLibreMap.getZoom(); if (zoom < 7) { mapEVStations.clear(); } else { const chargingStations = await search({ query: '', poiCategories: ['ELECTRIC_VEHICLE_STATION'], minPowerKW: minPowerKWMapEVStations, boundingBox: map.getBBox(), limit: zoom < 10 ? 50 : 100, }); mapEVStations.show(chargingStations); } }; // inverts the polygon, so it looks like a hole on the map instead const invert = (geometry: PolygonFeatures): PolygonFeatures => { const invertedArea = difference({ type: 'FeatureCollection', features: [bboxPolygon([-180, 90, 180, -90]), geometry?.features?.[0]], }); return invertedArea ? ({ type: 'FeatureCollection', features: [invertedArea], } as PolygonFeatures) : geometry; }; const searchEVStations = async () => { popUp.remove(); mapBasePOIs.setVisible(false); const areaToSearch = areaTextBox.value && (await search({ query: areaTextBox.value, geographyTypes: without(geographyTypes, 'Country'), limit: 1, })); areaToSearch && map.mapLibreMap.fitBounds(areaToSearch.bbox as LngLatBoundsLike, fitBoundsOptions); const geometryToSearch = areaToSearch && (await geometryData({ geometries: areaToSearch })); if (geometryToSearch) { mapGeometryModule.show(invert(geometryToSearch)); } else { mapGeometryModule.clear(); } const places = await search({ query: evBrandTextBox.value, ...(geometryToSearch && { geometries: [geometryToSearch] }), ...(!geometryToSearch && { boundingBox: map.getBBox() }), poiCategories: ['ELECTRIC_VEHICLE_STATION'], minPowerKW: minPowerKWSearchedEVStations, limit: 100, }); // We first show the places, then fetch their EV availability and show them again: mapSearchedEVStationsModule.show(places); mapSearchedEVStationsModule.show(await buildPlacesWithEVAvailability(places)); }; const clear = () => { evBrandTextBox.value = ''; areaTextBox.value = ''; popUp.remove(); mapSearchedEVStationsModule.clear(); selectedEVStationModule.clear(); mapGeometryModule.clear(); mapBasePOIs.setVisible(true); }; const selectEVStation = (evStation: Place<EVChargingStationPlaceProps>) => { selectedEVStationModule.show(evStation); showPopup(evStation); }; const listenToMapUserEvents = async () => { map.mapLibreMap.on('moveend', updateMapEVStations); mapEVStations.events.on('click', async (evStation) => selectEVStation(await buildPlaceWithEVAvailability(evStation)), ); mapSearchedEVStationsModule.events.on('click', async (evWithAvailability) => selectEVStation(evWithAvailability), ); popUp.on('close', () => selectedEVStationModule.clear()); }; const listenToHTMLUserEvents = () => { const searchButton = document.querySelector('#searchButton') as HTMLButtonElement; searchButton.addEventListener('click', searchEVStations); (document.querySelector('#clearButton') as HTMLButtonElement).addEventListener('click', clear); evBrandTextBox.addEventListener('keypress', (event) => event.key === 'Enter' && searchButton.click()); areaTextBox.addEventListener('keypress', (event) => event.key === 'Enter' && searchButton.click()); const minPowerKWMapEVStationsInput = document.querySelector('#minPowerKWMapEVStations') as HTMLInputElement; minPowerKWMapEVStationsInput.value = String(minPowerKWMapEVStations); const minPowerKWSearchedEVStationsInput = document.querySelector( '#minPowerKWSearchedEVStations', ) as HTMLInputElement; minPowerKWSearchedEVStationsInput.value = String(minPowerKWSearchedEVStations); minPowerKWMapEVStationsInput.addEventListener('keyup', async () => { minPowerKWMapEVStations = Number(minPowerKWMapEVStationsInput.value); await updateMapEVStations(); }); minPowerKWSearchedEVStationsInput.addEventListener( 'keyup', () => (minPowerKWSearchedEVStations = Number(minPowerKWSearchedEVStationsInput.value)), ); }; const getChargingPointAvailability = ( place: Place<EVChargingStationPlaceProps>, ): { availableCount: number; totalCount: number; ratio: number } | undefined => { const availability = place.properties.chargingPark?.availability?.chargingPointAvailability; if (availability) { const available = availability.statusCounts.Available ?? 0; return { availableCount: available, totalCount: availability.count, ratio: available / availability.count, }; } return undefined; }; map.mapLibreMap.addControl(new NavigationControl(), 'bottom-right'); await updateMapEVStations(); await listenToMapUserEvents(); listenToHTMLUserEvents(); })();
Related examples
Map autocomplete fuzzy search playground
Combine autocomplete search with fuzzy search functionality
Places and Search
Playground
Web
User Interaction Events
Geometry search playground
Search for places within specific geographic boundaries
Playground
Places and Search
Geometry
Web
Geometry search with POI categories
Geometry search combined with POI category filtering
Playground
Places and Search
Geometry
Web