8. Chapter 8: Introduction to MapLibre GL JS#
8.1. Learning Objectives#
By the end of this chapter, you will be able to:
Understand the core architecture and capabilities of MapLibre GL JS for modern web mapping
Initialize and configure interactive maps with custom styles and data sources
Work effectively with vector tiles, GeoJSON, and other data formats
Create sophisticated layer styling using data-driven expressions and conditional logic
Implement comprehensive user interactions including clicks, hover effects, and custom controls
Optimize map performance for production applications with large datasets
Build real-time mapping applications with dynamic data updates
8.2. Understanding MapLibre GL JS#
MapLibre GL JS is a powerful, open-source JavaScript library for rendering interactive maps using vector tiles and WebGL. Born from the Mapbox GL JS codebase, MapLibre GL JS provides a vendor-neutral alternative that gives developers complete control over their mapping stack without vendor lock-in.
8.2.1. What Makes MapLibre GL JS Special#
MapLibre GL JS stands out in the web mapping ecosystem for several compelling reasons. Unlike traditional mapping libraries that rely on raster tiles, MapLibre GL JS uses vector tiles, which offer superior performance, scalability, and customization capabilities.
Vector Tiles vs Raster Tiles:
Traditional mapping libraries like Leaflet primarily work with raster tiles—pre-rendered images that display map content. While simple to implement, raster tiles have significant limitations: they’re static, require multiple zoom levels to be pre-generated, and offer limited styling flexibility.
Vector tiles, by contrast, contain raw geographic data transmitted as compact binary files. This data is rendered dynamically in the browser using WebGL, enabling real-time styling, smooth zooming, and interactive features that simply aren’t possible with raster tiles.
WebGL-Powered Performance:
MapLibre GL JS leverages WebGL for hardware-accelerated rendering, enabling smooth animations, complex visualizations, and the ability to handle large datasets without performance degradation. This makes it ideal for applications that need to display millions of data points or provide fluid user interactions.
Style Specification and Customization:
The library uses the Mapbox Style Specification, a comprehensive JSON-based format for defining map appearance. This specification enables pixel-perfect control over every aspect of map styling, from colors and typography to data-driven styling and complex visual effects.
8.2.2. Core Architecture Concepts#
Understanding MapLibre GL JS architecture helps you build more effective mapping applications and troubleshoot issues when they arise.
The Map Object:
The central Map object manages the entire mapping interface, including the canvas element, camera position, layers, sources, and user interactions. It serves as the primary interface for all map operations.
Sources and Layers: MapLibre GL JS separates data (sources) from presentation (layers). Sources define where data comes from, while layers define how that data should be styled and rendered. This separation enables flexible styling and efficient data management.
Camera and Projection: The camera system controls the map view, including center point, zoom level, bearing (rotation), and pitch (3D tilt). MapLibre GL JS uses Web Mercator projection by default but supports custom projections.
Event System: A comprehensive event system enables responsive interactions, allowing you to respond to user actions like clicks, mouse movements, and zoom changes, as well as map state changes like data loading and style updates.
8.3. Getting Started with MapLibre GL JS#
8.3.1. Installation and Setup#
You can add MapLibre GL JS to your project using several methods, depending on your development setup and preferences.
CDN (Quick Start):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MapLibre GL JS Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet">
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [-74.5, 40],
zoom: 9
});
</script>
</body>
</html>
npm/pnpm Installation:
npm install maplibre-gl
# or
pnpm add maplibre-gl
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [-74.5, 40],
zoom: 9
});
8.3.2. Basic Map Configuration#
Creating a MapLibre GL JS map requires understanding the essential configuration options that control the map’s initial appearance and behavior.
// Basic map configuration
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [-74.006, 40.7128],
zoom: 12,
minZoom: 0,
maxZoom: 18
});
8.3.3. Understanding Map Styles#
Map styles define the visual appearance of your map, from colors and fonts to the data layers that are displayed. MapLibre GL JS styles are JSON objects that follow the Mapbox Style Specification.
Basic Style Structure:
const customStyle = {
version: 8,
name: "Custom Style",
metadata: {},
sources: {
"osm": {
type: "raster",
tiles: [
"https://tile.openstreetmap.org/{z}/{x}/{y}.png"
],
tileSize: 256,
attribution: "© OpenStreetMap contributors"
}
},
layers: [
{
id: "osm-tiles",
type: "raster",
source: "osm",
minzoom: 0,
maxzoom: 19
}
]
};
const map = new maplibregl.Map({
container: 'map',
style: customStyle,
center: [0, 0],
zoom: 2
});
Working with Existing Styles:
// Using OpenMapTiles style
const map = new maplibregl.Map({
container: 'map',
style: 'https://api.maptiler.com/maps/streets/style.json?key=YOUR_API_KEY',
center: [-74.006, 40.7128],
zoom: 12
});
// Loading a local style file
const map = new maplibregl.Map({
container: 'map',
style: './assets/custom-style.json',
center: [-74.006, 40.7128],
zoom: 12
});
8.3.4. Map Lifecycle Events#
Understanding map lifecycle events is crucial for building robust applications that respond appropriately to different map states.
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [-74.006, 40.7128],
zoom: 12
});
// Map is initialized but not yet loaded
map.on('load', () => {
console.log('Map has fully loaded');
// Safe to add sources, layers, and set up interactions
initializeMapFeatures();
});
// Style data has loaded or changed
map.on('styledata', () => {
console.log('Style data loaded');
// Style-related operations can be performed
});
// Source data has loaded or changed
map.on('sourcedata', (e) => {
if (e.sourceId && e.isSourceLoaded) {
console.log(`Source ${e.sourceId} has loaded`);
}
});
// Map view has changed (pan, zoom, rotate)
map.on('moveend', () => {
const center = map.getCenter();
const zoom = map.getZoom();
console.log(`Map moved to ${center.lng}, ${center.lat} at zoom ${zoom}`);
});
// Error handling
map.on('error', (e) => {
console.error('Map error:', e.error);
// Handle errors gracefully
});
8.4. Working with Data Sources#
Data sources in MapLibre GL JS define where map data comes from. The library supports various source types, each optimized for different use cases and data formats.
8.4.1. Vector Tile Sources#
Vector tile sources are the most efficient way to display large datasets. They deliver data as compressed binary tiles that are rendered dynamically.
map.on('load', () => {
// Add vector tile source
map.addSource('population-data', {
type: 'vector',
url: 'https://example.com/population-tiles/{z}/{x}/{y}.pbf',
// Or specify tile URLs directly
tiles: ['https://example.com/population-tiles/{z}/{x}/{y}.pbf'],
minzoom: 0,
maxzoom: 14
});
// Add layer using the vector source
map.addLayer({
id: 'population-layer',
type: 'fill',
source: 'population-data',
'source-layer': 'population', // Layer name within the vector tile
paint: {
'fill-color': [
'interpolate',
['linear'],
['get', 'population'],
0, '#f0f9ff',
1000, '#0ea5e9',
10000, '#0369a1',
100000, '#1e3a8a'
],
'fill-opacity': 0.8
}
});
});
8.4.2. GeoJSON Sources#
GeoJSON sources are perfect for dynamic data that changes frequently or smaller datasets that don’t require tiling.
map.on('load', () => {
// Add GeoJSON source with static data
map.addSource('restaurants', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [-74.006, 40.7128]
},
properties: {
name: 'Great Restaurant',
rating: 4.5,
cuisine: 'Italian'
}
}
]
}
});
// Add GeoJSON source with URL
map.addSource('bike-routes', {
type: 'geojson',
data: 'https://api.example.com/bike-routes.geojson'
});
// Dynamic GeoJSON with clustering
map.addSource('events', {
type: 'geojson',
data: 'https://api.example.com/events.geojson',
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
});
8.4.3. Raster Sources#
Raster sources display pre-rendered tile images, useful for satellite imagery, weather data, or legacy tile services.
map.on('load', () => {
// Add raster tile source
map.addSource('satellite', {
type: 'raster',
tiles: [
'https://server1.example.com/satellite/{z}/{x}/{y}.png',
'https://server2.example.com/satellite/{z}/{x}/{y}.png'
],
tileSize: 256,
attribution: '© Satellite Imagery Provider'
});
// Weather overlay example
map.addSource('precipitation', {
type: 'raster',
tiles: ['https://weather.example.com/precipitation/{z}/{x}/{y}.png'],
tileSize: 256,
opacity: 0.7
});
});
8.4.4. Image Sources#
Image sources allow you to overlay specific images on precise geographic coordinates.
map.on('load', () => {
// Add georeferenced image
map.addSource('historical-map', {
type: 'image',
url: 'https://example.com/historical-map-1850.png',
coordinates: [
[-74.02, 40.73], // top-left
[-73.98, 40.73], // top-right
[-73.98, 40.70], // bottom-right
[-74.02, 40.70] // bottom-left
]
});
map.addLayer({
id: 'historical-overlay',
type: 'raster',
source: 'historical-map',
paint: {
'raster-opacity': 0.8
}
});
});
8.4.5. Dynamic Data Updates#
MapLibre GL JS makes it easy to update data sources dynamically, enabling real-time applications.
// Update GeoJSON data
const updateRestaurants = (newData) => {
const source = map.getSource('restaurants');
if (source) {
source.setData(newData);
}
};
// Real-time updates example
const fetchAndUpdateData = async () => {
try {
const response = await fetch('https://api.example.com/live-locations.geojson');
const data = await response.json();
updateRestaurants(data);
} catch (error) {
console.error('Failed to update data:', error);
}
};
// Update every 30 seconds
setInterval(fetchAndUpdateData, 30000);
// React to user interactions
map.on('click', async (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['restaurants']
});
if (features.length > 0) {
const feature = features[0];
// Fetch additional data for clicked feature
const detailData = await fetch(`/api/restaurant/${feature.properties.id}`);
const details = await detailData.json();
// Update feature properties
feature.properties = { ...feature.properties, ...details };
// Update the source with modified data
const currentData = map.getSource('restaurants')._data;
const updatedFeatures = currentData.features.map(f =>
f.properties.id === feature.properties.id ? feature : f
);
updateRestaurants({
type: 'FeatureCollection',
features: updatedFeatures
});
}
});
8.5. Working with Layers#
Layers define how data from sources is visually represented on the map. MapLibre GL JS provides several layer types, each optimized for different data types and visualization needs.
8.5.1. Layer Types Overview#
Circle Layers: Perfect for point data with proportional symbols.
map.addLayer({
id: 'earthquake-circles',
type: 'circle',
source: 'earthquakes',
paint: {
'circle-radius': [
'interpolate',
['linear'],
['get', 'magnitude'],
1, 5,
5, 15,
9, 30
],
'circle-color': [
'interpolate',
['linear'],
['get', 'magnitude'],
1, '#fef3c7',
3, '#f59e0b',
5, '#dc2626',
7, '#7c2d12'
],
'circle-opacity': 0.8,
'circle-stroke-width': 1,
'circle-stroke-color': '#ffffff'
}
});
Fill Layers: For polygon data like countries, states, or buildings.
map.addLayer({
id: 'building-footprints',
type: 'fill',
source: 'buildings',
'source-layer': 'buildings',
paint: {
'fill-color': [
'case',
['boolean', ['get', 'commercial'], false],
'#3b82f6', // Blue for commercial
['boolean', ['get', 'residential'], false],
'#10b981', // Green for residential
'#6b7280' // Gray for other
],
'fill-opacity': 0.7,
'fill-outline-color': '#374151'
}
});
Line Layers: For linear features like roads, rivers, or routes.
map.addLayer({
id: 'transit-routes',
type: 'line',
source: 'transit',
'source-layer': 'routes',
paint: {
'line-color': [
'match',
['get', 'route_type'],
'subway', '#dc2626',
'bus', '#059669',
'rail', '#7c3aed',
'#6b7280' // default
],
'line-width': [
'interpolate',
['linear'],
['zoom'],
10, 2,
14, 4,
18, 8
],
'line-opacity': 0.8
}
});
Symbol Layers: For labels and icons.
map.addLayer({
id: 'poi-labels',
type: 'symbol',
source: 'points-of-interest',
layout: {
'text-field': ['get', 'name'],
'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
'text-offset': [0, 1.25],
'text-anchor': 'top',
'text-size': [
'interpolate',
['linear'],
['zoom'],
10, 12,
16, 16
],
'icon-image': [
'case',
['==', ['get', 'type'], 'restaurant'], 'restaurant-icon',
['==', ['get', 'type'], 'hotel'], 'hotel-icon',
'generic-poi-icon'
],
'icon-size': 0.8,
'icon-allow-overlap': false,
'text-allow-overlap': false
},
paint: {
'text-color': '#1f2937',
'text-halo-color': '#ffffff',
'text-halo-width': 1
}
});
8.5.2. Data-Driven Styling#
MapLibre GL JS supports sophisticated data-driven styling using expressions that dynamically calculate style properties based on data attributes.
Interpolation Expressions:
// Smooth color transitions based on numeric data
map.addLayer({
id: 'temperature-heatmap',
type: 'circle',
source: 'weather-stations',
paint: {
'circle-color': [
'interpolate',
['linear'],
['get', 'temperature'],
-20, '#1e3a8a', // Dark blue for very cold
0, '#3b82f6', // Blue for cold
20, '#10b981', // Green for mild
30, '#f59e0b', // Orange for warm
40, '#dc2626' // Red for hot
],
'circle-radius': [
'interpolate',
['exponential', 1.5],
['zoom'],
5, 3,
10, 8,
15, 20
]
}
});
Conditional Styling:
// Different styles based on categorical data
map.addLayer({
id: 'land-use',
type: 'fill',
source: 'land-use-data',
paint: {
'fill-color': [
'match',
['get', 'land_use'],
'residential', '#fef3c7',
'commercial', '#dbeafe',
'industrial', '#f3e8ff',
'park', '#d1fae5',
'water', '#bfdbfe',
'#f3f4f6' // default color
],
'fill-opacity': [
'case',
['boolean', ['feature-state', 'hover'], false],
0.8,
0.6
]
}
});
Zoom-Based Styling:
// Adaptive road styling based on zoom level
map.addLayer({
id: 'road-network',
type: 'line',
source: 'roads',
paint: {
'line-width': [
'interpolate',
['linear'],
['zoom'],
5, ['case', ['==', ['get', 'highway'], 'motorway'], 1, 0.5],
15, ['case', ['==', ['get', 'highway'], 'motorway'], 8, 2]
],
'line-color': [
'match',
['get', 'highway'],
'motorway', '#dc2626',
'primary', '#d97706',
'secondary', '#65a30d',
'#6b7280'
]
}
});
8.5.3. Layer Ordering and Visibility#
Proper layer management is crucial for creating clear, readable maps.
// Add layers in the correct order (bottom to top)
map.on('load', () => {
// Background layers first
map.addLayer(landUseLayer);
map.addLayer(waterLayer);
// Infrastructure
map.addLayer(roadLayer);
map.addLayer(railwayLayer);
// Buildings
map.addLayer(buildingLayer);
// Points of interest
map.addLayer(poiLayer);
// Labels on top
map.addLayer(labelLayer);
});
// Insert layer at specific position
map.addLayer(newLayer, 'existing-layer-id'); // Insert before existing layer
// Control layer visibility
const toggleLayer = (layerId) => {
const visibility = map.getLayoutProperty(layerId, 'visibility');
map.setLayoutProperty(
layerId,
'visibility',
visibility === 'visible' ? 'none' : 'visible'
);
};
// Dynamic layer filtering
const filterByDate = (startDate, endDate) => {
map.setFilter('events-layer', [
'all',
['>=', ['get', 'date'], startDate],
['<=', ['get', 'date'], endDate]
]);
};
// Update layer style properties
const updateLayerStyle = (layerId, property, value) => {
if (property.startsWith('paint.')) {
map.setPaintProperty(layerId, property.replace('paint.', ''), value);
} else if (property.startsWith('layout.')) {
map.setLayoutProperty(layerId, property.replace('layout.', ''), value);
}
};
8.6. User Interactions and Events#
MapLibre GL JS provides a comprehensive event system that enables rich user interactions, from simple clicks to complex gestures and data querying.
8.6.1. Mouse and Touch Events#
Understanding and handling user input events is essential for creating interactive mapping applications.
// Click events
map.on('click', (e) => {
const coordinates = e.lngLat;
console.log(`Clicked at ${coordinates.lng}, ${coordinates.lat}`);
// Query features at click point
const features = map.queryRenderedFeatures(e.point, {
layers: ['interactive-layer']
});
if (features.length > 0) {
const feature = features[0];
showPopup(feature, coordinates);
}
});
// Double-click to zoom to specific location
map.on('dblclick', (e) => {
map.flyTo({
center: e.lngLat,
zoom: map.getZoom() + 1
});
});
// Hover effects
map.on('mouseenter', 'interactive-layer', (e) => {
map.getCanvas().style.cursor = 'pointer';
// Highlight hovered feature
if (e.features.length > 0) {
const feature = e.features[0];
map.setFeatureState(
{ source: 'data-source', id: feature.id },
{ hover: true }
);
}
});
map.on('mouseleave', 'interactive-layer', () => {
map.getCanvas().style.cursor = '';
// Remove highlight
map.removeFeatureState(
{ source: 'data-source' },
'hover'
);
});
// Mouse movement for dynamic interactions
map.on('mousemove', (e) => {
const coordinates = e.lngLat;
updateCoordinateDisplay(coordinates);
});
8.6.3. Data Loading Events#
Handle asynchronous data loading and updates.
// Source data events
map.on('sourcedata', (e) => {
if (e.sourceId === 'my-source') {
if (e.isSourceLoaded) {
console.log('Source data fully loaded');
hideLoadingIndicator();
} else {
console.log('Source data loading...');
showLoadingIndicator();
}
}
});
// Data events for specific sources
map.on('data', (e) => {
if (e.sourceId === 'dynamic-data' && e.isSourceLoaded) {
updateDataStatistics();
}
});
// Error handling
map.on('error', (e) => {
console.error('Map error:', e.error);
showErrorMessage('Failed to load map data');
});
8.6.4. Custom Popup Implementation#
Create sophisticated popups and info windows for feature interaction.
// Simple custom popup implementation
class CustomPopup {
constructor() {
this.popup = new maplibregl.Popup({
closeButton: true,
maxWidth: '300px'
});
}
show(feature, coordinates) {
const props = feature.properties;
const content = `
<div class="custom-popup">
<h3>${props.name}</h3>
<p><strong>Type:</strong> ${props.type}</p>
<p><strong>Rating:</strong> ${'★'.repeat(Math.floor(props.rating))} (${props.rating})</p>
<button onclick="this.getDirections([${feature.geometry.coordinates}])">
Get Directions
</button>
</div>
`;
this.popup
.setLngLat(coordinates)
.setHTML(content)
.addTo(map);
}
getDirections(coords) {
const url = `https://www.google.com/maps/dir/?api=1&destination=${coords[1]},${coords[0]}`;
window.open(url, '_blank');
}
}
// Usage
const customPopup = new CustomPopup();
map.on('click', 'poi-layer', (e) => {
customPopup.show(e.features[0], e.lngLat);
});
8.6.5. Advanced Interaction Patterns#
Implement sophisticated interaction patterns for professional mapping applications.
// Selection and multi-selection
class FeatureSelector {
constructor(map) {
this.map = map;
this.selectedFeatures = new Set();
this.setupEventListeners();
}
setupEventListeners() {
this.map.on('click', 'selectable-layer', (e) => {
const feature = e.features[0];
const id = feature.properties.id;
if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) {
// Multi-select with Ctrl/Cmd
this.toggleSelection(id);
} else {
// Single select
this.clearSelection();
this.selectFeature(id);
}
});
// Clear selection on map click (not on features)
this.map.on('click', (e) => {
const features = this.map.queryRenderedFeatures(e.point, {
layers: ['selectable-layer']
});
if (features.length === 0) {
this.clearSelection();
}
});
}
selectFeature(id) {
this.selectedFeatures.add(id);
this.updateFeatureState(id, { selected: true });
this.onSelectionChange();
}
deselectFeature(id) {
this.selectedFeatures.delete(id);
this.updateFeatureState(id, { selected: false });
this.onSelectionChange();
}
toggleSelection(id) {
if (this.selectedFeatures.has(id)) {
this.deselectFeature(id);
} else {
this.selectFeature(id);
}
}
clearSelection() {
this.selectedFeatures.forEach(id => {
this.updateFeatureState(id, { selected: false });
});
this.selectedFeatures.clear();
this.onSelectionChange();
}
updateFeatureState(id, state) {
this.map.setFeatureState(
{ source: 'data-source', id: id },
state
);
}
onSelectionChange() {
const selectedCount = this.selectedFeatures.size;
document.getElementById('selection-count').textContent =
`${selectedCount} feature(s) selected`;
// Enable/disable bulk action buttons
const bulkActions = document.querySelectorAll('.bulk-action');
bulkActions.forEach(button => {
button.disabled = selectedCount === 0;
});
}
getSelectedFeatures() {
return Array.from(this.selectedFeatures);
}
}
// Drawing and measurement tools
class DrawingTools {
constructor(map) {
this.map = map;
this.isDrawing = false;
this.currentDrawing = null;
this.measurements = [];
}
startDrawing(type = 'polygon') {
this.isDrawing = true;
this.currentDrawing = {
type: type,
coordinates: []
};
this.map.getCanvas().style.cursor = 'crosshair';
this.map.on('click', this.onDrawingClick);
this.map.on('dblclick', this.finishDrawing);
}
onDrawingClick = (e) => {
if (!this.isDrawing) return;
const coordinates = [e.lngLat.lng, e.lngLat.lat];
this.currentDrawing.coordinates.push(coordinates);
// Update preview
this.updateDrawingPreview();
}
finishDrawing = () => {
if (!this.isDrawing || this.currentDrawing.coordinates.length < 3) return;
// Close polygon
this.currentDrawing.coordinates.push(this.currentDrawing.coordinates[0]);
// Calculate area
const area = this.calculatePolygonArea(this.currentDrawing.coordinates);
// Add to map
this.addDrawingToMap(this.currentDrawing, area);
// Cleanup
this.isDrawing = false;
this.currentDrawing = null;
this.map.getCanvas().style.cursor = '';
this.map.off('click', this.onDrawingClick);
this.map.off('dblclick', this.finishDrawing);
}
calculatePolygonArea(coordinates) {
// Simplified area calculation (for Web Mercator)
let area = 0;
const n = coordinates.length - 1;
for (let i = 0; i < n; i++) {
area += coordinates[i][0] * coordinates[i + 1][1];
area -= coordinates[i + 1][0] * coordinates[i][1];
}
return Math.abs(area) / 2;
}
addDrawingToMap(drawing, area) {
const id = `drawing-${Date.now()}`;
// Add to measurements
this.measurements.push({
id: id,
area: area,
coordinates: drawing.coordinates
});
// Add source and layer
this.map.addSource(id, {
type: 'geojson',
data: {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [drawing.coordinates]
},
properties: {
area: area
}
}
});
this.map.addLayer({
id: `${id}-fill`,
type: 'fill',
source: id,
paint: {
'fill-color': '#ff6b6b',
'fill-opacity': 0.3
}
});
this.map.addLayer({
id: `${id}-line`,
type: 'line',
source: id,
paint: {
'line-color': '#ff6b6b',
'line-width': 2
}
});
}
}
// Initialize interaction tools
const selector = new FeatureSelector(map);
const drawingTools = new DrawingTools(map);
// Bind to UI controls
document.getElementById('draw-polygon').addEventListener('click', () => {
drawingTools.startDrawing('polygon');
});
8.7. Summary#
MapLibre GL JS provides a powerful foundation for building sophisticated web mapping applications. Its vector tile support, WebGL rendering, and comprehensive API enable everything from simple location displays to complex data visualizations and interactive analysis tools.
Key concepts covered include map initialization and configuration, working with various data sources (vector tiles, GeoJSON, raster, and images), creating and styling layers with data-driven expressions, and implementing rich user interactions. The separation of data sources and presentation layers, combined with the flexible styling system, provides the architectural foundation needed for maintainable and scalable mapping applications.
The next chapter will explore Leaflet, a lightweight alternative that excels in different use cases and provides a complementary approach to web mapping.
8.8. Exercises#
8.8.1. Exercise 8.1: Basic MapLibre GL JS Setup#
Objective: Create a functional MapLibre GL JS map with proper configuration and styling.
Instructions:
Set up the basic map environment:
Create an HTML page with proper viewport and CSS setup
Initialize a MapLibre GL JS map with custom center and zoom
Configure map controls and interaction options
Add proper error handling for map loading
Customize the map appearance:
Create a custom map style with at least 3 different layer types
Implement a custom color scheme and typography
Add attribution and custom branding elements
Test responsive behavior:
Ensure the map works on different screen sizes
Test touch interactions on mobile devices
Verify performance on slower devices
Deliverable: A responsive web page with a fully configured MapLibre GL JS map.
8.8.2. Exercise 8.2: Data Source Integration#
Objective: Work with multiple data source types and implement dynamic data loading.
Instructions:
Implement various source types:
Add a vector tile source for base map data
Integrate GeoJSON data from a REST API
Include a raster overlay (satellite or weather data)
Add an image source for a georeferenced historical map
Create dynamic data management:
Implement data loading with proper error handling
Add loading indicators for asynchronous operations
Create functions to update GeoJSON data dynamically
Implement data caching to improve performance
Add data validation and processing:
Validate GeoJSON data before adding to map
Process and transform data as needed
Handle edge cases and invalid data gracefully
Deliverable: A map application that successfully loads and manages multiple data sources with proper error handling.
8.8.3. Exercise 8.3: Advanced Layer Styling#
Objective: Create sophisticated layer styling using data-driven expressions and conditional logic.
Instructions:
Implement data-driven styling:
Create choropleth maps using data interpolation
Implement proportional symbol mapping for point data
Use categorical styling for different feature types
Add zoom-dependent styling that adapts to scale
Create interactive layer controls:
Build a layer switcher with visibility toggles
Implement opacity controls for overlay layers
Add filtering controls based on data attributes
Create style presets that users can switch between
Advanced styling techniques:
Use feature state for hover and selection effects
Implement conditional styling based on multiple attributes
Create animated transitions between style states
Add custom icons and symbols for point features
Deliverable: A map with sophisticated styling controls that demonstrate mastery of MapLibre GL JS styling capabilities.
8.8.4. Exercise 8.4: Interactive Features and Events#
Objective: Implement comprehensive user interaction patterns and event handling.
Instructions:
Create basic interactions:
Implement click events with feature identification
Add hover effects with visual feedback
Create custom popups with detailed feature information
Add keyboard navigation support
Build advanced interaction tools:
Implement feature selection with multi-select support
Create drawing tools for polygons and measurements
Add search functionality with geocoding
Build custom navigation controls
Integrate with external APIs:
Connect popups to external data sources
Implement directions integration
Add social sharing capabilities
Create data export functionality
Deliverable: A fully interactive map application with comprehensive user interaction capabilities.
8.8.5. Exercise 8.5: Performance Optimization#
Objective: Optimize MapLibre GL JS applications for production use.
Instructions:
Analyze performance bottlenecks:
Use browser developer tools to profile map performance
Identify slow-loading data sources and large datasets
Measure rendering performance with complex styling
Test performance on low-end devices
Implement optimization strategies:
Optimize vector tile generation and serving
Implement data clustering for large point datasets
Use appropriate zoom-level data simplification
Optimize image and asset loading
Monitor and measure improvements:
Set up performance monitoring
Create performance benchmarks
Test under various network conditions
Document optimization techniques used
Deliverable: Performance analysis report and optimized map application with documented improvements.
8.8.6. Exercise 8.6: Real-time Data Integration#
Objective: Build a map application that handles real-time data updates and live interactions.
Instructions:
Set up real-time data connections:
Implement WebSocket connections for live data
Create polling mechanisms for REST API updates
Handle connection failures and reconnection logic
Add data buffering and rate limiting
Visualize real-time data:
Create animated markers for moving objects
Implement time-based data filtering
Add real-time charts and statistics
Show data freshness indicators
Build real-time collaboration features:
Allow multiple users to interact with the same map
Implement shared cursors or annotations
Add real-time chat or commenting
Handle concurrent edits and conflicts
Deliverable: A real-time mapping application demonstrating live data visualization and collaboration features.
8.8.7. Exercise 8.7: 3D and Advanced Visualization#
Objective: Explore 3D capabilities and advanced visualization techniques in MapLibre GL JS.
Instructions:
Implement 3D features:
Add 3D building extrusions based on height data
Create terrain visualization with elevation data
Implement 3D point clouds or mesh data
Add camera controls for 3D navigation
Create advanced visualizations:
Build animated data flows or connections
Implement heat maps and density visualizations
Create time-series animations
Add custom shaders for special effects
Optimize 3D performance:
Test 3D performance on various devices
Implement level-of-detail (LOD) systems
Optimize geometry and texture usage
Add performance monitoring for 3D features
Deliverable: A 3D mapping application showcasing advanced visualization techniques with good performance characteristics.
Reflection Questions:
How does MapLibre GL JS compare to traditional mapping libraries in terms of capabilities and performance?
What are the key considerations when choosing between raster and vector tiles?
How can data-driven styling improve the effectiveness of map visualizations?
What strategies work best for handling large datasets in web mapping applications?