I have a map project made on MaplibreGl.
It's a map with a list on the left, as in the mapbox example, clusters, and a filter based on radio buttons.
In this project, I'd like to be able to color cluster icons based on a property contained in my geoJson source. I managed to color the markers in my 'unclustered-point' layer.
I was able to consult a few examples on the subject, without managing to make them work:
changing cluster colors dynamically
Color Cluster based on GeoJSON Feature
Dynamic styling of cluster depending on a feature in it
These examples give me the impression that it is quite possible from a single source.
1- Is there an easy way to change the color of the clusters either by using the clusterProperties
attribute in the map.addSource
function or by using the paint
and circle-color
attribute in the "cluster" layer?
2-If it is preferable to go through several sources, then how do I manage the listing and filters?
Should we also duplicate the layers ('cluster', 'cluster count' and 'unclustered-point')?
Is it possible to do something like setData(stores1,store2 etc); map.getSource('stores1','stores2','stores3') in a filtering function?
Here is a working example with a single source. It was made largely with the help of TomazicM, especially for the filters. So my idea here would be to succeed in coloring the cluster icons: region 1 in red and region 2 in green. The default color of the other clusters would be blue (here region 3)
I tried to create the three sources. The problem is that I duplicate the layers, so instead of having 3, I end up with 9 layers. And the same goes for the filter functions and the listing, as they are based on a single source, I tried to duplicate them... And that's where it all gets complicated...
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Add a raster image to a map layer</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<link href="https://unpkg.com/[email protected]/dist/maplibre-gl.css" rel="stylesheet">
<script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.0/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js"></script>
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
.sidebar {
position:absolute;
top:0;
left:0;
width:25%;
background:white;
}
#map{
position:absolute;
top:0;
left:25%;
width:75%
}
.top {
position: absolute;
top: 0;
right: 0;
background:white;
}
</style>
</head>
<body>
<div style="padding:100px 0"><h1>My title</h1></div>
<div style="position:relative;height:100%">
<div class="sidebar">
<div class="heading font-body text-title-4 font-title text-primary1">
<p>Hospitals</p>
</div>
<div id="listings" class="listings font-body"></div>
</div>
<div id="map" class="map pad2"></div>
<div class="top">
<div class="map-overlay-inner">
<nav id="nav-filter">
<fieldset>
<legend>Région</legend>
<div>
<input id="all" type="radio" name="radio" onclick="filterMarkers()"/>
<label for="all">All</label>
</div>
<div>
<input id="region1" type="radio" name="radio" onclick="filterMarkers(this)"/>
<label for="region1">region 1</label>
</div>
<div>
<input id="region2" type="radio" name="radio" onclick="filterMarkers(this)"/>
<label for="region2">region 2</label>
</div>
<div>
<input id="region3" type="radio" name="radio" onclick="filterMarkers(this)"/>
<label for="region3">region 3</label>
</div>
</fieldset>
</nav>
</div>
</div>
</div>
<div style="padding:100px 0"><h2>My footer</h2></div>
<script>
var map = new maplibregl.Map({
container: 'map', // container id
style: {
'version': 8,
'sources': {
'raster-tiles': {
'type': 'raster',
'tiles': [
"https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png",
"https://b.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png",
"https://c.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png",
"https://d.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png"
],
'tileSize': 256,
'attribution':
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>'
}
},
"glyphs": "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf",
'layers': [
{
'id': 'simple-tiles',
'type': 'raster',
'source': 'raster-tiles',
'minzoom': 0,
'maxzoom': 22
}
]
},
center: [2.3514998, 48.85661], // starting position
zoom: 5, // starting zoom
cooperativeGestures: true
});
var stores =
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
5.8600224,
48.6805776
]
},
"properties": {
"name": "house 3",
"region": "region 2",
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
6.099311399999999,
48.6143564
]
},
"properties": {
"name": "house 2",
"region": "region 2",
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
6.1611808,
48.6718265
]
},
"properties": {
"name": "house1",
"region": "region 2",
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-0.5193906,
45.9452769
]
},
"properties": {
"name": "house 6",
"region": "region 1"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-1.0026331,
45.62014
]
},
"properties": {
"name": "house 5",
"region": "region 1"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
0.1955502,
46.0287897
]
},
"properties": {
"name": "house 4",
"region": "region 1"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
3.689009,
43.4474489
]
},
"properties": {
"name": "house 9",
"region": "region 3"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
3.9077075,
43.62659379999999
]
},
"properties": {
"name": "house 8",
"region": "region 3"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
3.8874525,
43.5972365
]
},
"properties": {
"name": "house 7",
"region": "region 3"
}
}
]
};
stores.features.forEach((store, i) => {
store.properties.id = i;
});
function getLabel(id) {
return $("#"+id).next("label").html();
}
var regions = {};
const regionCheckboxes = document.querySelectorAll('input[type=radio]');
regionCheckboxes.forEach(checkbox => {
const region = getLabel(checkbox.id);
if (region != 'All') {
var filteredFeatures = stores.features.filter(feature => (feature.properties.region == region));
var bbox = turf.bbox(turf.featureCollection(filteredFeatures));
regions[checkbox.id] = bbox;
}
});
map.on('load', () => {
map.addSource('stores', {
'type': 'geojson',
'data': stores,
'cluster': true,
'clusterMaxZoom': 14, // Max zoom to cluster points on
'clusterRadius': 50
});
// addMarkers();
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'stores',
filter: ['has', 'point_count'],
paint: {
'circle-color': [
'step',
['get', 'point_count'],
'#525CA3',
100,
'#525CA3',
750,
'#525CA3'
],
'circle-radius': [
'step',
['get', 'point_count'],
20,
100,
30,
750,
40
]
}
});
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'stores',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['Open Sans Bold'],
'text-size': 12,
"icon-image": "{marker-symbol}",
"icon-size": 0.20
},
paint: {
"text-color": "#ffffff"
}
});
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'stores',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#525CA3',
'circle-radius': 15,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
});
buildLocationList(stores);
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['clusters']
});
const clusterId = features[0].properties.cluster_id;
map.getSource('stores').getClusterExpansionZoom(
clusterId,
(err, zoom) => {
if (err) return;
map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom
});
}
);
});
map.on('click', 'unclustered-point', (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const region = e.features[0].properties.region;
const name = e.features[0].properties.name;
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
for (const marker of stores.features) {
const activeItem = document.getElementsByClassName('active');
if (activeItem[0]) {
activeItem[0].classList.remove('active');
}
const listing = document.getElementById(
`listing-${e.features[0].properties.id}`
);
listing.classList.add('active');
listing.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
};
new maplibregl.Popup({ closeOnClick: true })
.setLngLat(coordinates)
.setHTML(`<h3>${listing.properties.name}</h3><h4>${listing.properties.region}</h4>`)
.addTo(map);
});
map.on('mouseenter', 'clusters', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'clusters', () => {
map.getCanvas().style.cursor = '';
});
});
function buildLocationList(stores) {
for (const store of stores.features) {
const listings = document.getElementById('listings');
const listing = listings.appendChild(document.createElement('div'));
listing.id = `listing-${store.properties.id}`;
listing.className = 'item';
const link = listing.appendChild(document.createElement('a'));
link.href = 'javascript:void(0)';
link.className = 'title';
link.id = `link-${store.properties.id}`;
link.innerHTML = `<h3 style="margin:0 0 10px">${store.properties.name}</h3>`;
const details = listing.appendChild(document.createElement('div'));
if(store.properties.region) {
details.innerHTML += `<p style="margin:0">${store.properties.region }</p>`
}
link.addEventListener('click', function () {
for (const feature of stores.features) {
if (this.id === `link-${feature.properties.id}`) {
flyToStore(feature);
createPopUp(feature);
}
}
const activeItem = document.getElementsByClassName('active');
if (activeItem[0]) {
activeItem[0].classList.remove('active');
}
this.parentNode.classList.add('active');
});
}
}
function flyToStore(currentFeature) {
map.flyTo({
center: currentFeature.geometry.coordinates,
zoom: 15
});
}
function createPopUp(currentFeature) {
const popUps = document.getElementsByClassName('maplibregl-popup');
if (popUps[0]) popUps[0].remove();
const popup = new maplibregl.Popup({ closeOnClick: true })
.setLngLat(currentFeature.geometry.coordinates)
.setHTML(
`'<h3>${currentFeature.properties.name}</h3><h4>${currentFeature.properties.region}</h4>`
)
.addTo(map);
}
const nav = new maplibregl.NavigationControl({
visualizePitch: true
});
map.addControl(nav, 'bottom-right');
function getSelectedRegions() {
const selectedRegions = [];
const regionCheckboxes = document.querySelectorAll('input[type=radio]:checked');
regionCheckboxes.forEach(checkbox => selectedRegions.push(getLabel(checkbox.id)));
return selectedRegions;
}
function filterMarkers(button) {
const selectedRegions = getSelectedRegions();
const mapSource = map.getSource('stores');
if (selectedRegions[0] == 'All') {
setAllMarkersVisible(mapSource);
return;
}
const filteredFeatures = stores.features.filter(feature => selectedRegions.includes(feature.properties.region));
mapSource.setData({
...stores,
features: filteredFeatures
});
map.fitBounds(regions[button.id], {
padding: {top: 50, bottom: 50, left: 50, right: 50}
});
}
function setAllMarkersVisible(mapSource) {
mapSource.setData(stores);
map.flyTo({
center: [2.3514998, 48.85661], // starting position
zoom: 5, // starting zoom
});
}
</script>
</body>
</html>