Tutorial
Render live incidents as vector layers in Mapbox GL JS, including the boundary polygons agencies publish for larger fires. This is the heavier of our two mapping guides. If you want a map on a page in fifteen minutes with no account and no token, start with the Leaflet guide instead.
Both render the same data. They are not interchangeable, and picking on familiarity rather than fit is how projects end up fighting their map library.
| Leaflet | Mapbox GL JS | |
|---|---|---|
| Account or token | None | Required, and billed past the free tier |
| Rendering | Raster tiles, DOM markers | Vector tiles, WebGL |
| Hundreds of features | Slows down, markers are DOM nodes | Comfortable |
| Styling by data value | Write the branching yourself | Expressions, evaluated in the style |
| Polygons over a basemap | Works, no fill control to speak of | Fill, outline and opacity per feature |
| Offline or air-gapped | Straightforward | Needs a self-hosted style and tiles |
The short version: Leaflet if you want a map, Mapbox if the map is the product. The one technical reason to reach for Mapbox specifically with this API is polygons, which is the next section.
Most incidents are a point. Larger ones are not. When an agency publishes a fire perimeter or a warning area, DataQuoll carries it through as a boundary on the incident rather than reducing it to a dot, and a 40,000 hectare fire drawn as a single marker tells a reader almost nothing useful about where it is.
Ask for them with includeBoundary=true. They are opt-in because the geometry is large and most callers do not want it on every request.
Get a free DataQuoll key, and a public access token from your Mapbox account dashboard.
Both end up in browser JavaScript, so both need restricting. Scope the Mapbox token to your URL in their dashboard. Restrict the DataQuoll key to your origin in yours, under the key's Origins editor: the key then only answers calls made from a browser on your site, so copying it out of your page source gets somebody nothing.
One file. The API returns GeoJSON, and Mapbox consumes GeoJSON, so the response goes straight into a source with no conversion step.
<!DOCTYPE html>
<html lang="en-AU">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Live Australian emergency incidents</title>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.7.0/mapbox-gl.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.7.0/mapbox-gl.js"></script>
<style>
body { margin: 0; }
#map { position: absolute; inset: 0; }
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN'; // restrict this to your URL
const DATAQUOLL_KEY = 'YOUR_DATAQUOLL_KEY'; // restrict this to your origin
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v11',
center: [133.8, -25.3], // lng, lat -- GeoJSON order, not lat/lng
zoom: 3.6,
});
// The warning levels, worst first. Used by BOTH layers below so a polygon and
// its marker can never disagree about what colour "watch and act" is.
const LEVEL_COLOURS = [
'match', ['get', 'warningLevel'],
'emergency_warning', '#b91c1c',
'watch_and_act', '#ea580c',
'advice', '#2563eb',
'#6b7280', // anything else, including none
];
async function loadIncidents() {
// includeBoundary is OPT-IN. Without it every incident is a point, and a
// 40,000 hectare fire renders as one dot in the middle of it.
const params = new URLSearchParams({ includeBoundary: 'true', limit: '500' });
const res = await fetch('https://dataquoll.io/api/v1/incidents?' + params, {
headers: { Authorization: 'Bearer ' + DATAQUOLL_KEY },
});
if (!res.ok) throw new Error('DataQuoll returned ' + res.status);
const geojson = await res.json();
// Boundaries arrive as a "boundary" PROPERTY, not as the feature geometry:
// the geometry stays a point so that markers, clustering and the nearby
// endpoint all keep working. Split them into their own collection here.
const areas = {
type: 'FeatureCollection',
features: geojson.features
.filter((f) => f.properties.boundary)
.map((f) => ({ ...f, geometry: f.properties.boundary })),
};
map.getSource('incidents').setData(geojson);
map.getSource('incident-areas').setData(areas);
}
map.on('load', async () => {
map.addSource('incidents', { type: 'geojson', data: emptyCollection() });
map.addSource('incident-areas', { type: 'geojson', data: emptyCollection() });
// Polygons UNDER the points, so a marker is never hidden by its own fire.
map.addLayer({
id: 'areas-fill',
type: 'fill',
source: 'incident-areas',
paint: { 'fill-color': LEVEL_COLOURS, 'fill-opacity': 0.25 },
});
map.addLayer({
id: 'areas-outline',
type: 'line',
source: 'incident-areas',
paint: { 'line-color': LEVEL_COLOURS, 'line-width': 1.5 },
});
map.addLayer({
id: 'incident-points',
type: 'circle',
source: 'incidents',
paint: {
'circle-color': LEVEL_COLOURS,
// Interpolated by zoom so the map is readable at both ends.
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 3, 10, 8],
'circle-stroke-width': 1,
'circle-stroke-color': '#ffffff',
},
});
map.on('click', 'incident-points', (e) => {
const f = e.features[0];
const p = f.properties;
new mapboxgl.Popup()
.setLngLat(f.geometry.coordinates)
.setHTML(
'<strong>' + p.title + '</strong><br>' +
(p.location ? JSON.parse(p.location).suburb || '' : '') + '<br>' +
'Warning level: ' + (p.warningLevel || 'none')
)
.addTo(map);
});
map.on('mouseenter', 'incident-points', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'incident-points', () => { map.getCanvas().style.cursor = ''; });
await loadIncidents();
setInterval(loadIncidents, 300000); // 5 minutes; see the note on quota below
});
function emptyCollection() {
return { type: 'FeatureCollection', features: [] };
}
</script>
</body>
</html>integrity and crossorigin attributes using the hash Mapbox publishes for the exact version you pinned, or serve the library from your own origin. Take the hash from their release notes rather than from anywhere it has been copied to.[longitude, latitude], which is the reverse of how most people say it. A swapped Australian pair lands in the Indian Ocean and still looks like a valid coordinate, so it is worth checking the first time rather than wondering why the map is empty.location and details need parsing back inside a popup, as above. Top-level strings like warningLevel are fine to use directly in an expression.