Spaces:
Sleeping
Sleeping
| // @flow | |
| import potpack from 'potpack'; | |
| import {Event, ErrorEvent, Evented} from '../util/evented'; | |
| import {RGBAImage} from '../util/image'; | |
| import {ImagePosition} from './image_atlas'; | |
| import Texture from './texture'; | |
| import assert from 'assert'; | |
| import {renderStyleImage} from '../style/style_image'; | |
| import {warnOnce} from '../util/util'; | |
| import type {StyleImage} from '../style/style_image'; | |
| import type Context from '../gl/context'; | |
| import type {Bin} from 'potpack'; | |
| import type {Callback} from '../types/callback'; | |
| type Pattern = { | |
| bin: Bin, | |
| position: ImagePosition | |
| }; | |
| // When copied into the atlas texture, image data is padded by one pixel on each side. Icon | |
| // images are padded with fully transparent pixels, while pattern images are padded with a | |
| // copy of the image data wrapped from the opposite side. In both cases, this ensures the | |
| // correct behavior of GL_LINEAR texture sampling mode. | |
| const padding = 1; | |
| /* | |
| ImageManager does three things: | |
| 1. Tracks requests for icon images from tile workers and sends responses when the requests are fulfilled. | |
| 2. Builds a texture atlas for pattern images. | |
| 3. Rerenders renderable images once per frame | |
| These are disparate responsibilities and should eventually be handled by different classes. When we implement | |
| data-driven support for `*-pattern`, we'll likely use per-bucket pattern atlases, and that would be a good time | |
| to refactor this. | |
| */ | |
| class ImageManager extends Evented { | |
| images: {[_: string]: StyleImage}; | |
| updatedImages: {[_: string]: boolean}; | |
| callbackDispatchedThisFrame: {[_: string]: boolean}; | |
| loaded: boolean; | |
| requestors: Array<{ids: Array<string>, callback: Callback<{[_: string]: StyleImage}>}>; | |
| patterns: {[_: string]: Pattern}; | |
| atlasImage: RGBAImage; | |
| atlasTexture: ?Texture; | |
| dirty: boolean; | |
| constructor() { | |
| super(); | |
| this.images = {}; | |
| this.updatedImages = {}; | |
| this.callbackDispatchedThisFrame = {}; | |
| this.loaded = false; | |
| this.requestors = []; | |
| this.patterns = {}; | |
| this.atlasImage = new RGBAImage({width: 1, height: 1}); | |
| this.dirty = true; | |
| } | |
| isLoaded() { | |
| return this.loaded; | |
| } | |
| setLoaded(loaded: boolean) { | |
| if (this.loaded === loaded) { | |
| return; | |
| } | |
| this.loaded = loaded; | |
| if (loaded) { | |
| for (const {ids, callback} of this.requestors) { | |
| this._notify(ids, callback); | |
| } | |
| this.requestors = []; | |
| } | |
| } | |
| getImage(id: string): ?StyleImage { | |
| return this.images[id]; | |
| } | |
| addImage(id: string, image: StyleImage) { | |
| assert(!this.images[id]); | |
| if (this._validate(id, image)) { | |
| this.images[id] = image; | |
| } | |
| } | |
| _validate(id: string, image: StyleImage) { | |
| let valid = true; | |
| if (!this._validateStretch(image.stretchX, image.data && image.data.width)) { | |
| this.fire(new ErrorEvent(new Error(`Image "${id}" has invalid "stretchX" value`))); | |
| valid = false; | |
| } | |
| if (!this._validateStretch(image.stretchY, image.data && image.data.height)) { | |
| this.fire(new ErrorEvent(new Error(`Image "${id}" has invalid "stretchY" value`))); | |
| valid = false; | |
| } | |
| if (!this._validateContent(image.content, image)) { | |
| this.fire(new ErrorEvent(new Error(`Image "${id}" has invalid "content" value`))); | |
| valid = false; | |
| } | |
| return valid; | |
| } | |
| _validateStretch(stretch: ?Array<[number, number]> | void, size: number) { | |
| if (!stretch) return true; | |
| let last = 0; | |
| for (const part of stretch) { | |
| if (part[0] < last || part[1] < part[0] || size < part[1]) return false; | |
| last = part[1]; | |
| } | |
| return true; | |
| } | |
| _validateContent(content: ?[number, number, number, number] | void, image: StyleImage) { | |
| if (!content) return true; | |
| if (content.length !== 4) return false; | |
| if (content[0] < 0 || image.data.width < content[0]) return false; | |
| if (content[1] < 0 || image.data.height < content[1]) return false; | |
| if (content[2] < 0 || image.data.width < content[2]) return false; | |
| if (content[3] < 0 || image.data.height < content[3]) return false; | |
| if (content[2] < content[0]) return false; | |
| if (content[3] < content[1]) return false; | |
| return true; | |
| } | |
| updateImage(id: string, image: StyleImage) { | |
| const oldImage = this.images[id]; | |
| assert(oldImage); | |
| assert(oldImage.data.width === image.data.width); | |
| assert(oldImage.data.height === image.data.height); | |
| image.version = oldImage.version + 1; | |
| this.images[id] = image; | |
| this.updatedImages[id] = true; | |
| } | |
| removeImage(id: string) { | |
| assert(this.images[id]); | |
| const image = this.images[id]; | |
| delete this.images[id]; | |
| delete this.patterns[id]; | |
| if (image.userImage && image.userImage.onRemove) { | |
| image.userImage.onRemove(); | |
| } | |
| } | |
| listImages(): Array<string> { | |
| return Object.keys(this.images); | |
| } | |
| getImages(ids: Array<string>, callback: Callback<{[_: string]: StyleImage}>) { | |
| // If the sprite has been loaded, or if all the icon dependencies are already present | |
| // (i.e. if they've been added via runtime styling), then notify the requestor immediately. | |
| // Otherwise, delay notification until the sprite is loaded. At that point, if any of the | |
| // dependencies are still unavailable, we'll just assume they are permanently missing. | |
| let hasAllDependencies = true; | |
| if (!this.isLoaded()) { | |
| for (const id of ids) { | |
| if (!this.images[id]) { | |
| hasAllDependencies = false; | |
| } | |
| } | |
| } | |
| if (this.isLoaded() || hasAllDependencies) { | |
| this._notify(ids, callback); | |
| } else { | |
| this.requestors.push({ids, callback}); | |
| } | |
| } | |
| _notify(ids: Array<string>, callback: Callback<{[_: string]: StyleImage}>) { | |
| const response = {}; | |
| for (const id of ids) { | |
| if (!this.images[id]) { | |
| this.fire(new Event('styleimagemissing', {id})); | |
| } | |
| const image = this.images[id]; | |
| if (image) { | |
| // Clone the image so that our own copy of its ArrayBuffer doesn't get transferred. | |
| response[id] = { | |
| data: image.data.clone(), | |
| pixelRatio: image.pixelRatio, | |
| sdf: image.sdf, | |
| version: image.version, | |
| stretchX: image.stretchX, | |
| stretchY: image.stretchY, | |
| content: image.content, | |
| hasRenderCallback: Boolean(image.userImage && image.userImage.render) | |
| }; | |
| } else { | |
| warnOnce(`Image "${id}" could not be loaded. Please make sure you have added the image with map.addImage() or a "sprite" property in your style. You can provide missing images by listening for the "styleimagemissing" map event.`); | |
| } | |
| } | |
| callback(null, response); | |
| } | |
| // Pattern stuff | |
| getPixelSize() { | |
| const {width, height} = this.atlasImage; | |
| return {width, height}; | |
| } | |
| getPattern(id: string): ?ImagePosition { | |
| const pattern = this.patterns[id]; | |
| const image = this.getImage(id); | |
| if (!image) { | |
| return null; | |
| } | |
| if (pattern && pattern.position.version === image.version) { | |
| return pattern.position; | |
| } | |
| if (!pattern) { | |
| const w = image.data.width + padding * 2; | |
| const h = image.data.height + padding * 2; | |
| const bin = {w, h, x: 0, y: 0}; | |
| const position = new ImagePosition(bin, image); | |
| this.patterns[id] = {bin, position}; | |
| } else { | |
| pattern.position.version = image.version; | |
| } | |
| this._updatePatternAtlas(); | |
| return this.patterns[id].position; | |
| } | |
| bind(context: Context) { | |
| const gl = context.gl; | |
| if (!this.atlasTexture) { | |
| this.atlasTexture = new Texture(context, this.atlasImage, gl.RGBA); | |
| } else if (this.dirty) { | |
| this.atlasTexture.update(this.atlasImage); | |
| this.dirty = false; | |
| } | |
| this.atlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); | |
| } | |
| _updatePatternAtlas() { | |
| const bins = []; | |
| for (const id in this.patterns) { | |
| bins.push(this.patterns[id].bin); | |
| } | |
| const {w, h} = potpack(bins); | |
| const dst = this.atlasImage; | |
| dst.resize({width: w || 1, height: h || 1}); | |
| for (const id in this.patterns) { | |
| const {bin} = this.patterns[id]; | |
| const x = bin.x + padding; | |
| const y = bin.y + padding; | |
| const src = this.images[id].data; | |
| const w = src.width; | |
| const h = src.height; | |
| RGBAImage.copy(src, dst, {x: 0, y: 0}, {x, y}, {width: w, height: h}); | |
| // Add 1 pixel wrapped padding on each side of the image. | |
| RGBAImage.copy(src, dst, {x: 0, y: h - 1}, {x, y: y - 1}, {width: w, height: 1}); // T | |
| RGBAImage.copy(src, dst, {x: 0, y: 0}, {x, y: y + h}, {width: w, height: 1}); // B | |
| RGBAImage.copy(src, dst, {x: w - 1, y: 0}, {x: x - 1, y}, {width: 1, height: h}); // L | |
| RGBAImage.copy(src, dst, {x: 0, y: 0}, {x: x + w, y}, {width: 1, height: h}); // R | |
| } | |
| this.dirty = true; | |
| } | |
| beginFrame() { | |
| this.callbackDispatchedThisFrame = {}; | |
| } | |
| dispatchRenderCallbacks(ids: Array<string>) { | |
| for (const id of ids) { | |
| // the callback for the image was already dispatched for a different frame | |
| if (this.callbackDispatchedThisFrame[id]) continue; | |
| this.callbackDispatchedThisFrame[id] = true; | |
| const image = this.images[id]; | |
| assert(image); | |
| const updated = renderStyleImage(image); | |
| if (updated) { | |
| this.updateImage(id, image); | |
| } | |
| } | |
| } | |
| } | |
| export default ImageManager; | |