Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 97 additions & 1 deletion modules/mapbox/src/mapbox-overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export default class MapboxOverlay implements IControl {
private _container?: HTMLDivElement;
private _interleaved: boolean;
private _lastMouseDownPoint?: {x: number; y: number; clientX: number; clientY: number};
private _resizeObserver?: ResizeObserver;
private _lastCanvasSize?: {width: number; height: number};
private _mapResizeHandler?: () => void;

constructor(props: MapboxOverlayProps) {
const {interleaved = false} = props;
Expand Down Expand Up @@ -140,12 +143,31 @@ export default class MapboxOverlay implements IControl {
gl,
parameters: {...getDefaultParameters(map, false), ...this._props.parameters},
deviceProps: {
createCanvasContext: {autoResize: true}
createCanvasContext: {
autoResize: false
}
}
})
});

map.on('styledata', this._handleStyleChange);
// Note: map.on('resize') doesn't provide dimensions, so we don't pass arguments
this._mapResizeHandler = () => this._handleInterleavedResize();
map.on('resize', this._mapResizeHandler);

// Watch for canvas size changes (including DPR changes)
// When DPR changes, the map adjusts canvas.width/height but doesn't fire 'resize' event
const canvas = map.getCanvas();
this._resizeObserver = new ResizeObserver(() => {
this._handleInterleavedResize();
});
try {
this._resizeObserver.observe(canvas, {box: 'device-pixel-content-box'});
} catch (e) {
// Fallback for Safari - use content-box
this._resizeObserver.observe(canvas, {box: 'content-box'});
}

resolveLayers(map, this._deck, [], this._props.layers);

return document.createElement('div');
Expand Down Expand Up @@ -184,6 +206,12 @@ export default class MapboxOverlay implements IControl {

private _onRemoveInterleaved(map: Map): void {
map.off('styledata', this._handleStyleChange);
if (this._mapResizeHandler) {
map.off('resize', this._mapResizeHandler);
this._mapResizeHandler = undefined;
}
this._resizeObserver?.disconnect();
this._resizeObserver = undefined;
resolveLayers(map, this._deck, this._props.layers, []);
removeDeckInstance(map);
}
Expand Down Expand Up @@ -238,6 +266,74 @@ export default class MapboxOverlay implements IControl {
}
};

private _handleInterleavedResize = () => {
if (!this._deck || !this._map) return;

// Wait for deck to be initialized before syncing sizes
// @ts-ignore - accessing private device property
if (!this._deck.device || !this._deck.isInitialized) {
return;
}

const canvas = this._map.getCanvas();
// @ts-ignore - accessing private device property
const device = this._deck.device;

// IMPORTANT: In interleaved mode, the map controls the canvas size.
// Maplibre/Mapbox may keep canvas.width/height at CSS pixel dimensions,
// NOT device pixel dimensions. We should always use the actual canvas.width/height
// as the drawing buffer size, not the ResizeObserver's device pixel size.
const drawingBufferWidth = canvas.width;
const drawingBufferHeight = canvas.height;

// Skip if size hasn't changed
if (
this._lastCanvasSize?.width === drawingBufferWidth &&
this._lastCanvasSize?.height === drawingBufferHeight
) {
return;
}
this._lastCanvasSize = {width: drawingBufferWidth, height: drawingBufferHeight};

// Synchronize luma.gl's drawing buffer size tracking with the actual canvas dimensions
// This is needed because in interleaved mode, deck shares the GL context with the map,
// and the map controls the canvas size, but luma.gl needs to know about size changes
if (device?.canvasContext) {
// @ts-ignore - directly updating internal state since canvas is externally managed
device.canvasContext.drawingBufferWidth = drawingBufferWidth;
// @ts-ignore
device.canvasContext.drawingBufferHeight = drawingBufferHeight;
}

// Also sync CSS size for cssToDeviceRatio() to work correctly
const cssWidth = canvas.clientWidth;
const cssHeight = canvas.clientHeight;
if (device?.canvasContext) {
// @ts-ignore - cssWidth/cssHeight are public but TS doesn't see them
device.canvasContext.cssWidth = cssWidth;
// @ts-ignore
device.canvasContext.cssHeight = cssHeight;
}

// In interleaved mode, deck's width/height should be in CSS pixels
// The drawing buffer is in device pixels, and cssToDeviceRatio() provides the scaling
// @ts-expect-error - writing to readonly property
this._deck.width = cssWidth;
// @ts-expect-error - writing to readonly property
this._deck.height = cssHeight;

// Update viewManager with CSS pixel size
// @ts-expect-error - accessing protected property
this._deck.viewManager?.setProps({width: cssWidth, height: cssHeight});

// Request a redraw to ensure deck renders with the new size
// This is especially important for DPR changes where the map may not immediately trigger a render
this._deck.redraw();

// Also trigger the map to repaint to fix white-out issue
this._map.triggerRepaint();
};

private _updateContainerSize = () => {
if (this._map && this._container) {
const {clientWidth, clientHeight} = this._map.getContainer();
Expand Down
11 changes: 11 additions & 0 deletions test/modules/mapbox/mapbox-gl-mock/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export default class Map extends Evented {
super();

this._container = document.createElement('div');
this._canvas = document.createElement('canvas');
this._canvas.width = 800;
this._canvas.height = 600;
this.options = options;
this.version = options.version;
this.style = new Style(options.style);
Expand All @@ -39,6 +42,14 @@ export default class Map extends Evented {
return this._container;
}

getCanvas() {
return this._canvas;
}

getPixelRatio() {
return 1;
}

getCenter() {
return this.transform.center;
}
Expand Down
Loading