Maps SDK for JavaScript
Back to all examples
EV charging stations search
Interactive exploration of charging station availability
import { type EVChargingStationWithAvailabilityPlaceProps, 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 { geometryData, getPlacesWithEVAvailability, getPlaceWithEVAvailability, hasChargingAvailability, search, } from '@tomtom-org/maps-sdk/services'; import { bboxPolygon, difference } from '@turf/turf'; import { without } from 'lodash-es'; import { type LngLatBoundsLike, Popup } from 'maplibre-gl'; import './style.css'; import { ViewportPlaces } from '@tomtom-org/maps-sdk-plugin-viewport-places'; import { API_KEY } from './config'; import { connectorsHTML } from './htmlTemplates'; // (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: 'sdk-example-maps-sdk-js-popup', }); const map = new TomTomMap({ mapLibre: { 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 placesLayers = new ViewportPlaces(map); let mapEVStationsModule: PlacesModule | null = null; const buildAvailabilityText = (place: Place<EVChargingStationWithAvailabilityPlaceProps>): string => { const availability = getChargingPointAvailability(place); return availability ? `${availability.availableCount}/${availability.totalCount}` : ''; }; const evStationPinConfig: PlacesModuleConfig = { extraFeatureProps: { availabilityText: buildAvailabilityText, availabilityRatio: (place: Place<EVChargingStationWithAvailabilityPlaceProps>) => 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 mapEVStationsId = 'ev-stations'; const showPopup = (evStation: Place | Place<EVChargingStationWithAvailabilityPlaceProps>) => { const { address, poi, chargingPark } = evStation.properties; popUp .setHTML( ` <h3>${poi?.name}</h3> <span class="sdk-example-address">${address.freeformAddress}</span> <br/><br/> ${chargingPark ? connectorsHTML(chargingPark) : 'Charging park data not available.'} `, ) .setLngLat(evStation.geometry.coordinates as [number, number]) .addTo(map.mapLibreMap); }; // 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 getPlacesWithEVAvailability(places)); }; const clear = () => { evBrandTextBox.value = ''; areaTextBox.value = ''; popUp.remove(); mapSearchedEVStationsModule.clear(); selectedEVStationModule.clear(); mapGeometryModule.clear(); mapBasePOIs.setVisible(true); }; const selectEVStation = (evStation: Place | Place<EVChargingStationWithAvailabilityPlaceProps>) => { selectedEVStationModule.show(evStation); showPopup(evStation); }; const listenToMapUserEvents = async () => { mapEVStationsModule?.events.on('click', async (evStation) => selectEVStation((await getPlaceWithEVAvailability(evStation)) ?? evStation), ); mapSearchedEVStationsModule.events.on('click', async (evWithAvailability) => selectEVStation(evWithAvailability), ); popUp.on('close', () => selectedEVStationModule.clear()); }; const listenToHTMLUserEvents = () => { const searchButton = document.querySelector('#sdk-example-searchButton') as HTMLButtonElement; searchButton.addEventListener('click', searchEVStations); (document.querySelector('#sdk-example-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( '#sdk-example-minPowerKWMapEVStations', ) as HTMLInputElement; minPowerKWMapEVStationsInput.value = String(minPowerKWMapEVStations); const minPowerKWSearchedEVStationsInput = document.querySelector( '#sdk-example-minPowerKWSearchedEVStations', ) as HTMLInputElement; minPowerKWSearchedEVStationsInput.value = String(minPowerKWSearchedEVStations); minPowerKWMapEVStationsInput.addEventListener('keyup', async () => { minPowerKWMapEVStations = Number(minPowerKWMapEVStationsInput.value); await placesLayers.update({ id: mapEVStationsId, searchOptions: { minPowerKW: minPowerKWMapEVStations } }); }); minPowerKWSearchedEVStationsInput.addEventListener( 'keyup', () => (minPowerKWSearchedEVStations = Number(minPowerKWSearchedEVStationsInput.value)), ); }; const getChargingPointAvailability = ( place: Place | Place<EVChargingStationWithAvailabilityPlaceProps>, ): { availableCount: number; totalCount: number; ratio: number } | undefined => { const chargingPark = place.properties.chargingPark; if (hasChargingAvailability(chargingPark)) { const availability = chargingPark.availability.chargingPointAvailability; const available = availability.statusCounts.Available ?? 0; return { availableCount: available, totalCount: availability.count, ratio: available / availability.count, }; } return undefined; }; mapEVStationsModule = await placesLayers.addPOICategories({ id: mapEVStationsId, categories: ['ELECTRIC_VEHICLE_STATION'], minZoom: 7, }); 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
EV charging stations custom display
Customize EV charging station icons, text, and availability display
Places and Search
Electric Vehicles
Customization
Web
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