MapLibre GL JS

repository·main·Indexed 11 days ago

https://github.com/maplibre/maplibre-gl-js

An open-source, high-performance mapping library for web and webview applications. A BSD-licensed community fork of mapbox-gl, it uses GPU-accelerated vector tile rendering and supports features such as Globe projection, custom GLSL shader pragmas, and programmatic camera control. Version 6.3.0.

Tokens
26.9K
Snippets
74
Records
117
Agent score
93%

What's inside MapLibre

  1. MapLibre GL JS Overview

    main
    MapLibre GL JS is an open-source library for publishing interactive maps on websites or webview-based applications. It utilizes GPU-accelerated vector tile rendering to ensure high-performance map display. It originated as a fork of mapbox-gl-js and has since evolved into a distinct project with additional functionality.
  2. Understand how `pitchTileLoadingBehavior` affects tile loading

    main

    The pitchTileLoadingBehavior parameter (represented by the variable $b$ in internal calculations) determines how tiles are loaded based on the camera's pitch angle. This parameter controls the relationship between the requested center scale factor and the actual tile scale factor, effectively deciding whether tiles are loaded based on screen width, area, or height.

    Common behaviors for $b$ include:

    • $b = 0$: Tiles are loaded with approximately equal screen width. This matches behavior when pitch == 0.
    • $b = 1$ (Default): Tiles are loaded with approximately equal screen area.
    • $b = 2$: Tiles are loaded with approximately equal screen height.
    • $b = -1$: All tiles are loaded at the same zoom level, meaning all tiles change zoom level simultaneously.
  3. How `maxZoomLevelsOnScreen` is calculated

    main

    The number of zoom levels visible on the screen ($N$) is determined by the difference between the maximum and minimum zoom levels ($Z_{max}$ and $Z_{min}$) required to cover the visible area.

    Mathematically, it is expressed as: $$N = Z_{max} - Z_{min} + 1 = \log_2(\frac{S(\theta_{min})}{S(\theta_{max})}) + 1$$

    Where $S$ is the tile scale factor. The value of $N$ is maximized when the maximum pitch angle ($ heta_{max}$) reaches the horizon. The parameter $b$ (pitchTileLoadingBehavior) directly influences this value.

  4. How the Event Loop updates map state and triggers rendering

    main

    The map's state is managed by the transform object, which holds viewport details like pitch, zoom, bearing, and bounds. The transform is updated via two primary mechanisms:

    • Explicit Camera Calls: Using methods like map.setCenter() or map.panTo() (via the Camera class) updates the transform directly.
    • User Interactions (DOM Events): DOM events (resize, pan, click, scroll) are managed by the HandlerManager. It forwards events to interaction processors in src/ui/handler, which produce a HandlerResult. This result is used to nudge the map.transform and kick off a render frame loop. For continuous interactions like panning, the loop continues until inertia decreases to 0.

    Triggering a Render: Whenever the camera or handler_manager updates the transform, they fire map events such as move, zoom, movestart, or moveend. These events (along with style changes and data load events) trigger Map#_render(), which renders a single frame of the map.

  5. Use MapLibre GL JS shader pragmas

    main

    MapLibre GL JS uses custom GLSL pragmas to abstract over how variables are declared based on their context (uniforms vs. attributes/varyings). This allows you to define variables without manually managing the complexity of whether they are constant for all features or unique to each feature.

    Pragma Syntax

    Pragmas follow this pattern: #pragma maplibre: (define|initialize) (lowp|mediump|highp) (float|vec2|vec3|vec4) {name}

    Requirements for Use

    To use a pragma-defined variable, you must follow these rules:

    1. Dual Declaration: Every variable must have both a define pragma and an initialize pragma.
    2. Scope:
      • define pragmas must be placed in the file scope.
      • initialize pragmas must be placed in the function scope (e.g., inside main()).
    3. Vertex/Fragment Synchronization: If a variable is defined and initialized in the fragment shader, it must also be defined and initialized in the vertex shader. This is because attributes are not directly accessible in the fragment shader and must be passed through via interpolation (varyings) managed by the pragma system.
    #pragma maplibre: define highp vec4 color
    
    main() {
        #pragma maplibre: initialize highp vec4 color
        ...
        fragColor = color;
    }
  6. Understand the tile loading lifecycle

    main

    MapLibre GL JS uses a multi-threaded approach to load and process tiles to keep the main thread responsive. The process generally follows these steps:

    1. Trigger: The Map._render() loop detects that sources are dirty (Map._sourcesDirty === true).
    2. Tile Management: TileManager#update(transform) computes which tiles should cover the current viewport. Missing tiles are requested via Source#loadTile().
    3. Worker Processing: For heavy lifting (Vector, GeoJSON, or DEM tiles), the work is offloaded to a Web Worker. The worker fetches data, decodes it (e.g., PBF for vector tiles), and parses it into a WorkerTile.
    4. Data Dependency Resolution: If a layer needs glyphs (fonts) or images (icons/patterns), the worker requests these from the main thread's GlyphManager or ImageManager. Once fetched, the worker creates a GlyphAtlas and ImageAtlas.
    5. Geometry Preparation: The worker computes layout properties, creates buckets, and triangulates features (converting them into GPU-ready buffers).
    6. Main Thread Integration: The processed data (buckets, atlases, collision boxes) is sent back to the main thread. The TileManager performs final steps like _backfillDEM() to prevent edge artifacts, and the map fires a sourcedata event to trigger a new render frame.
  7. How Globe projection works

    main

    The Globe projection renders vector polygons and lines on a unit sphere. The projection process follows three steps:

    1. Compute angular spherical coordinates from source Web Mercator tile data.
    2. Convert spherical coordinates to a 3D vector (a point on the surface of a unit sphere).
    3. Project the 3D vector using a common perspective projection matrix.

    Geometry is projected to the sphere within the vertex shader using the projectTile function. This function accepts a 2D vector of coordinates (range 0..EXTENT, where EXTENT is 8192) and returns the final projection for gl_Position.

  8. How `tileCountMaxMinRatio` affects tile loading

    main

    The tileCountMaxMinRatio is used to control the ratio of the total tile area at a given pitch compared to the tile area when pitch == 0.

    This ratio is used to calculate the center scale factor ($S_c$) and the center zoom level ($Z_c$) to ensure consistent tile density and performance across different camera angles. The calculation involves integrating the cosine of the pitch angle over the visible field of view, often utilizing the hypergeometric function ${}_2F_1()$ to solve the integral.

  9. How camera position is calculated from center point and zoom

    main

    In MapLibre GL JS, the camera's location is controlled indirectly via Transform variables: center, elevation, zoom, pitch, bearing, and fov.

    • elevation: Sets the height of the 'center point' above sea level.
      • If centerClampedToGround = true (default), the library automatically adjusts elevation to keep the center point on the terrain (or 0 MSL if no terrain is enabled).
      • If centerClampedToGround = false, the user manually provides the elevation of the center point.
    • zoom: Sets the distance from the center point to the camera (working in conjunction with a hardcoded fovInRadians).
    • Altitude: The camera's altitude is a combination of the elevation of the center point and the distance calculated from zoom and pitch.

    To support a pitch greater than 90 degrees, you must set centerClampedToGround = false. This allows the 'center point' to be placed above the ground, preventing the camera from being forced underground when pitching steeply.

    // Conceptual logic for altitude calculation
    // altitude = (distance from camera to center) + center_elevation
    const altitude = Math.cos(this.pitchInRadians) * this._cameraToCenterDistance / this._helper._pixelPerMeter;
    return altitude + this.elevation;
  10. Understanding Globe subdivision

    main

    Because MapLibre triangulates geometry (polygons and lines) using the earcut algorithm, it can create very large triangles. If these large triangles were projected directly onto a sphere, they would appear deformed and fail to show curved horizons.

    To fix this, MapLibre performs subdivision on the geometry before a tile is finished loading. This creates a more granular mesh (ideally a square grid) that allows for smooth curves.

    Subdivision is configured in the Projection object and is governed by:

    • Base granularity: Defined by the tile zoom level.
    • Minimal granularity: A floor to prevent excessive subdivision.
    • Maximal granularity: Currently 128 for fill layers, which balances curved horizons against vertex index limits (16-bit).
  11. How vector tile rendering works in MapLibre GL JS

    main

    Vector tile rendering is a multi-stage process split between WebWorker threads and the main thread to ensure smooth performance.

    1. Parsing and Layout (Worker Thread)

    Vector tiles are fetched and processed on WebWorker threads to avoid blocking the main thread. This involves:

    • Deserialization: Converting PBF data into source layers, feature properties, and geometries using vector-tile-js.
    • Layout: Transforming deserialized data into render-ready data for WebGL shaders. This is performed by WorkerTile, Bucket classes, and ProgramConfiguration.
    • Indexing: Creating a FeatureIndex for spatial queries like queryRenderedFeatures.

    WorkerTile#parse() handles the deserialization, fetches required resources (fonts, images), and creates a Bucket for each 'family' of style layers that share the same layout properties.

    2. WebGL Rendering (Main Thread)

    Once layout is complete, data is transferred to the main thread. The data structure follows this hierarchy:

    • Tile: Contains multiple Bucket instances.
    • Bucket: The central authority for turning vector tiles into WebGL buffers. Each bucket holds ArrayGroup data (vertex and element arrays).
    • ArrayGroup: Contains globalProperties (like zoom), layoutVertexArray, indexArray, and layerData (which maps style layer IDs to their specific programConfiguration, paintVertexArray, and paintPropertyStatistics).

    Rendering is executed by Painter#renderPass(), which iterates through style layers and calls layer-specific drawXxxx() methods. These methods:

    1. Obtain a configured shader program from the Painter.
    2. Set uniform values based on style layer properties.
    3. Bind layout buffer data via BufferGroup and execute gl.drawElements().

    3. Shader Management

    Painter and ProgramConfiguration manage the compilation and caching of GL shader programs. ProgramConfiguration handles:

    • Expanding #pragma maplibre statements in shader source into uniform, attribute, varying, or local variable declarations based on whether properties are data-driven.
    • Creating and populating paint vertex arrays for data-driven properties (this occurs during the layout phase on the worker side).
  12. Understand the MapLibre GL JS render loop

    main

    The render loop is the process by which MapLibre GL JS updates the map view based on user interaction or data changes. When the map state is not dirty (map._sourcesDirty === false), the rendering process occurs on the main UI thread and follows these high-level steps:

    1. Property Recalculation: The map calls Style#update(transform) to recompute paint properties for each layer based on the current zoom level and transition status.
    2. Tile Management: The TileManager#update(transform) method is called to fetch any new tiles required for the current view.
    3. Painter Execution: The Painter#render(style) method orchestrates the actual drawing:
      • Tile Preparation: For each tile, vertex attributes are uploaded to the GPU via Tile#upload(context) (which uses Bucket#upload(context)), and image textures (like icons or patterns) are uploaded via Tile#prepare(imageManager).
      • Layer Rendering Passes: The painter executes four distinct passes over each layer to ensure correct visual stacking and effects:
        • offscreen pass: Used for precomputing and caching data (e.g., for hillshading or heatmaps) to an offscreen framebuffer.
        • opaque pass: Renders fill and background layers with no opacity from top to bottom.
        • translucent pass: Renders all other layers from bottom to top.
        • debug pass: Renders debug information like tile boundaries or collision boxes on top.
    4. GPU Drawing: For every visible tile in a layer, the engine binds textures, uses a shader program from src/shaders, and calls Program#draw() to execute gl.drawElements(), which renders the pixels to the screen.
    5. Repaint/Idle: The map triggers another repaint if more work is pending; otherwise, it emits an idle event.