Building a Keyless EV Charging Map with MapLibre GL and OpenFreeMap
Render thousands of Indian EV chargers on a responsive map using open-source MapLibre GL JS and OpenFreeMap vector tiles with zero proprietary API keys.
7 min read
## Overview Building a nationwide EV charging station locator using proprietary mapping APIs like Google Maps Platform or Mapbox can quickly run up thousands of dollars in tile requests and geocoding bills.
By pairing MapLibre GL JS (an open-source WebGL vector map library) with OpenFreeMap (free vector map tile hosting) and this keyless EV Charging Station API, you can deploy a zero-cost, high-performance charging map.
1. MapLibre GL + OpenFreeMap setup OpenFreeMap provides production-grade vector tiles hosted on Cloudflare with zero rate limits or registration keys.
<!-- Include MapLibre GL CSS & JS -->
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>Initialize the map container with the OpenFreeMap vector style:
import maplibregl from 'maplibre-gl';const map = new maplibregl.Map({ container: 'map-container', style: 'https://tiles.openfreemap.org/styles/liberty', center: [77.5946, 12.9716], // Bengaluru center [lng, lat] zoom: 11, }); ```
2. Converting API radius responses to GeoJSON When the user pans or searches a location, query `/v1/stations/near` and convert the `data` array into a standard GeoJSON FeatureCollection:
async function loadStations(lat, lng, radiusKm = 15) {
const url = new URL('https://ev-charging-stations-api.vercel.app/v1/stations/near');
url.searchParams.set('lat', lat);
url.searchParams.set('lng', lng);
url.searchParams.set('radius', radiusKm);const res = await fetch(url); const json = await res.json(); if (!json.success) return null;
return { type: 'FeatureCollection', features: json.data.map((station) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [station.coordinates.lng, station.coordinates.lat], }, properties: { id: station.id, operator: station.operator, powerKw: station.max_power_kw, hasDc: station.chargers.some((c) => c.current === 'dc'), address: station.address, }, })), }; } ```
3. Clustered circle rendering When displaying hundreds of chargers in dense urban centers like Delhi NCR or Mumbai, render points using MapLibre's native clustering engine so the browser maintains 60 FPS:
map.on('load', async () => {map.addSource('stations', { type: 'geojson', data: geojson, cluster: true, clusterMaxZoom: 14, clusterRadius: 50, });
// Cluster circles map.addLayer({ id: 'clusters', type: 'circle', source: 'stations', filter: ['has', 'point_count'], paint: { 'circle-color': 'teal', 'circle-radius': ['step', ['get', 'point_count'], 18, 10, 24, 50, 32], }, });
// Cluster numbers map.addLayer({ id: 'cluster-count', type: 'symbol', source: 'stations', filter: ['has', 'point_count'], layout: { 'text-field': '{point_count_abbreviated}', 'text-size': 12, }, paint: { 'text-color': 'white' }, });
// Individual unclustered station points map.addLayer({ id: 'unclustered-point', type: 'circle', source: 'stations', filter: ['!', ['has', 'point_count']], paint: { 'circle-color': ['case', ['get', 'hasDc'], 'teal', 'blue'], 'circle-radius': 7, 'circle-stroke-width': 2, 'circle-stroke-color': 'white', }, }); }); ```