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:

  1. 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

  2. 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

  3. 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:

  1. 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

  2. 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

  3. 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:

  1. 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

  2. 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

  3. 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:

  1. 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

  2. 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

  3. 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:

  1. 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

  2. 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

  3. 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:

  1. 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

  2. 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

  3. 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:

  1. 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

  2. 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

  3. 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?

8.9. Further Reading#