Display HTML clusters with custom properties
Extend clustering with HTML markers and custom property expressions. Requires Trimble Maps v3.0.0 or later.<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Display HTML clusters with custom properties</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.3.css" /> <script src="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.3.js"></script> <style> body { margin: 0; padding: 0; } html, body, #map { height: 100%; } </style> </head> <body> <div id="map"></div> <script> TrimbleMaps.setAPIKey('YOUR_API_KEY_HERE'); const map = new TrimbleMaps.Map({ container: 'map', zoom: 0.3, center: [0, 20], style: TrimbleMaps.Common.Style.TRANSPORTATION, }); map.addControl(new TrimbleMaps.NavigationControl()); // filters for classifying earthquakes into five categories based on magnitude const mag1 = ['<', ['get', 'mag'], 2]; const mag2 = ['all', ['>=', ['get', 'mag'], 2], ['<', ['get', 'mag'], 3]]; const mag3 = ['all', ['>=', ['get', 'mag'], 3], ['<', ['get', 'mag'], 4]]; const mag4 = ['all', ['>=', ['get', 'mag'], 4], ['<', ['get', 'mag'], 5]]; const mag5 = ['>=', ['get', 'mag'], 5]; // colors to use for the categories const colors = ['#fed976', '#feb24c', '#fd8d3c', '#fc4e2a', '#e31a1c']; map.on('load', () => { // add a clustered GeoJSON source for a sample set of earthquakes map.addSource('earthquakes', { type: 'geojson', data: 'https://developer.trimblemaps.com/maps-sdk/assets/earthquakes.geojson', cluster: true, clusterRadius: 80, clusterProperties: { // keep separate counts for each magnitude category in a cluster mag1: ['+', ['case', mag1, 1, 0]], mag2: ['+', ['case', mag2, 1, 0]], mag3: ['+', ['case', mag3, 1, 0]], mag4: ['+', ['case', mag4, 1, 0]], mag5: ['+', ['case', mag5, 1, 0]], }, }); // circle and symbol layers for rendering individual earthquakes (unclustered points) map.addLayer({ id: 'earthquake_circle', type: 'circle', source: 'earthquakes', filter: ['!=', 'cluster', true], paint: { 'circle-color': [ 'case', mag1, colors[0], mag2, colors[1], mag3, colors[2], mag4, colors[3], colors[4], ], 'circle-opacity': 0.6, 'circle-radius': 12, }, }); map.addLayer({ id: 'earthquake_label', type: 'symbol', source: 'earthquakes', filter: ['!=', 'cluster', true], layout: { 'text-field': [ 'number-format', ['get', 'mag'], {'min-fraction-digits': 1, 'max-fraction-digits': 1}, ], 'text-font': ['Open Sans Regular', 'Noto Sans Regular'], 'text-size': 10, }, paint: { 'text-color': ['case', ['<', ['get', 'mag'], 3], 'black', 'white'], }, }); // objects for caching and keeping track of HTML marker objects (for performance) const markers = {}; let markersOnScreen = {}; function updateMarkers() { const newMarkers = {}; const features = map.querySourceFeatures('earthquakes'); // for every cluster on the screen, create an HTML marker for it (if we didn't yet), // and add it to the map if it's not there already for (let i = 0; i < features.length; i++) { const coords = features[i].geometry.coordinates; const props = features[i].properties; if (!props.cluster) continue; const id = props.cluster_id; let marker = markers[id]; if (!marker) { const el = createDonutChart(props); marker = markers[id] = new TrimbleMaps.Marker({ element: el, }).setLngLat(coords); } newMarkers[id] = marker; if (!markersOnScreen[id]) marker.addTo(map); } // for every marker we've added previously, remove those that are no longer visible for (id in markersOnScreen) { if (!newMarkers[id]) markersOnScreen[id].remove(); } markersOnScreen = newMarkers; } // after the GeoJSON data is loaded, update markers on the screen and do so on every map move/moveend map.on('data', (e) => { if (e.sourceId !== 'earthquakes' || !e.isSourceLoaded) return; map.on('move', updateMarkers); map.on('moveend', updateMarkers); updateMarkers(); }); }); // code for creating an SVG donut chart from feature properties function createDonutChart(props) { const offsets = []; const counts = [ props.mag1, props.mag2, props.mag3, props.mag4, props.mag5, ]; let total = 0; for (let i = 0; i < counts.length; i++) { offsets.push(total); total += counts[i]; } const fontSize = total >= 1000 ? 22 : total >= 100 ? 20 : total >= 10 ? 18 : 16; const r = total >= 1000 ? 50 : total >= 100 ? 32 : total >= 10 ? 24 : 18; const r0 = Math.round(r * 0.6); const w = r * 2; let html = `<div><svg width="${w}" height="${w}" viewbox="0 0 ${w} ${w}" text-anchor="middle" style="font: ${fontSize}px sans-serif; display: block">`; for (i = 0; i < counts.length; i++) { html += donutSegment( offsets[i] / total, (offsets[i] + counts[i]) / total, r, r0, colors[i] ); } html += `<circle cx="${r}" cy="${r}" r="${r0}" fill="white" /><text dominant-baseline="central" transform="translate(${r}, ${r})">${total.toLocaleString()}</text></svg></div>`; const el = document.createElement('div'); el.innerHTML = html; return el.firstChild; } function donutSegment(start, end, r, r0, color) { if (end - start === 1) end -= 0.00001; const a0 = 2 * Math.PI * (start - 0.25); const a1 = 2 * Math.PI * (end - 0.25); const x0 = Math.cos(a0), y0 = Math.sin(a0); const x1 = Math.cos(a1), y1 = Math.sin(a1); const largeArc = end - start > 0.5 ? 1 : 0; return [ '<path d="M', r + r0 * x0, r + r0 * y0, 'L', r + r * x0, r + r * y0, 'A', r, r, 0, largeArc, 1, r + r * x1, r + r * y1, 'L', r + r0 * x1, r + r0 * y1, 'A', r0, r0, 0, largeArc, 0, r + r0 * x0, r + r0 * y0, `" fill="${color}" />`, ].join(' '); } </script> </body> </html>
Display HTML clusters with custom properties
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Display HTML clusters with custom properties</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.3.css" /> <script src="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.3.js"></script> <style> body { margin: 0; padding: 0; } html, body, #map { height: 100%; } </style> </head> <body> <div id="map"></div> <script> TrimbleMaps.setAPIKey('YOUR_API_KEY_HERE'); const map = new TrimbleMaps.Map({ container: 'map', zoom: 0.3, center: [0, 20], style: TrimbleMaps.Common.Style.TRANSPORTATION, }); map.addControl(new TrimbleMaps.NavigationControl()); // filters for classifying earthquakes into five categories based on magnitude const mag1 = ['<', ['get', 'mag'], 2]; const mag2 = ['all', ['>=', ['get', 'mag'], 2], ['<', ['get', 'mag'], 3]]; const mag3 = ['all', ['>=', ['get', 'mag'], 3], ['<', ['get', 'mag'], 4]]; const mag4 = ['all', ['>=', ['get', 'mag'], 4], ['<', ['get', 'mag'], 5]]; const mag5 = ['>=', ['get', 'mag'], 5]; // colors to use for the categories const colors = ['#fed976', '#feb24c', '#fd8d3c', '#fc4e2a', '#e31a1c']; map.on('load', () => { // add a clustered GeoJSON source for a sample set of earthquakes map.addSource('earthquakes', { type: 'geojson', data: 'https://developer.trimblemaps.com/maps-sdk/assets/earthquakes.geojson', cluster: true, clusterRadius: 80, clusterProperties: { // keep separate counts for each magnitude category in a cluster mag1: ['+', ['case', mag1, 1, 0]], mag2: ['+', ['case', mag2, 1, 0]], mag3: ['+', ['case', mag3, 1, 0]], mag4: ['+', ['case', mag4, 1, 0]], mag5: ['+', ['case', mag5, 1, 0]], }, }); // circle and symbol layers for rendering individual earthquakes (unclustered points) map.addLayer({ id: 'earthquake_circle', type: 'circle', source: 'earthquakes', filter: ['!=', 'cluster', true], paint: { 'circle-color': [ 'case', mag1, colors[0], mag2, colors[1], mag3, colors[2], mag4, colors[3], colors[4], ], 'circle-opacity': 0.6, 'circle-radius': 12, }, }); map.addLayer({ id: 'earthquake_label', type: 'symbol', source: 'earthquakes', filter: ['!=', 'cluster', true], layout: { 'text-field': [ 'number-format', ['get', 'mag'], {'min-fraction-digits': 1, 'max-fraction-digits': 1}, ], 'text-font': ['Open Sans Regular', 'Noto Sans Regular'], 'text-size': 10, }, paint: { 'text-color': ['case', ['<', ['get', 'mag'], 3], 'black', 'white'], }, }); // objects for caching and keeping track of HTML marker objects (for performance) const markers = {}; let markersOnScreen = {}; function updateMarkers() { const newMarkers = {}; const features = map.querySourceFeatures('earthquakes'); // for every cluster on the screen, create an HTML marker for it (if we didn't yet), // and add it to the map if it's not there already for (let i = 0; i < features.length; i++) { const coords = features[i].geometry.coordinates; const props = features[i].properties; if (!props.cluster) continue; const id = props.cluster_id; let marker = markers[id]; if (!marker) { const el = createDonutChart(props); marker = markers[id] = new TrimbleMaps.Marker({ element: el, }).setLngLat(coords); } newMarkers[id] = marker; if (!markersOnScreen[id]) marker.addTo(map); } // for every marker we've added previously, remove those that are no longer visible for (id in markersOnScreen) { if (!newMarkers[id]) markersOnScreen[id].remove(); } markersOnScreen = newMarkers; } // after the GeoJSON data is loaded, update markers on the screen and do so on every map move/moveend map.on('data', (e) => { if (e.sourceId !== 'earthquakes' || !e.isSourceLoaded) return; map.on('move', updateMarkers); map.on('moveend', updateMarkers); updateMarkers(); }); }); // code for creating an SVG donut chart from feature properties function createDonutChart(props) { const offsets = []; const counts = [ props.mag1, props.mag2, props.mag3, props.mag4, props.mag5, ]; let total = 0; for (let i = 0; i < counts.length; i++) { offsets.push(total); total += counts[i]; } const fontSize = total >= 1000 ? 22 : total >= 100 ? 20 : total >= 10 ? 18 : 16; const r = total >= 1000 ? 50 : total >= 100 ? 32 : total >= 10 ? 24 : 18; const r0 = Math.round(r * 0.6); const w = r * 2; let html = `<div><svg width="${w}" height="${w}" viewbox="0 0 ${w} ${w}" text-anchor="middle" style="font: ${fontSize}px sans-serif; display: block">`; for (i = 0; i < counts.length; i++) { html += donutSegment( offsets[i] / total, (offsets[i] + counts[i]) / total, r, r0, colors[i] ); } html += `<circle cx="${r}" cy="${r}" r="${r0}" fill="white" /><text dominant-baseline="central" transform="translate(${r}, ${r})">${total.toLocaleString()}</text></svg></div>`; const el = document.createElement('div'); el.innerHTML = html; return el.firstChild; } function donutSegment(start, end, r, r0, color) { if (end - start === 1) end -= 0.00001; const a0 = 2 * Math.PI * (start - 0.25); const a1 = 2 * Math.PI * (end - 0.25); const x0 = Math.cos(a0), y0 = Math.sin(a0); const x1 = Math.cos(a1), y1 = Math.sin(a1); const largeArc = end - start > 0.5 ? 1 : 0; return [ '<path d="M', r + r0 * x0, r + r0 * y0, 'L', r + r * x0, r + r * y0, 'A', r, r, 0, largeArc, 1, r + r * x1, r + r * y1, 'L', r + r0 * x1, r + r0 * y1, 'A', r0, r0, 0, largeArc, 0, r + r0 * x0, r + r0 * y0, `" fill="${color}" />`, ].join(' '); } </script> </body> </html>