Previous Article
CodeIgniter 4 Tutorial for Beginners (and Why CodeIgniter 3 Is Done)
13 July 2018
Asking the browser where someone is and drawing a route from there to a destination is a common feature — store locators, delivery estimates, "directions to us" pages. The code that circulates for it is mostly from before 2018 and no longer runs.
This guide covers what actually works now: the current Maps loader, the permission model, and what to do when a user says no.
If you have copied a snippet like this, it cannot work:
<!-- Broken for years --> <script src="http://maps.google.com/maps/api/js?sensor=true"></script>
sensor parameter was removed in 2013. It is ignored now, but its presence marks the snippet's age.navigator.geolocation from insecure origins in version 50. On http:// the object is simply not there — which is also why it works on localhost (treated as secure) and then fails on a staging server.Google's current approach is the bootstrap loader with importLibrary(), which fetches libraries on demand instead of pulling everything up front:
<script>
(g => { let h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary",
q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {});
const d = b.maps || (b.maps = {}), r = new Set(), e = new URLSearchParams(),
u = () => h || (h = new Promise(async (f, n) => {
a = m.createElement("script"); e.set("libraries", [...r] + "");
for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]);
e.set("callback", c + ".maps." + q);
a.src = `https://maps.${c}apis.com/maps/api/js?` + e;
d[q] = f; a.onerror = () => h = n(Error(p + " could not load."));
m.head.append(a);
}));
d[l] ? console.warn(p + " only loads once. Ignoring:", g)
: d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n));
})({ key: "YOUR_API_KEY", v: "weekly" });
</script>
Then load only the libraries you need, when you need them:
const { Map } = await google.maps.importLibrary('maps');
const { DirectionsService, DirectionsRenderer } = await google.maps.importLibrary('routes');
const { Geocoder } = await google.maps.importLibrary('geocoding');
Your key is visible in page source — that is unavoidable. Protect it in the Google Cloud Console instead:
Never put a key with Directions or Geocoding server-side permissions in client code — those are billed per request and are the ones worth stealing.
function getPosition(options = {}) {
return new Promise((resolve, reject) => {
if (!('geolocation' in navigator)) {
reject(new Error('unsupported'));
return;
}
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10_000,
maximumAge: 60_000, // accept a cached fix up to a minute old
...options,
});
});
}
The three options each matter. enableHighAccuracy asks for GPS rather than a network estimate — more precise, slower, and heavier on battery. timeout prevents hanging forever when a fix never arrives, which happens indoors. maximumAge lets the browser return a recent cached position instantly instead of powering up the GPS again.
This is the part most implementations skip. Users decline location prompts often, and a page that just breaks is worse than one that never asked.
async function fillFromLocation(inputId) {
const input = document.getElementById(inputId);
const status = document.getElementById('status');
status.textContent = 'Finding your location…';
try {
const position = await getPosition();
const { latitude, longitude } = position.coords;
const { Geocoder } = await google.maps.importLibrary('geocoding');
const { results } = await new Geocoder().geocode({
location: { lat: latitude, lng: longitude },
});
if (!results.length) throw new Error('no-address');
input.value = results[0].formatted_address;
status.textContent = '';
} catch (err) {
status.textContent = describeError(err);
input.focus(); // fall back to typing an address
}
}
function describeError(err) {
switch (err.code) {
case 1: return 'Location access was blocked. Type an address instead, or enable location in your browser settings.';
case 2: return 'Your location could not be determined. Please type an address.';
case 3: return 'Finding your location took too long. Please type an address.';
default: return 'Type an address to continue.';
}
}
Codes 1, 2 and 3 are PERMISSION_DENIED, POSITION_UNAVAILABLE and TIMEOUT. Distinguishing them lets you give advice that actually helps rather than a generic failure.
Only request location from a user gesture. Prompting on page load gets denied — and once denied, the browser will not ask again. Put it behind a "Use my location" button, exactly as the form below does.
<form id="route-form">
<div class="field">
<label for="from">From</label>
<input type="text" id="from" name="from" required autocomplete="street-address" />
<button type="button" data-locate="from">Use my location</button>
</div>
<div class="field">
<label for="to">To</label>
<input type="text" id="to" name="to" required />
</div>
<button type="submit">Get directions</button>
<p id="status" role="status" aria-live="polite"></p>
</form>
<div id="map" style="height: 480px"></div>
<div id="directions-panel"></div>
let map, directionsService, directionsRenderer;
async function initMap() {
const { Map } = await google.maps.importLibrary('maps');
const { DirectionsService, DirectionsRenderer } =
await google.maps.importLibrary('routes');
map = new Map(document.getElementById('map'), {
zoom: 10,
center: { lat: 12.9716, lng: 77.5946 }, // Bengaluru
mapId: 'YOUR_MAP_ID', // required for vector maps
});
directionsService = new DirectionsService();
// Create the renderer ONCE and reuse it. Creating a new one per search
// leaves every previous route drawn on the map.
directionsRenderer = new DirectionsRenderer({
map,
panel: document.getElementById('directions-panel'),
});
}
async function calculateRoute(from, to) {
const status = document.getElementById('status');
status.textContent = 'Calculating route…';
try {
const result = await directionsService.route({
origin: from,
destination: to,
travelMode: google.maps.TravelMode.DRIVING,
unitSystem: google.maps.UnitSystem.METRIC,
provideRouteAlternatives: true,
});
directionsRenderer.setDirections(result);
const leg = result.routes[0].legs[0];
status.textContent = `${leg.distance.text} — about ${leg.duration.text}`;
} catch (err) {
status.textContent = err.code === 'ZERO_RESULTS'
? 'No route found between those places.'
: 'Could not calculate a route. Check the addresses and try again.';
}
}
document.getElementById('route-form').addEventListener('submit', (e) => {
e.preventDefault();
calculateRoute(document.getElementById('from').value, document.getElementById('to').value);
});
document.querySelectorAll('[data-locate]').forEach((btn) => {
btn.addEventListener('click', () => fillFromLocation(btn.dataset.locate));
});
initMap();
Two changes from the old code worth calling out. route() now returns a promise, so the callback-with-status-check pattern is gone. And google.maps.DirectionsTravelMode is the legacy alias — the current name is google.maps.TravelMode.
The single reused DirectionsRenderer is a real bug fix. The original constructed a new one inside the route callback on every search, so each new route was drawn on top of the last and none were ever cleared.
The Geolocation API's accuracy varies enormously by device and context:
| Source | Typical accuracy | When used |
|---|---|---|
| GPS | 5–20 m | Mobile, outdoors, high accuracy on |
| Wi-Fi positioning | 20–100 m | Indoors, urban |
| Cell tower | 100 m – 3 km | Mobile without GPS lock |
| IP address | City to country | Desktop, no other signal |
Always read position.coords.accuracy — it is a radius in metres. If it comes back as 3,000, do not present the result as a precise address. Show the resolved area and let the user correct it.
Maps is billed per map load, per geocode and per directions request. There is a monthly free allowance, but a busy store locator can exceed it. Practical reductions:
Precise location is sensitive personal data. Request it only when the feature genuinely needs it, explain why before triggering the prompt, do not store coordinates unless you have a reason, and always provide a manual address route. A user who declines should be able to complete the same task by typing.
localhost).DirectionsRenderer rather than creating one per search.importLibrary() and the promise-based route().coords.accuracy before trusting a fix.Advertisement
Written by
Amit VermaFounder / Senior Software Engineer
Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.
More articles by AmitYou might also like
We ship software that scales. Let's work together.