maplibre-contour

repository·main·Indexed 43 days ago

https://github.com/onthegomap/maplibre-contour

A plugin for MapLibre GL JS that renders topographic contour lines on-the-fly from raster Digital Elevation Model (DEM) sources. It supports customizable thresholds, unit multipliers, and offloads isoline computation to web workers to maintain browser performance. Version 0.1.1.

Tokens
5K
Snippets
17
Records
24
Agent score
54%

What's inside maplibre-contour

  1. How maplibre-contour works

    main

    The library uses MapLibre's addProtocol to intercept vector tile requests and generate them on-the-fly from raster DEM data.

    The Pipeline:

    1. Fetching: DemManager fetches the raster-dem tile and its neighbors to ensure continuity across boundaries. If worker: true is set, this happens in a web worker via RemoteDemManager.
    2. Decoding: decode-image.ts converts RGB values from the raster tile into meters above sea level.
    3. Stitching: HeightTile creates a "virtual tile" by stitching raw DEM tiles, aligning measurements to the grid, and smoothing values.
    4. Isoline Generation: isoline.ts uses a marching-squares implementation (derived from d3-contour) to generate contour lines from the HeightTile.
    5. Encoding: vtpbf.ts encodes the resulting isolines into Mapbox Vector Tile (MVT) bytes.

    This approach allows for infinite variations of contour lines (units, thresholds, smoothing) without needing to host massive pre-generated vector tile datasets.

  2. Setup maplibre-contour using a Web Worker (Advanced)

    main

    For reduced bundle size and support for other protocols, you can register the contour logic directly in MapLibre workers. This requires using the importScriptInWorkers method and communicating via the global dispatcher.

    Workflow:

    1. Import the worker script via CDN.
    2. Listen for the contour-worker message via the dispatcher.
    3. Broadcast configuration parameters (like demUrlPattern, encoding, and maxzoom) to the worker.
    4. Add a source using the dem-contour:// protocol with a manually constructed URL string containing your parameters.
    maplibregl.importScriptInWorkers("https://unpkg.com/maplibre-contour@0.1.0/dist/add-protocol-worker.js");
    
    const dispatcher = maplibregl.getGlobalDispatcher();
    dispatcher.registerMessageHandler("contour-worker" as any, async () => {
    
      await dispatcher.broadcast("contour-worker" as any, {
          demUrlPattern: "https://url/of/dem/source/{z}/{x}/{y}.png", // this can also support other protocols such as pmtiles://
          encoding: "terrarium",
          maxzoom: 13
      });
    
      // once this is configured, you can add a source with the following address for example, and layers as described above.
      map.addSource("contour-source", {
        type: "vector",
        tiles: ["dem-contour://{z}/{x}/{y}?contourLayer=contours&elevationKey=ele&levelKey=level&multiplier=3.28084&overzoom=1&thresholds=11*200*1000~12*10*100~13*10*100~14*10*100~15*10*100"],
      });
    });
  3. Install maplibre-contour

    main

    You can install maplibre-contour using a script tag for direct browser usage or via npm for ES6 module environments.

    Script Tag:

    <script src="https://unpkg.com/maplibre-contour@0.1.0/dist/index.min.js"></script>

    NPM:

    npm add maplibre-contour

    ES6 Import:

    import mlcontour from "maplibre-contour";
    import mlcontour from "maplibre-contour";
  4. Render contour lines and labels

    main

    After adding the contour source, add standard MapLibre layers to visualize the lines and labels. The level property (determined by your thresholds) can be used to style line widths or filter labels.

    Example: Line Layer Uses match on the level property to vary line width.

    Example: Symbol Layer Uses filter to hide level 0 labels and text-field to display the elevation (ele).

    map.addLayer({
      id: "contour-lines",
      type: "line",
      source: "contour-source",
      "source-layer": "contours",
      paint: {
        "line-color": "rgba(0,0,0, 50%)",
        // level = highest index in thresholds array the elevation is a multiple of
        "line-width": ["match", ["get", "level"], 1, 1, 0.5],
      },
    });
    map.addLayer({
      id: "contour-labels",
      type: "symbol",
      source: "contour-source",
      "source-layer": "contours",
      filter: [">", ["get", "level"], 0],
      layout: {
        "symbol-placement": "line",
        "text-size": 10,
        "text-field": ["concat", ["number-format", ["get", "ele"], {}], "'"],
        "text-font": ["Noto Sans Bold"],
      },
      paint: {
        "text-halo-color": "white",
        "text-halo-width": 1,
      },
    });
  5. Add contour vector source to MapLibre

    main

    Once DemSource is initialized, use its contourProtocolUrl method to generate the tile URL for a MapLibre vector source. This allows you to define specific contour parameters like thresholds and unit multipliers on-the-fly.

    contourProtocolUrl Options:

    • multiplier: Scale factor for elevation (e.g., 3.28084 to convert meters to feet). Default is 1 (meters).
    • thresholds: An object mapping zoom levels to [minor, major] contour intervals. Example: { 11: [200, 1000] }.
    • contourLayer: (Optional) Override the vector tile layer name. Default is "contours".
    • elevationKey: (Optional) Override the elevation property key. Default is "ele".
    • levelKey: (Optional) Override the contour level property key. Default is "level".
    • extent: (Optional) Vector tile extent. Default is 4096.
    • buffer: (Optional) Tile buffer. Default is 1.
    map.addSource("contour-source", {
      type: "vector",
      tiles: [
        demSource.contourProtocolUrl({
          // convert meters to feet, default=1 for meters
          multiplier: 3.28084,
          thresholds: {
            // zoom: [minor, major]
            11: [200, 1000],
            12: [100, 500],
            14: [50, 200],
            15: [20, 100],
          },
          // optional, override vector tile parameters:
          contourLayer: "contours",
          elevationKey: "ele",
          levelKey: "level",
          extent: 4096,
          buffer: 1,
        }),
      ],
      maxzoom: 15,
    });
  6. Share cached DEM tiles with other sources

    main

    To avoid redundant network requests, you can share the tiles cached by DemSource with other MapLibre sources (like a raster-dem source for hillshading) using demSource.sharedDemProtocolUrl.

    map.addSource("dem", {
      type: "raster-dem",
      encoding: "terrarium",
      tiles: [demSource.sharedDemProtocolUrl],
      maxzoom: 13,
      tileSize: 256,
    });
  7. Configure DemSource for contour generation

    main

    To generate contours, you must first create a DemSource instance and register it with your MapLibre GL JS instance using setupMaplibre. This object manages the fetching, caching, and decoding of elevation data.

    Configuration Options:

    • url: The URL template for the raster-dem source (e.g., https://url/of/dem/source/{z}/{x}/{y}.png).
    • encoding: The DEM encoding format. Supports "mapbox" or "terrarium" (default: "terrarium").
    • maxzoom: Maximum zoom level for the DEM source.
    • worker: Boolean. If true, offloads isoline computation to a web worker to prevent main-thread jank.
    • cacheSize: Number of most-recent tiles to keep in cache.
    • timeoutMs: Timeout for fetch requests in milliseconds.
    var demSource = new mlcontour.DemSource({
      url: "https://url/of/dem/source/{z}/{x}/{y}.png",
      encoding: "terrarium", // "mapbox" or "terrarium" default="terrarium"
      maxzoom: 13,
      worker: true, // offload isoline computation to a web worker to reduce jank
      cacheSize: 100, // number of most-recent tiles to cache
      timeoutMs: 10_000, // timeout on fetch requests,
    });
    demSource.setupMaplibre(maplibregl);
  8. Configure DemSource options

    main

    The DemSource constructor accepts an options object with the following properties:

    OptionTypeDefaultDescription
    urlstringRequiredRemote DEM tile URL using {z}, {x}, and {y} placeholders.
    maxzoomnumberRequiredMaximum zoom level of tiles contained in the source.
    idstring'dem'Prefix for the MapLibre protocols. If the ID is already in use, it will be incremented (e.g., dem1).
    encoding'terrarium' | 'mapbox''terrarium'The encoding format of the DEM tiles.
    cacheSizenumber100Number of most-recently-used tiles to keep in cache.
    workerbooleantrueIf true, uses RemoteDemManager to handle requests in a shared web worker. If false, uses LocalDemManager.
    timeoutMsnumber10000Request timeout in milliseconds.
    actorActor<WorkerDispatch>undefinedHandle for communication if using a worker.
  9. Use the maplibre-contour package entrypoint

    main

    The maplibre-contour package provides tools for generating isolines (contour lines) from Digital Elevation Model (DEM) data. The default export provides access to the core isoline generation logic, DEM source management, and worker configuration.

    import maplibreContour from 'maplibre-contour';
    
    // Access core features
    const { generateIsolines, DemSource, LocalDemManager } = maplibreContour;
    
    // Configure the worker URL
    maplibreContour.workerUrl = '/path/to/your/worker.js';
  10. Configure the worker URL via workerUrl

    main

    To ensure the library can correctly spawn its background worker, you can set the workerUrl property on the default export. This is useful when your build system or deployment structure requires a specific path to the worker script.

    import maplibreContour from 'maplibre-contour';
    
    maplibreContour.workerUrl = 'https://cdn.example.com/maplibre-contour-worker.js';
  11. Use DemManager to fetch contour tiles

    main

    Once a DemManager is initialized, you can use its fetchContourTile method to generate vector tiles containing contour lines.

    Method Signature: fetchContourTile(z: number, x: number, y: number, options: IndividualContourTileOptions, abortController: AbortController, timer?: Timer): Promise<ContourTile>

    • z, x, y: The tile coordinates.
    • options: An IndividualContourTileOptions object defining the specific elevation levels.
    • abortController: An AbortController to cancel the request.
    • Returns: A Promise resolving to a ContourTile containing the encoded Mapbox vector tile arrayBuffer.
    export interface DemManager {
      // ...
      fetchContourTile(
        z: number,
        x: number,
        y: number,
        options: IndividualContourTileOptions,
        abortController: AbortController,
        timer?: Timer,
      ): Promise<ContourTile>;
    }
  12. Generate contour tile URLs

    main

    Use the contourProtocolUrl method to generate a URL that can be used in a MapLibre layer (e.g., in a vector source). This URL includes the correct protocol prefix and encodes the GlobalContourTileOptions as query parameters.

    Example usage in a MapLibre style or layer configuration:

    const contourUrl = demSource.contourProtocolUrl({
      // ... your GlobalContourTileOptions
    });
    
    // The resulting URL will look like:
    // 'my-dem-contour://{z}/{x}/{y}?option1=val1&option2=val2'
    const contourUrl = demSource.contourProtocolUrl({
      // options here
    });