Skip to main content

Filter features within map view

This example uses queryRenderedFeatures to restrict a list of features in a vector tile layer to only return those that are visible in the map view.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Filter features within map view</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<style>
#map {
position: absolute;
left: 25%;
top: 0;
bottom: 0;
width: 75%;
}
.map-overlay {
position: absolute;
width: 25%;
top: 0;
bottom: 0;
left: 0;
font:
12px/20px 'Helvetica Neue',
Arial,
Helvetica,
sans-serif;
background-color: #efefef;
max-height: 100%;
overflow: hidden;
}

.map-overlay fieldset {
display: none;
background: #ddd;
border: none;
padding: 10px;
margin: 0;
}

.map-overlay input {
display: block;
border: none;
width: 100%;
border-radius: 3px;
padding: 10px;
margin: 0;
box-sizing: border-box;
}

.map-overlay .listing {
overflow: auto;
max-height: 100%;
}

.map-overlay .listing > * {
display: block;
padding: 5px 10px;
margin: 0;
}

.map-overlay .listing a {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
color: #404;
text-decoration: none;
}

.map-overlay .listing a:last-child {
border: none;
}

.map-overlay .listing a:hover {
background: #f0f0f0;
}
</style>

<div id="map"></div>

<div class="map-overlay">
<fieldset>
<input id="feature-filter" type="text" placeholder="Filter results by name">
</fieldset>
<div id="feature-listing" class="listing"></div>
</div>

<script>
// TO MAKE THE MAP APPEAR YOU MUST
// ADD YOUR ACCESS TOKEN FROM
// https://account.mapbox.com
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
// Choose from Mapbox's core styles, or make your own style with Mapbox Studio
style: 'mapbox://styles/mapbox/streets-v12',
center: [-98, 38.88],
maxZoom: 5,
minZoom: 1,
zoom: 3
});

// Holds visible airport features for filtering
let airports = [];

// Create a popup, but don't add it to the map yet.
const popup = new mapboxgl.Popup({
closeButton: false
});

const filterEl = document.getElementById('feature-filter');
const listingEl = document.getElementById('feature-listing');

function renderListings(features) {
const empty = document.createElement('p');
// Clear any existing listings
listingEl.innerHTML = '';
if (features.length) {
for (const feature of features) {
const itemLink = document.createElement('a');
const label = `${feature.properties.name} (${feature.properties.abbrev})`;
itemLink.href = feature.properties.wikipedia;
itemLink.target = '_blank';
itemLink.textContent = label;
itemLink.addEventListener('mouseover', () => {
// Highlight corresponding feature on the map
popup
.setLngLat(feature.geometry.coordinates)
.setText(label)
.addTo(map);
});
listingEl.appendChild(itemLink);
}

// Show the filter input
filterEl.parentNode.style.display = 'block';
} else if (features.length === 0 && filterEl.value !== '') {
empty.textContent = 'No results found';
listingEl.appendChild(empty);
} else {
empty.textContent = 'Drag the map to populate results';
listingEl.appendChild(empty);

// Hide the filter input
filterEl.parentNode.style.display = 'none';

// remove features filter
map.setFilter('airport', ['has', 'abbrev']);
}
}

function normalize(string) {
return string.trim().toLowerCase();
}

// Because features come from tiled vector data,
// feature geometries may be split
// or duplicated across tile boundaries.
// As a result, features may appear
// multiple times in query results.
function getUniqueFeatures(features, comparatorProperty) {
const uniqueIds = new Set();
const uniqueFeatures = [];
for (const feature of features) {
const id = feature.properties[comparatorProperty];
if (!uniqueIds.has(id)) {
uniqueIds.add(id);
uniqueFeatures.push(feature);
}
}
return uniqueFeatures;
}

map.on('load', () => {
map.addSource('airports', {
'type': 'vector',
'url': 'mapbox://mapbox.04w69w5j'
});
map.addLayer({
'id': 'airport',
'source': 'airports',
'source-layer': 'ne_10m_airports',
'type': 'circle',
'paint': {
'circle-color': '#4264fb',
'circle-radius': 4,
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
});

map.on('movestart', () => {
// reset features filter as the map starts moving
map.setFilter('airport', ['has', 'abbrev']);
});

map.on('moveend', () => {
const features = map.queryRenderedFeatures({ layers: ['airport'] });

if (features) {
const uniqueFeatures = getUniqueFeatures(features, 'iata_code');
// Populate features for the listing overlay.
renderListings(uniqueFeatures);

// Clear the input container
filterEl.value = '';

// Store the current features in sn `airports` variable to
// later use for filtering on `keyup`.
airports = uniqueFeatures;
}
});

map.on('mousemove', 'airport', (e) => {
// Change the cursor style as a UI indicator.
map.getCanvas().style.cursor = 'pointer';

// Populate the popup and set its coordinates based on the feature.
const feature = e.features[0];
popup
.setLngLat(feature.geometry.coordinates)
.setText(
`${feature.properties.name} (${feature.properties.abbrev})`
)
.addTo(map);
});

map.on('mouseleave', 'airport', () => {
map.getCanvas().style.cursor = '';
popup.remove();
});

filterEl.addEventListener('keyup', (e) => {
const value = normalize(e.target.value);

// Filter visible features that match the input value.
const filtered = [];
for (const feature of airports) {
const name = normalize(feature.properties.name);
const code = normalize(feature.properties.abbrev);
if (name.includes(value) || code.includes(value)) {
filtered.push(feature);
}
}

// Populate the sidebar with filtered results
renderListings(filtered);

// Set the filter to populate features into the layer.
if (filtered.length) {
map.setFilter('airport', [
'match',
['get', 'abbrev'],
filtered.map((feature) => {
return feature.properties.abbrev;
}),
true,
false
]);
}
});

// Call this function on initialization
// passing an empty array to render an empty state
renderListings([]);
});
</script>

</body>
</html>
This code snippet will not work as expected until you replace YOUR_MAPBOX_ACCESS_TOKEN with an access token from your Mapbox account.
Was this example helpful?