Spaces:
Sleeping
Sleeping
File size: 10,356 Bytes
fcf3b03 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | // @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;
|