MapLibre GL JS Integration Guide

Add Lynker Spatial tiles to your web maps with MapLibre GL JS

Overview

MapLibre GL JS is a free, open-source WebGL mapping library that makes it easy to add interactive maps to web pages. It provides excellent support for vector tiles and works seamlessly with the Lynker Spatial Tile Service.

Installation

Install MapLibre GL JS via npm or use the CDN version:

NPM Installation

npm install maplibre-gl

CDN Installation (HTML)

<link href='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css' rel='stylesheet' /> <script src='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js'></script>

Basic Map Setup

<!DOCTYPE html> <html> <head> <meta charset='utf-8' /> <title>Lynker Spatial Map</title> <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' /> <script src='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js'></script> <link href='https://unpkg.com/maplibre-gl@latest/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: [-100, 40], zoom: 4 }); </script> </body> </html>

Adding Lynker Spatial Tiles

Vector Tiles (Recommended)

Vector tiles provide the best performance and styling flexibility:

map.on('load', () => { // Add data source map.addSource('padus', { type: 'vector', tiles: [ 'https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}' ], minzoom: 0, maxzoom: 14 }); // Add layer to map map.addLayer({ id: 'padus-layer', type: 'fill', source: 'padus', 'source-layer': 'flowpath', paint: { 'fill-color': '#088', 'fill-opacity': 0.5 } }); });

Handling Authentication

Since MapLibre runs in the browser, authentication requires special handling. Here are three approaches:

Option 1: Server-Side Proxy (Recommended)

Create a proxy endpoint on your server that adds the Authentication header:

// Node.js/Express example app.get('/api/tiles/:source/:z/:x/:y', async (req, res) => { const { source, z, x, y } = req.params; const token = process.env.LYNKER_TOKEN; const response = await fetch( `https://tiles.lynker-spatial.com/api/tiles/${source}/${z}/${x}/${y}`, { headers: { 'Authorization': `Bearer ${token}` } } ); res.set('Content-Type', 'application/octet-stream'); res.send(await response.buffer()); }); // Update tile URL to use proxy map.addSource('padus', { type: 'vector', tiles: [ '/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}' ] });

Option 2: Token in Authorization Header (Development Only)

let token = localStorage.getItem('lynker_token'); // Refresh token if needed async function ensureValidToken() { const stored = localStorage.getItem('lynker_token_expires'); if (Date.now() > new Date(stored).getTime()) { const response = await fetch('/api/refresh-token', { method: 'POST' }); token = await response.json().access_token; localStorage.setItem('lynker_token', token); } } map.on('load', async () => { await ensureValidToken(); map.addSource('padus', { type: 'vector', tiles: [ `https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}?token=${token}` ] }); });

Option 3: Programmatic Tile Addition with Custom Fetch

// Use a plugin or custom protocol handler map.on('load', () => { const token = localStorage.getItem('lynker_token'); // Override protocol map.addProtocol('lynker', async (params, callback) => { const url = `https://tiles.lynker-spatial.com${params.url.replace('lynker:', '')}`; try { const response = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }); const buffer = await response.arrayBuffer(); callback(null, buffer); } catch (error) { callback(error); } }); map.addSource('padus', { type: 'vector', tiles: ['lynker://padus/{z}/{x}/{y}'] }); });
Security Note: Storing raw tokens in browser localStorage is not secure. Use Option 1 (server-side proxy) for production applications.

Layer Styling

Simple Fill Styling

map.addLayer({ id: 'padus-fill', type: 'fill', source: 'padus', 'source-layer': 'flowpath', paint: { 'fill-color': '#088', 'fill-opacity': 0.7 } });

Data-Driven Styling

map.addLayer({ id: 'padus-by-protection', type: 'fill', source: 'padus', 'source-layer': 'flowpath', paint: { 'fill-color': [ 'interpolate', ['linear'], ['get', 'protection_level'], 0, '#fee5d9', 1, '#fcae91', 2, '#fb6a4a', 3, '#cb181d' ], 'fill-opacity': 0.75 } });

Adding Outlines

map.addLayer({ id: 'padus-outline', type: 'line', source: 'padus', 'source-layer': 'flowpath', paint: { 'line-color': '#627BC1', 'line-width': 2, 'line-opacity': 0.8 } });

Interactive Features

Popup on Click

map.on('click', 'padus-fill', (e) => { const properties = e.features[0].properties; let popupContent = '<div>'; for (const key in properties) { popupContent += `<p><strong>${key}:</strong> ${properties[key]}</p>`; } popupContent += '</div>'; new maplibregl.Popup() .setLngLat(e.lngLat) .setHTML(popupContent) .addTo(map); }); // Change cursor on hover map.on('mouseenter', 'padus-fill', () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', 'padus-fill', () => { map.getCanvas().style.cursor = ''; });

Hover Highlighting

let hoveredId = null; map.on('mousemove', 'padus-fill', (e) => { if (e.features.length > 0) { if (hoveredId !== null) { map.setFeatureState( { source: 'padus', id: hoveredId }, { hover: false } ); } hoveredId = e.features[0].id; map.setFeatureState( { source: 'padus', id: hoveredId }, { hover: true } ); } }); map.on('mouseleave', 'padus-fill', () => { if (hoveredId !== null) { map.setFeatureState( { source: 'padus', id: hoveredId }, { hover: false } ); } hoveredId = null; }); // Add hover styling map.addLayer({ id: 'padus-highlight', type: 'fill', source: 'padus', 'source-layer': 'flowpath', paint: { 'fill-color': '#ff6b6b', 'fill-opacity': [ 'case', ['boolean', ['feature-state', 'hover'], false], 0.9, 0 ] } });

Performance Optimization

Tile Caching

// Enable browser caching with appropriate HTTP headers map.addSource('padus', { type: 'vector', tiles: [ 'https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}' ], minzoom: 0, maxzoom: 14, // Browser caching is automatic with HTTP headers from server });

Zoom-Level Based Layer Control

// Show detail only at higher zoom levels map.addLayer({ id: 'padus-detail', type: 'fill', source: 'padus', 'source-layer': 'padus-detail', minzoom: 10, paint: { 'fill-color': '#088', 'fill-opacity': 0.7 } });

Multiple Tile Sources

map.on('load', () => { // PADUS layer map.addSource('padus', { type: 'vector', tiles: ['https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}'], minzoom: 0, maxzoom: 14 }); // Wetlands layer map.addSource('wetlands', { type: 'vector', tiles: ['https://tiles.lynker-spatial.com/api/tiles/wetlands/{z}/{x}/{y}'], minzoom: 0, maxzoom: 14 }); // Hydrofabric layer map.addSource('lynker-spatial-modeling-fabric', { type: 'vector', tiles: ['https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}'], minzoom: 0, maxzoom: 14 }); // Add layers with different z-index ordering map.addLayer({ id: 'wetlands-layer', type: 'fill', source: 'wetlands', ... }); map.addLayer({ id: 'padus-layer', type: 'fill', source: 'padus', ... }); map.addLayer({ id: 'hydrofabric-layer', type: 'line', source: 'lynker-spatial-modeling-fabric', ... }); });

Troubleshooting

Tiles Not Appearing

Authentication Errors (401)

If you see "401 Unauthorized" errors in the browser console, your authentication setup needs adjustment. Use Option 1 (server-side proxy) to add the Authorization header server-side.

Performance Issues

Example: Complete Map Application

<!DOCTYPE html> <html> <head> <meta charset='utf-8' /> <title>Lynker Spatial Watershed Map</title> <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' /> <script src='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js'></script> <link href='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css' rel='stylesheet' /> <style> body { margin: 0; padding: 0; font-family: Arial; } #map { position: absolute; top: 0; bottom: 0; width: 100%; } #info { position: absolute; top: 20px; left: 20px; background: white; padding: 20px; border-radius: 12px; z-index: 100; max-width: 300px; } </style> </head> <body> <div id='map'></div> <div id='info'> <h2>Watershed Map</h2> <p>Click on protected areas for more information.</p> <div id='feature-info'></div> </div> <script> const map = new maplibregl.Map({ container: 'map', style: 'https://demotiles.maplibre.org/style.json', center: [-100, 40], zoom: 5 }); map.on('load', () => { // Add data source map.addSource('padus', { type: 'vector', // Canonical tile URL — omit the .pbf extension. Use a proxy if you need to inject Authorization headers. tiles: ['https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}'], minzoom: 0, maxzoom: 14 }); // Add layer map.addLayer({ id: 'padus-fill', type: 'fill', source: 'padus', 'source-layer': 'flowpath', paint: { 'fill-color': '#088', 'fill-opacity': 0.5 } }); map.addLayer({ id: 'padus-outline', type: 'line', source: 'padus', 'source-layer': 'flowpath', paint: { 'line-color': '#627BC1', 'line-width': 2 } }); // Add interactivity map.on('click', 'padus-fill', (e) => { const info = e.features[0].properties; let html = '<h3>' + (info.name || 'Protected Area') + '</h3>'; html += '<p>' + (info.description || 'No description available') + '</p>'; document.getElementById('feature-info').innerHTML = html; }); map.on('mouseenter', 'padus-fill', () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', 'padus-fill', () => { map.getCanvas().style.cursor = ''; }); }); </script> </body> </html>

Resources