MapLibre GL JS Overview
mainmapbox-gl-js and has since evolved into a distinct project with additional functionality.repository·main·Indexed 11 days ago
https://github.com/maplibre/maplibre-gl-jsAn 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.
mapbox-gl-js and has since evolved into a distinct project with additional functionality.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:
pitch == 0.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.
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:
map.setCenter() or map.panTo() (via the Camera class) updates the transform directly.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.
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.
Pragmas follow this pattern:
#pragma maplibre: (define|initialize) (lowp|mediump|highp) (float|vec2|vec3|vec4) {name}
To use a pragma-defined variable, you must follow these rules:
define pragma and an initialize pragma.define pragmas must be placed in the file scope.initialize pragmas must be placed in the function scope (e.g., inside main()).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;
}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:
Map._render() loop detects that sources are dirty (Map._sourcesDirty === true).TileManager#update(transform) computes which tiles should cover the current viewport. Missing tiles are requested via Source#loadTile().WorkerTile.GlyphManager or ImageManager. Once fetched, the worker creates a GlyphAtlas and ImageAtlas.TileManager performs final steps like _backfillDEM() to prevent edge artifacts, and the map fires a sourcedata event to trigger a new render frame.The Globe projection renders vector polygons and lines on a unit sphere. The projection process follows three steps:
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.
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.
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.centerClampedToGround = true (default), the library automatically adjusts elevation to keep the center point on the terrain (or 0 MSL if no terrain is enabled).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).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;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:
Vector tile rendering is a multi-stage process split between WebWorker threads and the main thread to ensure smooth performance.
Vector tiles are fetched and processed on WebWorker threads to avoid blocking the main thread. This involves:
vector-tile-js.WorkerTile, Bucket classes, and ProgramConfiguration.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.
Once layout is complete, data is transferred to the main thread. The data structure follows this hierarchy:
Bucket instances.ArrayGroup data (vertex and element arrays).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:
Painter.BufferGroup and execute gl.drawElements().Painter and ProgramConfiguration manage the compilation and caching of GL shader programs. ProgramConfiguration handles:
#pragma maplibre statements in shader source into uniform, attribute, varying, or local variable declarations based on whether properties are data-driven.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:
Style#update(transform) to recompute paint properties for each layer based on the current zoom level and transition status.TileManager#update(transform) method is called to fetch any new tiles required for the current view.Painter#render(style) method orchestrates the actual drawing:Tile#upload(context) (which uses Bucket#upload(context)), and image textures (like icons or patterns) are uploaded via Tile#prepare(imageManager).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.src/shaders, and calls Program#draw() to execute gl.drawElements(), which renders the pixels to the screen.idle event.