Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

### 🐞 Bug fixes
- Fix missing `constrainOverride` setter in `TransformHelper.apply` ([#6642](https://github.com/maplibre/maplibre-gl-js/pull/6642)) (by [@larsmaxfield](https://github.com/larsmaxfield))
- Fix blank map after WebGL context restore ([#6242](https://github.com/maplibre/maplibre-gl-js/issues/6242)) (by [@ToHold](https://github.com/ToHold))

- _...Add new stuff here..._

## 5.11.0
Expand Down
15 changes: 14 additions & 1 deletion src/render/glyph_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export class GlyphManager {
unicodeBlockLookup['CJK Symbols and Punctuation'](id) || // 、。〃〄々〆〇〈〉《》「...
unicodeBlockLookup['Halfwidth and Fullwidth Forms'](id) // !?"#$%&...
);

}

/**
Expand Down Expand Up @@ -283,4 +283,17 @@ export class GlyphManager {
}
return match;
}

destroy() {
for (const stack in this.entries) {
const entry = this.entries[stack];
if (entry.tinySDF) {
entry.tinySDF = null;
}
entry.glyphs = {};
entry.requests = {};
entry.ranges = {};
}
this.entries = {};
}
}
28 changes: 27 additions & 1 deletion src/render/image_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ export class ImageManager extends Evented {
this.dirty = true;
}

destroy() {
// Destroy atlas texture if it exists
if (this.atlasTexture) {
this.atlasTexture.destroy();
this.atlasTexture = null;
}
// Remove all images and patterns
for (const id of Object.keys(this.images)) {
this.removeImage(id);
}

this.patterns = {};
this.atlasImage = new RGBAImage({width: 1, height: 1});
this.dirty = true;
}
isLoaded() {
return this.loaded;
}
Expand All @@ -90,7 +105,6 @@ export class ImageManager extends Evented {

getImage(id: string): StyleImage {
const image = this.images[id];

// Extract sprite image data on demand
if (image && !image.data && image.spriteData) {
const spriteData = image.spriteData;
Expand Down Expand Up @@ -334,4 +348,16 @@ export class ImageManager extends Evented {
}
}
}

cloneImages() {
const clonedImages: Record<string, StyleImage> = {};
for (const id in this.images) {
const image = this.images[id];
clonedImages[id] = {
...image,
data: image.data ? image.data.clone() : null
};
}
return clonedImages;
}
}
36 changes: 36 additions & 0 deletions src/render/painter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,9 +793,45 @@ export class Painter {
}

destroy() {
if (this._tileTextures) {
for (const size in this._tileTextures) {
const textures = this._tileTextures[size];
if (textures) {
for (const texture of textures) {
texture.destroy();
}
}
}
this._tileTextures = {};
}

if (this.tileExtentBuffer) this.tileExtentBuffer.destroy();
if (this.debugBuffer) this.debugBuffer.destroy();
if (this.rasterBoundsBuffer) this.rasterBoundsBuffer.destroy();
if (this.rasterBoundsBufferPosOnly) this.rasterBoundsBufferPosOnly.destroy();
if (this.viewportBuffer) this.viewportBuffer.destroy();
if (this.tileBorderIndexBuffer) this.tileBorderIndexBuffer.destroy();
if (this.quadTriangleIndexBuffer) this.quadTriangleIndexBuffer.destroy();
if (this.tileExtentMesh) this.tileExtentMesh.vertexBuffer?.destroy();
if (this.tileExtentMesh) this.tileExtentMesh.indexBuffer?.destroy();

if (this.debugOverlayTexture) {
this.debugOverlayTexture.destroy();
}

if (this.cache) {
for (const key in this.cache) {
const program = this.cache[key];
if (program && program.program) {
this.context.gl.deleteProgram(program.program);
}
}
this.cache = {};
}

if (this.context) {
this.context.setDefault();
}
}

/*
Expand Down
2 changes: 2 additions & 0 deletions src/source/canvas_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ export class CanvasSource extends ImageSource {
serialize(): CanvasSourceSpecification {
return {
type: 'canvas',
animate: this.animate,
canvas: this.options.canvas,
coordinates: this.coordinates
};
}
Expand Down
108 changes: 99 additions & 9 deletions src/style/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,15 +261,7 @@ export class Style extends Evented {
this.lineAtlas = new LineAtlas(256, 512);
this.crossTileSymbolIndex = new CrossTileSymbolIndex();

this._spritesImagesIds = {};
this._layers = {};

this._order = [];
this.tileManagers = {};
this.zoomHistory = new ZoomHistory();
this._loaded = false;
this._availableImages = [];
this._globalState = {};
this._setInitialValues();

this._resetUpdates();

Expand Down Expand Up @@ -300,6 +292,36 @@ export class Style extends Evented {
});
}

private _setInitialValues() {
this._spritesImagesIds = {};
this._layers = {};
this._order = [];
this.tileManagers = {};
this.zoomHistory = new ZoomHistory();
this._availableImages = [];
this._globalState = {};
this._serializedLayers = {};
this.stylesheet = null;
this.light = null;
this.sky = null;
if (this.projection) {
this.projection.destroy();
delete this.projection;
}
this._loaded = false;
this._changed = false;
this._updatedLayers = {};
this._updatedSources = {};
this._changedImages = {};
this._glyphsDidChange = false;
this._updatedPaintProps = {};
this._layerOrderChanged = false;
this.crossTileSymbolIndex = new (this.crossTileSymbolIndex?.constructor || Object)();
this.pauseablePlacement = undefined;
this.placement = undefined;
this.z = 0;
}

_rtlPluginLoaded = () => {
for (const id in this.tileManagers) {
const sourceType = this.tileManagers[id].getSource().type;
Expand Down Expand Up @@ -1997,4 +2019,72 @@ export class Style extends Evented {
}
}
}

/**
* Destroys all internal resources of the style (sources, images, layers, etc.)
*/
destroy() {
// cancel any pending requests
if (this._frameRequest) {
this._frameRequest.abort();
this._frameRequest = null;
}
if (this._loadStyleRequest) {
this._loadStyleRequest.abort();
this._loadStyleRequest = null;
}
if (this._spriteRequest) {
this._spriteRequest.abort();
this._spriteRequest = null;
}

// remove sourcecaches
for (const id in this.tileManagers) {
const tileManager = this.tileManagers[id];
tileManager.setEventedParent(null);

if (tileManager._tiles) {
for (const tileId in tileManager._tiles) {
const tile = tileManager._tiles[tileId];
tile.unloadVectorData();
}
tileManager._tiles = {};
}
tileManager._cache.reset();
tileManager.onRemove(this.map);
}
this.tileManagers = {};

// Destroy imageManager and clear images
if (this.imageManager) {
this.imageManager.setEventedParent(null);
this.imageManager.destroy();
this._availableImages = [];
this._spritesImagesIds = {};
}

// Destroy glyphManager
if (this.glyphManager) {
this.glyphManager.destroy();
}

// Remove layers
for (const layerId in this._layers) {
const layer = this._layers[layerId];
layer.setEventedParent(null);
if (layer.onRemove) layer.onRemove(this.map);
}

// reset internal state
this._setInitialValues();

// Remove event listeners
this.setEventedParent(null);
this.dispatcher.unregisterMessageHandler(MessageType.getGlyphs);
this.dispatcher.unregisterMessageHandler(MessageType.getImages);
this.dispatcher.unregisterMessageHandler(MessageType.getDashes);
this.dispatcher.remove(true);
this._listeners = {};
this._oneTimeListeners = {};
}
}
59 changes: 57 additions & 2 deletions src/ui/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export type MapOptions = {
*/
trackResize?: boolean;
/**
* The initial geographical centerpoint of the map. If `center` is not specified in the constructor options, MapLibre GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `[0, 0]`
* The initial geographical centerpoint of the map. If `center` is not specified in the constructor options, MapLibre GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `[0, 0]`
* !!! note
* MapLibre GL JS uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match GeoJSON.
* @defaultValue [0, 0]
Expand Down Expand Up @@ -387,6 +387,11 @@ type DelegatedListener = {

type Delegate<E extends Event = Event> = (e: E) => void;

type LostContextStyle = {
style: StyleSpecification | null;
images: {[_: string]: StyleImage} | null;
};

const defaultMinZoom = -2;
const defaultMaxZoom = 22;

Expand Down Expand Up @@ -545,6 +550,15 @@ export class Map extends Camera {
*/
_imageQueueHandle: number;

/**
* @internal
* Used to store the previous style and images when a context loss occurs, so they can be restored.
*/
_lostContextStyle: LostContextStyle = {
style: null,
images: null
};

/**
* The map's {@link ScrollZoomHandler}, which implements zooming in and out with a scroll wheel or trackpad.
* Find more details and examples using `scrollZoom` in the {@link ScrollZoomHandler} section.
Expand Down Expand Up @@ -1279,7 +1293,7 @@ export class Map extends Camera {
/** Sets or clears the callback overriding how the map constrains the viewport's lnglat and zoom to respect the longitude and latitude bounds.
*
* @param constrain - A {@link TransformConstrainFunction} callback defining how the viewport should respect the bounds.
*
*
* `null` clears the callback and reverts the constrain to the map transform's default constrain function.
* @example
* ```ts
Expand Down Expand Up @@ -2035,6 +2049,21 @@ export class Map extends Camera {
}
}

/**
* @internal
* Returns the map's style and cloned images to restore context.
* @returns An object containing the style and images.
*/
_getStyleAndImages(): LostContextStyle {
if (this.style) {
return {
style: this.style.serialize(),
images: this.style.imageManager.cloneImages()
};
}
return {style: null, images: {}};
}

/**
* Returns a Boolean indicating whether the map's style is fully loaded.
*
Expand Down Expand Up @@ -3257,10 +3286,36 @@ export class Map extends Camera {
this._frameRequest.abort();
this._frameRequest = null;
}
this.painter.destroy();

// check if style contains custom layers to warn user that they can't be restored automatically
for (const layer of Object.values(this.style._layers)) {
if (layer.type === 'custom') {
console.warn(`Custom layer with id '${layer.id}' cannot be restored after WebGL context loss. You will need to re-add it manually after context restoration.`);
}

if (layer._listeners) {
for (const [event] of Object.entries(layer._listeners)) {
console.warn(`Custom layer with id '${layer.id}' had event listeners for event '${event}' which cannot be restored after WebGL context loss. You will need to re-add them manually after context restoration.`);
}
}
}

this._lostContextStyle = this._getStyleAndImages();
this.style.destroy();
this.style = null;
this.fire(new Event('webglcontextlost', {originalEvent: event}));
};

_contextRestored = (event: any) => {
if (this._lostContextStyle.style) {
this.setStyle(this._lostContextStyle.style, {diff: false});
}

if (this._lostContextStyle.images) {
this.style.imageManager.images = this._lostContextStyle.images;
}

this._setupPainter();
this.resize();
this._update();
Expand Down
4 changes: 4 additions & 0 deletions src/util/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export class Actor implements IActor {
this.messageHandlers[type] = handler;
}

unregisterMessageHandler<T extends MessageType>(type: T) {
delete this.messageHandlers[type];
}

/**
* Sends a message from a main-thread map to a Worker or from a Worker back to
* a main-thread map instance.
Expand Down
6 changes: 6 additions & 0 deletions src/util/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export class Dispatcher {
actor.registerMessageHandler(type, handler);
}
}

public unregisterMessageHandler<T extends MessageType>(type: T) {
for (const actor of this.actors) {
actor.unregisterMessageHandler(type);
}
}
}

let globalDispatcher: Dispatcher;
Expand Down
Loading