Skip to content

Conversation

@fank
Copy link
Member

@fank fank commented Feb 11, 2026

Summary

  • Eliminates MapLibre GL from the deck.gl renderer path entirely — vector tiles now render directly via deck.gl TileLayer + @loaders.gl/mvt, raster tiles via TileLayer + BitmapLayer
  • Compiles MapLibre GL style expressions (from topo.json) to JS accessor functions at load time, so the existing style pipeline (styles.go) requires no changes
  • Adds three performance optimizations: batched sub-layers by geometry type (~6 per tile vs ~30), stable getTileData reference to preserve tile cache across zoom, and RAF-batched flush instead of synchronous flushNow() during zoom gestures

New files

File Purpose
deckgl-expressions.ts Compile MapLibre expression arrays (["interpolate", ...]) to (props, zoom) => value functions
deckgl-style-parser.ts Parse topo.json style document into compiled layer definitions
deckgl-basemap.ts Build basemap layers: MVT vector tiles, raster tiles, background, sprite atlas
deckgl-scale-control.ts Minimal HTML scale bar (deck.gl has no built-in scale control)

What we lose (acceptable)

Feature Impact
Contour labels along curves (symbol-placement: "line") Labels render at point positions instead
SDF text rendering TextLayer uses bitmap text — slightly less crisp at extreme zoom
Font loading (glyphs) System sans-serif font used

Test plan

  • npx vite build — no errors, deck.gl still code-split (~911KB chunk)
  • npx vitest run — all 673 tests pass
  • Visual check: sea/land fills, contour lines, roads, buildings, satellite raster tiles
  • Pan/zoom smoothness — no 70ms+ jank frames in DevTools performance recording
  • Entity markers render and move during playback
  • Verify ?renderer=leaflet still works (MapLibre unchanged for Leaflet path)

fank added 3 commits February 10, 2026 21:50
Add a new DeckGLRenderer that uses MapLibre GL standalone + deck.gl
overlay for WebGL-accelerated entity rendering. Selectable via
?renderer=deckgl URL param; deck.gl bundle (~817KB) is only loaded
when requested.

Pre-work: extract shared constants (icon sizes/paths/states,
transition duration, grid utils) from Leaflet-specific files into
src/renderers/shared/ so both renderers can reuse them.
…ibre tuning

- Replace markDirty() with per-collection dirty methods (dirtyEntities,
  dirtyLines, dirtyBriefing, dirtyPulses) and revision counters
- Cache data arrays in DeckState to avoid Array.from() every frame
- Use revision counters as updateTriggers instead of N-element arrays
- Add MapLibre ScaleControl, maxBounds, renderWorldCopies: false,
  fadeDuration: 0, maxTileCacheSize: 256, collectResourceTiming: false
- Enable interleaved: true for camera-synced rendering
- Add CSS offsets for MapLibre control containers matching panel layout
Eliminate MapLibre GL entirely from the deck.gl renderer path — vector
tiles are now rendered directly via deck.gl TileLayer + MVTLoader, and
raster tiles via TileLayer + BitmapLayer.

New files:
- deckgl-expressions.ts: compile MapLibre GL style expressions to JS fns
- deckgl-style-parser.ts: parse topo.json into compiled layer definitions
- deckgl-basemap.ts: MVTLayer, raster, background, sprite atlas builders
- deckgl-scale-control.ts: HTML scale bar (deck.gl has no built-in one)

Performance optimizations:
- Batch vector sub-layers by geometry type (~6 per tile instead of ~30)
- Stable getTileData reference preserves tile cache across zoom changes
- RAF-batched flush on zoom change instead of synchronous flushNow()

Deps: +@deck.gl/geo-layers, -@deck.gl/mapbox
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @fank, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant architectural change by integrating a new deck.gl-based map renderer. The primary goal is to enhance rendering performance and flexibility by moving away from MapLibre GL for basemap display. It achieves this by directly processing vector and raster tiles within deck.gl, compiling MapLibre styles for compatibility, and implementing several performance-focused optimizations. This change allows for a more modern and efficient rendering pipeline while maintaining the existing map styling definitions.

Highlights

  • New Deck.gl Renderer: Introduced a new renderer based on deck.gl, completely replacing MapLibre GL for basemap rendering. This allows vector tiles to be rendered directly via deck.gl's TileLayer and @loaders.gl/mvt, and raster tiles via TileLayer + BitmapLayer.
  • MapLibre Style Expression Compilation: Implemented a system to compile MapLibre GL style expressions (from topo.json) into JavaScript accessor functions at load time, ensuring compatibility with the existing style pipeline without requiring changes to styles.go.
  • Performance Optimizations: Incorporated three key performance improvements: batched sub-layers by geometry type (reducing draw calls significantly), a stable getTileData reference to preserve the tile cache across zoom levels, and requestAnimationFrame-batched flushes instead of synchronous flushNow() during zoom gestures.
  • Dynamic Renderer Selection: Enabled dynamic switching between the new deck.gl renderer and the existing Leaflet renderer using a URL parameter (?renderer=deckgl), facilitating testing and gradual rollout.
  • Shared Utilities Refactoring: Moved common utility functions for grid calculations, icon constants, and transition durations into a new shared/ directory, promoting code reuse and better organization across different renderers.
Changelog
  • static-next/package-lock.json
    • Added numerous new dependencies related to deck.gl, @loaders.gl, and @luma.gl for the new rendering stack.
    • Removed the 'flatbuffers' dependency.
    • Updated various other package versions to their latest compatible releases.
  • static-next/package.json
    • Added core deck.gl dependencies: '@deck.gl/core', '@deck.gl/geo-layers', and '@deck.gl/layers'.
  • static-next/src/App.tsx
    • Removed direct import of LeafletRenderer.
    • Modified App component to accept MapRenderer as a prop, enabling dynamic renderer injection.
    • Updated parseUrlParams to include a renderer parameter for selecting the map rendering engine.
  • static-next/src/main.tsx
    • Implemented asynchronous bootstrapping to dynamically load either DeckGLRenderer or LeafletRenderer based on a URL parameter.
    • Passed the selected renderer instance to the App component.
  • static-next/src/renderers/deckgl/deckgl-basemap.ts
    • Added new file: Implements logic for building deck.gl basemap layers (vector, raster, background) from compiled MapLibre styles.
    • Includes functions for loading sprite atlases and creating stable getTileData fetchers for PMTiles vector sources.
  • static-next/src/renderers/deckgl/deckgl-expressions.ts
    • Added new file: Provides functionality to compile MapLibre GL style expressions and filters into executable JavaScript functions.
    • Includes a utility for parsing CSS color strings into RGBA arrays.
  • static-next/src/renderers/deckgl/deckgl-icon-atlas.ts
    • Added new file: Manages the creation of a single-canvas icon atlas for entity markers, optimizing icon rendering by batching different types and states.
  • static-next/src/renderers/deckgl/deckgl-layers.ts
    • Added new file: Contains functions responsible for constructing various deck.gl layers, including entity icons and labels, fire lines, briefing polygons, paths, icons, and pulse effects.
  • static-next/src/renderers/deckgl/deckgl-renderer.ts
    • Added new file: Implements the MapRenderer interface using deck.gl, managing the map's lifecycle, view state, basemap integration, and all custom overlay layers.
    • Handles camera controls, entity marker management, briefing markers, lines, pulses, layer visibility, and smoothing settings.
  • static-next/src/renderers/deckgl/deckgl-scale-control.ts
    • Added new file: Provides a custom HTML scale bar implementation for the deck.gl renderer, as deck.gl does not include a built-in one.
  • static-next/src/renderers/deckgl/deckgl-style-parser.ts
    • Added new file: Parses MapLibre GL style documents into a compiled structure, converting style properties and filters into functions for use with deck.gl.
  • static-next/src/renderers/leaflet/tests/grid-utils.test.ts
    • Updated import path for grid-utils to reference the new shared location.
  • static-next/src/renderers/leaflet/leaflet-grid.ts
    • Updated import path for grid utility functions to reference the new shared location.
  • static-next/src/renderers/leaflet/leaflet-icons.ts
    • Refactored to import icon-related constants and utility functions from the new shared icon-constants.ts file.
  • static-next/src/renderers/leaflet/leaflet-smoothing.ts
    • Refactored to import getTransitionDuration from the new shared transitions.ts file.
  • static-next/src/renderers/mock-renderer.ts
    • Added a setNameDisplayMode method to the mock renderer to align with the updated MapRenderer interface.
  • static-next/src/renderers/shared/grid-utils.ts
    • Renamed file from static-next/src/renderers/leaflet/grid-utils.ts to static-next/src/renderers/shared/grid-utils.ts.
  • static-next/src/renderers/shared/icon-constants.ts
    • Added new file: Centralizes definitions for icon sizes, paths, states, and the aliveVariant function, previously scattered across renderer implementations.
  • static-next/src/renderers/shared/transitions.ts
    • Added new file: Centralizes the getTransitionDuration function, previously defined within leaflet-smoothing.ts.
  • static-next/src/ui/tests/App.test.tsx
    • Updated App component rendering in tests to pass a mock renderer instance.
    • Added a mock for storage-factory to prevent actual storage access during tests.
  • static-next/src/ui/styles/global.css
    • Added CSS rules for MapLibre GL control positioning to match Leaflet offsets.
    • Added styling for the MapLibre GL scale ruler to fit the dark theme.
Activity
  • The pull request introduces a new deck.gl renderer, marking a significant shift in the map rendering architecture.
  • New files for deck.gl specific logic, expression parsing, icon atlas generation, layer building, and state management have been added.
  • Existing Leaflet-related utility files have been refactored and moved to a shared directory to support multiple renderer implementations.
  • Dependencies have been extensively updated to include deck.gl and its associated libraries, while removing some older ones.
  • The main application entry point has been modified to allow dynamic selection of the rendering engine via URL parameters.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an impressive and significant refactoring that replaces MapLibre GL with a pure deck.gl rendering path for the basemap. The changes are well-structured, introducing a new DeckGLRenderer and a suite of helper modules for style parsing, state management, and layer generation. I appreciate the clear focus on performance, with several key optimizations like batched sub-layers, stable tile data fetching, and RAF-batched rendering updates. The refactoring of shared utilities into a common directory is also a great improvement for code maintainability.

I've found a couple of potential rendering bugs in the new deck.gl implementation and a minor code duplication issue. My detailed feedback is in the review comments. Overall, this is a high-quality contribution that modernizes the rendering stack.

Comment on lines +205 to +224
let pixelOffset: [number, number] = [0, 0];
if (textOffset) {
const off = textOffset({}, zoom);
if (Array.isArray(off)) pixelOffset = [off[0] * 16, off[1] * 16];
}

for (const f of filtered) {
const text = textField(f.properties, zoom);
if (text != null && String(text) !== "") {
texts.push({
position: f.geometry?.coordinates ?? [0, 0],
_text: String(text),
_size: evalNumber(textSize, f.properties, zoom, 14),
_color: evalColor(layer.paint["text-color"], f.properties, zoom, "#000"),
_anchor: mappedAnchor,
_pixelOffset: pixelOffset,
});
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The calculation for pixelOffset seems incorrect. It's calculated once per style layer using a hardcoded font size of 16, but MapLibre's text-offset unit is em, which is relative to the text-size. Since text-size can be data-driven and is evaluated per-feature, the offset should also be calculated per-feature using the evaluated size. Additionally, textOffset itself is evaluated with empty properties, which would be incorrect if it's also data-driven.

          for (const f of filtered) {
            const text = textField(f.properties, zoom);
            if (text != null && String(text) !== "") {
              const size = evalNumber(textSize, f.properties, zoom, 14);
              let pixelOffset: [number, number] = [0, 0];
              if (textOffset) {
                const off = textOffset(f.properties, zoom);
                if (Array.isArray(off)) {
                  pixelOffset = [off[0] * size, off[1] * size];
                }
              }
              texts.push({
                position: f.geometry?.coordinates ?? [0, 0],
                _text: String(text),
                _size: size,
                _color: evalColor(layer.paint["text-color"], f.properties, zoom, "#000"),
                _anchor: mappedAnchor,
                _pixelOffset: pixelOffset,
              });
            }
          }

]));
}
poly.polygon = ring;
poly.fillColor[3] = Math.round(Math.min(poly.fillColor[3] / 255, state.alpha) * 255);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic for updating the polygon's fill opacity appears to be stateful in a way that might be unintentional. It uses the polygon's current alpha (poly.fillColor[3]) in the calculation. This means if state.alpha ever decreases, the polygon's alpha will be permanently capped at that lower value, even if state.alpha increases again in subsequent frames. The calculation should likely use the base fill opacity defined by the marker's brush type, which is stored in internal.shapeOpts.

Suggested change
poly.fillColor[3] = Math.round(Math.min(poly.fillColor[3] / 255, state.alpha) * 255);
poly.fillColor[3] = Math.round(Math.min(internal.shapeOpts.fillOpacity, state.alpha) * 255);

cy + sin * dx + cos * dy,
]),
);
poly.fillColor[3] = Math.round(Math.min(poly.fillColor[3] / 255, state.alpha) * 255);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the ellipse shape, the logic for updating the rectangle's fill opacity appears to be stateful in a way that might be unintentional. It uses the polygon's current alpha (poly.fillColor[3]) in the calculation. This means if state.alpha ever decreases, the polygon's alpha will be permanently capped at that lower value. The calculation should use the base fill opacity defined by the marker's brush type from internal.shapeOpts.

Suggested change
poly.fillColor[3] = Math.round(Math.min(poly.fillColor[3] / 255, state.alpha) * 255);
poly.fillColor[3] = Math.round(Math.min(internal.shapeOpts.fillOpacity, state.alpha) * 255);

Comment on lines +67 to +80
export function hexToRGBA(hex: string, alpha = 1): [number, number, number, number] {
let r = 0, g = 0, b = 0;
const h = hex.replace("#", "");
if (h.length === 3) {
r = parseInt(h[0] + h[0], 16);
g = parseInt(h[1] + h[1], 16);
b = parseInt(h[2] + h[2], 16);
} else if (h.length >= 6) {
r = parseInt(h.slice(0, 2), 16);
g = parseInt(h.slice(2, 4), 16);
b = parseInt(h.slice(4, 6), 16);
}
return [r, g, b, Math.round(alpha * 255)];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This hexToRGBA function duplicates color parsing logic found in deckgl-expressions.ts. The version in deckgl-expressions.ts (parseCssColor) is more comprehensive, handling rgb(), rgba(), and 8-digit hex values with alpha. To avoid code duplication and potential inconsistencies, consider using a single, robust color parsing utility. You could move parseCssColor to a shared utility file and use it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant