Skip to content

Commit c07008c

Browse files
lucaswojandrewharvey
authored andcommitted
Add "Map#addImage" / "Map#removeImage" (mapbox#4404)
* Update yarn.lock files * Add "Map#addImage" * Make `SpriteAtlas` fire `error` and `data` events * Add "Map#removeImage" * Relax requirement that styles using "icon-image" must have a "sprite" * Add "pixelRatio" parameter to "Map#addImage" * Enable "runtime-styling/image-add" integration test * Add "runtime-styling/image-remove" integration test * Add "runtime-styling/image-add-pattern" integration test * Add docs for `Map#addImage` and `Map#removeImage` * Add explanatory comment to `SpriteAtlas` * Rename SpriteAtlas#atlas to SpriteAtlas#shelfPack * Eliminate `AtlasImage` * Moar comments * Ensure `SpriteAtlas#addImage` returns early after an error * Misc docs improvements * Remove logic that clears texture when image is removed * fixup! Eliminate `AtlasImage` * Remove addImage debug page
1 parent 6f5bca8 commit c07008c

File tree

14 files changed

+381
-209
lines changed

14 files changed

+381
-209
lines changed

src/style-spec/validate/validate_property.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ module.exports = function validateProperty(options, propertyType) {
4040
const errors = [];
4141

4242
if (options.layerType === 'symbol') {
43-
if (propertyKey === 'icon-image' && style && !style.sprite) {
44-
errors.push(new ValidationError(key, value, 'use of "icon-image" requires a style "sprite" property'));
45-
} else if (propertyKey === 'text-field' && style && !style.glyphs) {
43+
if (propertyKey === 'text-field' && style && !style.glyphs) {
4644
errors.push(new ValidationError(key, value, 'use of "text-field" requires a style "glyphs" property'));
4745
}
4846
}

src/style-spec/yarn.lock

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
22
# yarn lockfile v1
3-
4-
5-
"JSV@>= 4.0.x":
6-
version "4.0.2"
7-
resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57"
8-
93
ansi-styles@~1.0.0:
104
version "1.0.0"
115
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
@@ -37,6 +31,10 @@ jsonlint-lines-primitives@~1.6.0:
3731
JSV ">= 4.0.x"
3832
nomnom ">= 1.5.x"
3933

34+
"JSV@>= 4.0.x":
35+
version "4.0.2"
36+
resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57"
37+
4038
lodash._baseisequal@^3.0.0:
4139
version "3.0.7"
4240
resolved "https://registry.yarnpkg.com/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz#d8025f76339d29342767dcc887ce5cb95a5b51f1"
@@ -117,3 +115,4 @@ strip-ansi@~0.1.0:
117115
underscore@~1.6.0:
118116
version "1.6.0"
119117
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
118+

src/style/style.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class Style extends Evented {
5858
this.animationLoop = (map && map.animationLoop) || new AnimationLoop();
5959
this.dispatcher = new Dispatcher(getWorkerPool(), this);
6060
this.spriteAtlas = new SpriteAtlas(1024, 1024);
61+
this.spriteAtlas.setEventedParent(this);
6162
this.lineAtlas = new LineAtlas(256, 512);
6263

6364
this._layers = {};
@@ -863,7 +864,7 @@ class Style extends Evented {
863864
this.spriteAtlas.setSprite(this.sprite);
864865
this.spriteAtlas.addIcons(params.icons, callback);
865866
};
866-
if (this.sprite.loaded()) {
867+
if (!this.sprite || this.sprite.loaded()) {
867868
updateSpriteAtlas();
868869
} else {
869870
this.sprite.on('data', updateSpriteAtlas);

src/symbol/sprite_atlas.js

+87-27
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,20 @@
33
const ShelfPack = require('@mapbox/shelf-pack');
44
const browser = require('../util/browser');
55
const util = require('../util/util');
6+
const window = require('../util/window');
7+
const Evented = require('../util/evented');
68

7-
class AtlasImage {
8-
constructor(rect, width, height, sdf, pixelRatio) {
9-
this.rect = rect;
10-
this.width = width;
11-
this.height = height;
12-
this.sdf = sdf;
13-
this.pixelRatio = pixelRatio;
14-
}
15-
}
16-
17-
class SpriteAtlas {
9+
// The SpriteAtlas class is responsible for turning a sprite and assorted
10+
// other images added at runtime into a texture that can be consumed by WebGL.
11+
class SpriteAtlas extends Evented {
1812

1913
constructor(width, height) {
14+
super();
15+
2016
this.width = width;
2117
this.height = height;
2218

23-
this.atlas = new ShelfPack(width, height);
19+
this.shelfPack = new ShelfPack(width, height);
2420
this.images = {};
2521
this.data = false;
2622
this.texture = 0; // WebGL ID
@@ -41,7 +37,7 @@ class SpriteAtlas {
4137
const packWidth = pixelWidth + padding + (4 - (pixelWidth + padding) % 4);
4238
const packHeight = pixelHeight + padding + (4 - (pixelHeight + padding) % 4);// + 4;
4339

44-
const rect = this.atlas.packOne(packWidth, packHeight);
40+
const rect = this.shelfPack.packOne(packWidth, packHeight);
4541
if (!rect) {
4642
util.warnOnce('SpriteAtlas out of space.');
4743
return null;
@@ -50,6 +46,62 @@ class SpriteAtlas {
5046
return rect;
5147
}
5248

49+
addImage(name, pixels, options) {
50+
let width, height, pixelRatio;
51+
if (pixels instanceof window.HTMLImageElement) {
52+
width = pixels.width;
53+
height = pixels.height;
54+
pixels = browser.getImageData(pixels);
55+
pixelRatio = this.pixelRatio;
56+
} else {
57+
width = options.width;
58+
height = options.height;
59+
pixelRatio = options.pixelRatio || this.pixelRatio;
60+
}
61+
62+
if (ArrayBuffer.isView(pixels)) {
63+
pixels = new Uint32Array(pixels.buffer);
64+
}
65+
66+
if (!(pixels instanceof Uint32Array)) {
67+
return this.fire('error', {error: new Error('Image provided in an invalid format. Supported formats are HTMLImageElement, ImageData, and ArrayBufferView.')});
68+
}
69+
70+
if (this.images[name]) {
71+
return this.fire('error', {error: new Error('An image with this name already exists.')});
72+
}
73+
74+
const rect = this.allocateImage(width, height);
75+
if (!rect) {
76+
return this.fire('error', {error: new Error('There is not enough space to add this image.')});
77+
}
78+
79+
const image = {
80+
rect,
81+
width: width / pixelRatio,
82+
height: height / pixelRatio,
83+
sdf: false,
84+
pixelRatio: 1
85+
};
86+
this.images[name] = image;
87+
88+
this.copy(pixels, width, rect, {pixelRatio, x: 0, y: 0, width, height}, false);
89+
90+
this.fire('data', {dataType: 'style'});
91+
}
92+
93+
removeImage(name) {
94+
const image = this.images[name];
95+
delete this.images[name];
96+
97+
if (!image) {
98+
return this.fire('error', {error: new Error('No image with this name exists.')});
99+
}
100+
101+
this.shelfPack.unref(image.rect);
102+
this.fire('data', {dataType: 'style'});
103+
}
104+
53105
getImage(name, wrap) {
54106
if (this.images[name]) {
55107
return this.images[name];
@@ -69,10 +121,18 @@ class SpriteAtlas {
69121
return null;
70122
}
71123

72-
const image = new AtlasImage(rect, pos.width / pos.pixelRatio, pos.height / pos.pixelRatio, pos.sdf, pos.pixelRatio / this.pixelRatio);
124+
const image = {
125+
rect,
126+
width: pos.width / pos.pixelRatio,
127+
height: pos.height / pos.pixelRatio,
128+
sdf: pos.sdf,
129+
pixelRatio: pos.pixelRatio / this.pixelRatio
130+
};
73131
this.images[name] = image;
74132

75-
this.copy(rect, pos, wrap);
133+
if (!this.sprite.imgData) return null;
134+
const srcImg = new Uint32Array(this.sprite.imgData.buffer);
135+
this.copy(srcImg, this.sprite.width, rect, pos, wrap);
76136

77137
return image;
78138
}
@@ -108,29 +168,29 @@ class SpriteAtlas {
108168
}
109169
}
110170

111-
copy(dst, src, wrap) {
112-
if (!this.sprite.imgData) return;
113-
const srcImg = new Uint32Array(this.sprite.imgData.buffer);
114-
171+
// Copy some portion of srcImage into `SpriteAtlas#data`
172+
copy(srcImg, srcImgWidth, dstPos, srcPos, wrap) {
115173
this.allocate();
116174
const dstImg = this.data;
117175

118176
const padding = 1;
119177

120178
copyBitmap(
121179
/* source buffer */ srcImg,
122-
/* source stride */ this.sprite.width,
123-
/* source x */ src.x,
124-
/* source y */ src.y,
180+
/* source stride */ srcImgWidth,
181+
/* source x */ srcPos.x,
182+
/* source y */ srcPos.y,
125183
/* dest buffer */ dstImg,
126184
/* dest stride */ this.width * this.pixelRatio,
127-
/* dest x */ (dst.x + padding) * this.pixelRatio,
128-
/* dest y */ (dst.y + padding) * this.pixelRatio,
129-
/* icon dimension */ src.width,
130-
/* icon dimension */ src.height,
131-
/* wrap */ wrap
185+
/* dest x */ (dstPos.x + padding) * this.pixelRatio,
186+
/* dest y */ (dstPos.y + padding) * this.pixelRatio,
187+
/* icon dimension */ srcPos.width,
188+
/* icon dimension */ srcPos.height,
189+
/* wrap */ wrap
132190
);
133191

192+
// Indicates that `SpriteAtlas#data` has changed and needs to be
193+
// reuploaded into the GL texture specified by `SpriteAtlas#texture`.
134194
this.dirty = true;
135195
}
136196

src/ui/map.js

+26
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,32 @@ class Map extends Camera {
817817
return this.style.getSource(id);
818818
}
819819

820+
/**
821+
* Add an image to the style. This image can be used in `icon-image`,
822+
* `background-pattern`, `fill-pattern`, and `line-pattern`. An
823+
* {@link Map#error} event will be fired if there is not enough space in the
824+
* sprite to add this image.
825+
*
826+
* @param {string} name The name of the image.
827+
* @param {HTMLImageElement|ArrayBufferView} image The image as an `HTMLImageElement` or `ArrayBufferView` (using the format of [`ImageData#data`](https://developer.mozilla.org/en-US/docs/Web/API/ImageData/data))
828+
* @param {Object} [options] Required if and only if passing an `ArrayBufferView`
829+
* @param {number} [options.width] The pixel width of the `ArrayBufferView` image
830+
* @param {number} [options.height] The pixel height of the `ArrayBufferView` image
831+
* @param {number} [options.pixelRatio] The ratio of pixels in the `ArrayBufferView` image to physical pixels on the screen
832+
*/
833+
addImage(name, image, options) {
834+
this.style.spriteAtlas.addImage(name, image, options);
835+
}
836+
837+
/**
838+
* Remove an image from the style (such as one used by `icon-image` or `background-pattern`).
839+
*
840+
* @param {string} name The name of the image.
841+
*/
842+
removeImage(name) {
843+
this.style.spriteAtlas.removeImage(name);
844+
}
845+
820846
/**
821847
* Adds a [Mapbox style layer](https://www.mapbox.com/mapbox-gl-style-spec/#layers)
822848
* to the map's style.
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"version": 8,
3+
"zoom": 1,
4+
"metadata": {
5+
"test": {
6+
"width": 64,
7+
"height": 64,
8+
"operations": [
9+
[
10+
"addImage",
11+
"marker",
12+
"./image/marker.png"
13+
],
14+
[
15+
"addLayer",
16+
{
17+
"id": "fill",
18+
"type": "fill",
19+
"source": "geojson",
20+
"paint": {
21+
"fill-antialias": false,
22+
"fill-pattern": "marker"
23+
}
24+
}
25+
],
26+
[
27+
"wait"
28+
]
29+
]
30+
}
31+
},
32+
"sources": {
33+
"geojson": {
34+
"type": "geojson",
35+
"data": {
36+
"type": "Polygon",
37+
"coordinates": [
38+
[
39+
[
40+
-10,
41+
-10
42+
],
43+
[
44+
-10,
45+
10
46+
],
47+
[
48+
10,
49+
10
50+
],
51+
[
52+
10,
53+
-10
54+
],
55+
[
56+
-10,
57+
-10
58+
]
59+
]
60+
]
61+
}
62+
}
63+
},
64+
"layers": []
65+
}

test/integration/render-tests/runtime-styling/image-add/style.json

-6
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@
44
"test": {
55
"width": 64,
66
"height": 64,
7-
"ignored": {
8-
"js": "https://github.com/mapbox/mapbox-gl-js/issues/2059"
9-
},
10-
"skipped": {
11-
"js": true
12-
},
137
"operations": [
148
[
159
"addImage",
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"version": 8,
3+
"metadata": {
4+
"test": {
5+
"width": 64,
6+
"height": 64,
7+
"operations": [
8+
[
9+
"addImage",
10+
"marker",
11+
"./image/0.png"
12+
],
13+
[
14+
"removeImage",
15+
"marker"
16+
],
17+
[
18+
"addImage",
19+
"marker",
20+
"./image/marker.png"
21+
],
22+
[
23+
"addLayer",
24+
{
25+
"id": "geometry",
26+
"type": "symbol",
27+
"source": "geometry",
28+
"layout": {
29+
"icon-image": "marker"
30+
}
31+
}
32+
],
33+
[
34+
"wait"
35+
]
36+
]
37+
}
38+
},
39+
"sources": {
40+
"geometry": {
41+
"type": "geojson",
42+
"data": {
43+
"type": "Point",
44+
"coordinates": [0, 0]
45+
}
46+
}
47+
},
48+
"layers": []
49+
}

0 commit comments

Comments
 (0)