Svelte MapLibre GL

website·Indexed 32 days ago

https://svelte-maplibre-gl.mierune.dev

A Svelte 5 wrapper for MapLibre GL JS designed to build interactive web maps. It provides reactive components for map controls, markers, popups, and 3D features like terrain, buildings, and extrusions, while maintaining full support for MapLibre GL JS usage.

Tokens
14.8K
Snippets
53
Records
56
Agent score
65%

What's inside Svelte MapLibre GL

  1. Create a scope (circular mask) map effect

    A 'scope' or 'magnifying glass' effect can be achieved by overlaying one map on top of another using absolute positioning and applying a CSS clip-path: circle() to the top map. Update the circle's coordinates based on mouse movement to move the scope.
    <div onmousemove={(ev) => (point = { x: ev.offsetX, y: ev.offsetY })}>
      <!-- Base Map -->
      <MapLibre class="flex-1" ... />
    
      <!-- Scope Map -->
      <MapLibre
        class="absolute inset-0"
        inlineStyle={`clip-path: circle(15% at ${point.x}px ${point.y}px);`}
        ... 
      />
    </div>
  2. Implement a WebGL Custom Layer for a globe projection

    To draw custom WebGL content on a MapLibre globe, create a class that implements the CustomLayerInterface (omitting id and type). The implementation must handle shader creation and rendering using MapLibre's projection data to ensure geometry correctly wraps around the globe.

    Key requirements for globe projection:

    1. Shader Injection: You must inject shaderDescription.vertexShaderPrelude and shaderDescription.define into your vertex shader. This provides the projectTile function necessary for projecting Mercator coordinates to the globe.
    2. Projection Variants: Use shaderDescription.variantName to cache and retrieve shaders, as MapLibre may use different shader variants depending on the current projection state.
    3. Uniforms: In the render method, you must provide the projection matrices and clipping plane data from args.defaultProjectionData to the shader uniforms.
    4. Geometry Subdivision: Large triangles or shapes may be clipped by the globe's "horizon plane" at high zoom levels. Subdividing geometry into smaller segments is recommended to prevent clipping and ensure the geometry curves with the globe.
    import 'svelte-maplibre-gl/vite'; // Required for GL JS v6+
    import { MapLibre, CustomLayer, Projection } from 'svelte-maplibre-gl';
    import * as maplibregl from 'maplibre-gl';
    
    class CustomLayerImpl implements Omit<maplibregl.CustomLayerInterface, 'id' | 'type'> {
      private shaderMap: Map<string, WebGLProgram> = new Map();
      private buffer: WebGLBuffer | null = null;
    
      private getShader(gl: WebGL2RenderingContext, shaderDescription: maplibregl.CustomRenderMethodInput['shaderData']) {
        if (this.shaderMap.has(shaderDescription.variantName)) return this.shaderMap.get(shaderDescription.variantName)!;
    
        const vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
        const vertexSource = `#version 300 es
          ${shaderDescription.vertexShaderPrelude}
          ${shaderDescription.define}
          in vec2 a_pos;
          void main() { gl_Position = projectTile(a_pos); }`;
        
        gl.shaderSource(vertexShader, vertexSource);
        gl.compileShader(vertexShader);
        // ... fragment shader setup and program linking ...
        return program;
      }
    
      onAdd(_map: maplibregl.Map, gl: WebGL2RenderingContext) {
        this.buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([/* mercator coords */]), gl.STATIC_DRAW);
      }
    
      render(gl: WebGL2RenderingContext, args: maplibregl.CustomRenderMethodInput) {
        const program = this.getShader(gl, args.shaderData);
        gl.useProgram(program);
        
        // Set essential projection uniforms
        gl.uniformMatrix4fv(gl.getUniformLocation(program, 'u_projection_matrix'), false, args.defaultProjectionData.mainMatrix);
        gl.uniform4f(gl.getUniformLocation(program, 'u_projection_tile_mercator_coords'), ...args.defaultProjectionData.tileMercatorCoords);
        gl.uniform4f(gl.getUniformLocation(program, 'u_projection_clipping_plane'), ...args.defaultProjectionData.clippingPlane);
    
        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 3);
      }
    }
    
    // Usage in Svelte component
    <MapLibre style="..." center={[20, 58]} zoom={3}>
      <CustomLayer implementation={new CustomLayerImpl()} />
      <Projection type="globe" />
    </MapLibre>
  3. Load and manage map images from URLs using ImageLoader

    The ImageLoader component in svelte-maplibre-gl provides a utility to asynchronously load images from URLs and add them to the MapLibre map. It ensures that child components (such as SymbolLayer) are only added to the map after all specified images have been successfully loaded.

    Images can be defined as a simple URL string or as an array containing a URL and a configuration object for stretchable images (defining stretchX, stretchY, content, and pixelRatio).

    <script lang="ts">
      import 'svelte-maplibre-gl/vite'; // Required for GL JS v6+
      import { MapLibre, ImageLoader, GeoJSONSource, SymbolLayer } from 'svelte-maplibre-gl';
      import type { FeatureCollection } from 'geojson';
    
      let data: FeatureCollection = {
        type: 'FeatureCollection',
        features: [
          {
            type: 'Feature',
            geometry: { type: 'Point', coordinates: [-48.47279, -1.44585] },
            properties: { imageName: 'osgeo' }
          }
        ]
      };
    </script>
    
    <MapLibre
      style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
      zoom={1.5}
      center={{ lng: -10.0, lat: -20 }}
    >
      <GeoJSONSource {data}>
        <ImageLoader
          images={{
            // Simple image load
            osgeo: 'https://maplibre.org/maplibre-gl-js/docs/assets/osgeo-logo.png',
            // Stretchable image configuration
            'popup-debug': [
              'https://maplibre.org/maplibre-gl-js/docs/assets/popup_debug.png',
              {
                stretchX: [[25, 55], [85, 115]],
                stretchY: [[25, 100]],
                content: [25, 25, 115, 100],
                pixelRatio: 2
              }
            ]
          }}
        >
          <SymbolLayer
            layout={{
              'icon-image': ['get', 'imageName'],
              'icon-size': 1
            }}
          />
        </ImageLoader>
      </GeoJSONSource>
    </MapLibre>
  4. Create custom map controls with CustomControl

    The CustomControl component allows developers to add user-defined UI elements (buttons, HTML, or native MapLibre IControl plugins) to the map interface. It supports positioning via the position prop (e.g., 'top-left', 'top-right', 'bottom-left', 'bottom-right') and can either wrap Svelte components as children or accept a pre-instantiated MapLibre control via the control prop.
    <script lang="ts">
      import 'svelte-maplibre-gl/vite'; // Required for GL JS v6+
      import { MapLibre, CustomControl } from 'svelte-maplibre-gl';
      import type * as maplibregl from 'maplibre-gl';
    
      let controlPosition: maplibregl.ControlPosition = 'top-left';
    </script>
    
    <MapLibre>
      <!-- Example 1: Wrapping arbitrary Svelte/HTML elements -->
      <CustomControl position="bottom-left">
        <button onclick={() => alert('Clicked!')}>
          Custom Button
        </button>
      </CustomControl>
    
      <!-- Example 2: Dynamic positioning -->
      <CustomControl position={controlPosition}>
        <div class="p-2">Dynamic Position Control</div>
      </CustomControl>
    
      <!-- Example 3: Injecting a native MapLibre IControl plugin -->
      <CustomControl position="top-left" control={myNativeControlInstance} />
    </MapLibre>
  5. Implement 3D Terrain and Sky in Svelte MapLibre

    To render 3D elevation and a realistic sky, use a combination of RasterDEMTileSource, Terrain, and Sky components. For MapLibre GL JS v6+, you must import svelte-maplibre-gl/vite at the start of your script. The Terrain component requires a RasterDEMTileSource to provide the elevation data.
    import 'svelte-maplibre-gl/vite';
    import { MapLibre, RasterDEMTileSource, Terrain, Sky, Light, GlobeControl } from 'svelte-maplibre-gl';
    
    // Inside Svelte component:
    <MapLibre style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json">
      <GlobeControl />
      <Light anchor="map" />
      
      <Sky 
        sky-color="#001560" 
        horizon-color="#0090c0" 
        fog-color="#ffffff" 
      />
    
      <RasterDEMTileSource id="terrain" url="https://tiles.mapterhorn.com/tilejson.json">
        <Terrain exaggeration={1.0} />
      </RasterDEMTileSource>
    </MapLibre>
  6. Setup svelte-maplibre-gl for GL JS v6+

    For projects using MapLibre GL JS v6 or newer, a specific Vite import is required to ensure the library functions correctly.
    import 'svelte-maplibre-gl/vite'; // Required only for GL JS v6+
  7. Add a 3D model to a MapLibre map using Three.js

    To render 3D models (such as GLTF files) on a MapLibre map, implement a custom layer using Three.js. The implementation requires a CustomLayer class that manages a Three.js scene, camera, and renderer, sharing the MapLibre GL canvas. The layer must handle projection matrices to correctly position the model in both Mercator and Globe projections.
    import 'svelte-maplibre-gl/vite';
    import { MapLibre, CustomLayer, GlobeControl, Projection } from 'svelte-maplibre-gl';
    import * as maplibregl from 'maplibre-gl';
    import * as THREE from 'three';
    import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
    
    const earthRadius = 6_371_008.8;
    
    // Helper to calculate matrix for Mercator projection
    function getMercatorModelMatrix(origin: maplibregl.LngLatLike, altitude: number) {
      const modelAsMercatorCoordinate = maplibregl.MercatorCoordinate.fromLngLat(origin, altitude);
      const scale = modelAsMercatorCoordinate.meterInMercatorCoordinateUnits();
    
      return new THREE.Matrix4()
        .makeTranslation(modelAsMercatorCoordinate.x, modelAsMercatorCoordinate.y, modelAsMercatorCoordinate.z)
        .multiply(new THREE.Matrix4().makeRotationZ(Math.PI))
        .multiply(new THREE.Matrix4().makeRotationX(Math.PI / 2))
        .scale(new THREE.Vector3(-scale, scale, scale));
    }
    
    // Helper to calculate matrix for Globe projection
    function getGlobeModelMatrix(origin: maplibregl.LngLatLike, altitude: number) {
      const { lng, lat } = maplibregl.LngLat.convert(origin);
      const scale = 1 / earthRadius;
    
      return new THREE.Matrix4()
        .makeRotationY((lng / 180) * Math.PI)
        .multiply(new THREE.Matrix4().makeRotationX((-lat / 180) * Math.PI))
        .multiply(new THREE.Matrix4().makeTranslation(0, 0, 1 + altitude / earthRadius))
        .multiply(new THREE.Matrix4().makeRotationX(Math.PI / 2))
        .scale(new THREE.Vector3(scale, scale, scale));
    }
    
    class CustomLayerImpl implements Omit<maplibregl.CustomLayerInterface, 'id' | 'type'> {
      renderingMode = '3d' as const;
      private camera = new THREE.Camera();
      private scene = new THREE.Scene();
      private renderer: THREE.WebGLRenderer | null = null;
      private map: maplibregl.Map | null = null;
    
      onAdd(map: maplibregl.Map, gl: WebGL2RenderingContext) {
        this.map = map;
    
        // Lighting
        const directionalLight1 = new THREE.DirectionalLight(0xffffff);
        directionalLight1.position.set(0, -70, 100).normalize();
        this.scene.add(directionalLight1);
    
        const directionalLight2 = new THREE.DirectionalLight(0xffffff);
        directionalLight2.position.set(0, 70, 100).normalize();
        this.scene.add(directionalLight2);
    
        // Load GLTF Model
        const loader = new GLTFLoader();
        loader.load('https://maplibre.org/maplibre-gl-js/docs/assets/34M_17/34M_17.gltf', (gltf) => {
          this.scene.add(gltf.scene);
        });
    
        this.renderer = new THREE.WebGLRenderer({
          canvas: map.getCanvas(),
          context: gl,
          antialias: true
        });
        this.renderer.autoClear = false;
      }
    
      render(_gl: WebGL2RenderingContext | WebGLRenderingContext, args: maplibregl.CustomRenderMethodInput) {
        const modelOrigin: [number, number] = [148.9819, -35.39847];
        const modelAltitude = 0;
        const scaling = 10_000.0;
        const m = new THREE.Matrix4().fromArray(args.defaultProjectionData.mainMatrix);
        
        // Switch matrix based on projection transition
        const l = args.defaultProjectionData.projectionTransition > 0
          ? getGlobeModelMatrix(modelOrigin, modelAltitude)
          : getMercatorModelMatrix(modelOrigin, modelAltitude);
        
        l.scale(new THREE.Vector3(scaling, scaling, scaling));
    
        this.camera.projectionMatrix = m.multiply(l);
        this.renderer!.resetState();
        this.renderer!.render(this.scene, this.camera);
        this.map!.triggerRepaint();
      }
    }
    
    const customLayerImpl = new CustomLayerImpl();
  8. Install svelte-maplibre-gl in a SvelteKit project

    To use svelte-maplibre-gl, first create a SvelteKit project. It is recommended to enable the Tailwind CSS add-on during project creation as the library's examples rely on it for styling. Then, install the package as a development dependency.
    # Create SvelteKit project
    npx sv create myapp
    cd myapp
    npm install
    
    # Install svelte-maplibre-gl
    npm install -D svelte-maplibre-gl
  9. Implement custom URL protocols in svelte-maplibre-gl

    Use the <Protocol /> component to add a custom resource loading function that handles a specific URL scheme. This allows you to intercept requests for tiles or resources and provide custom data (such as dynamically generated images or specialized file formats) instead of standard HTTP requests. For MapLibre GL JS v6+, ensure svelte-maplibre-gl/vite is imported.
    import 'svelte-maplibre-gl/vite'; // Required for GL JS v6+
    import { Protocol, MapLibre, RasterTileSource, RasterLayer } from 'svelte-maplibre-gl';
    import type * as maplibregl from 'maplibre-gl';
    
    // Define the loading function
    const myProtocolLoadFn: maplibregl.AddProtocolAction = async (params) => {
      const zxy = params.url.replace('myprotocol://', '');
      const [z, x, y] = zxy.split('/').map((v) => parseInt(v, 10));
    
      // Example: Generate a dynamic PNG canvas
      const png = await new Promise((resolve) => {
        const canvas = document.createElement('canvas');
        canvas.width = 512;
        canvas.height = 512;
        const context = canvas.getContext('2d')!;
        context.fillStyle = 'white';
        context.fillText(`${z}/${x}/${y}`, 32, 64);
        canvas.toBlob(async (blob) => resolve(await blob!.arrayBuffer()));
      });
    
      return { data: png };
    };

    <!-- In your Svelte component --> <Protocol scheme="myprotocol" loadFn={myProtocolLoadFn} />

    <MapLibre center={[140.0, 37.5]} zoom={6}> <RasterTileSource tiles={['myprotocol://{z}/{x}/{y}']} tileSize={256}> <RasterLayer /> </RasterTileSource> </MapLibre>

  10. Implement PMTiles protocol in a Svelte MapLibre application

    To use PMTiles files as a vector source in svelte-maplibre-gl, you must include the <PMTilesProtocol /> component globally in your application. This enables the pmtiles:// protocol handler, allowing MapLibre to fetch tiles directly from a .pmtiles URL. When defining the map style, set the source url using the pmtiles:// prefix followed by the absolute URL of the PMTiles file.
    import 'svelte-maplibre-gl/vite'; // Required for GL JS v6+
    import { MapLibre } from 'svelte-maplibre-gl';
    import { PMTilesProtocol } from '@svelte-maplibre-gl/pmtiles';
    
    // In your Svelte component:
    <PMTilesProtocol />
    
    <MapLibre
      style={{
        version: 8,
        sources: {
          openmaptiles: {
            type: 'vector',
            url: 'pmtiles://https://tile.openstreetmap.jp/static/planet.pmtiles',
            attribution: 'OSM Japan © OpenMapTiles © OpenStreetMap contributors'
          }
        },
        layers: [] // Your layer specifications
      }}
    />
  11. Install Svelte MapLibre GL

    Svelte MapLibre GL is a Svelte 5 wrapper for MapLibre GL JS that allows for declarative and reactive map building while still supporting traditional imperative MapLibre GL JS usage.
    npm install --save-dev svelte-maplibre-gl
  12. Implement a split-screen map view

    To create a split-screen effect where two maps are visible side-by-side but maintain a single unified coordinate space, use the padding prop on each MapLibre component. By setting the padding of one map to cover half the container's width, the map's center is offset to the visible portion of the screen.
    <!-- Left Map -->
    <MapLibre
      padding={{ left: 0, right: width / 2, top: 0, bottom: 0 }}
      ... 
    />
    
    <!-- Right Map -->
    <MapLibre
      padding={{ left: width / 2, right: 0, top: 0, bottom: 0 }}
      ... 
    />