diff --git a/docs/pages/example/add-image-missing-generated.html b/docs/pages/example/add-image-missing-generated.html
new file mode 100644
index 00000000000..30286b5a916
--- /dev/null
+++ b/docs/pages/example/add-image-missing-generated.html
@@ -0,0 +1,81 @@
+
+
+
diff --git a/docs/pages/example/add-image-missing-generated.js b/docs/pages/example/add-image-missing-generated.js
new file mode 100644
index 00000000000..df53999905a
--- /dev/null
+++ b/docs/pages/example/add-image-missing-generated.js
@@ -0,0 +1,11 @@
+/*---
+title: Generate and add a missing icon to the map
+description: Add a missing icon to the map that was generated at runtime.
+tags:
+ - styles
+ - layers
+pathname: /mapbox-gl-js/example/add-image-missing-generated/
+---*/
+import Example from '../../components/example';
+import html from './add-image-missing-generated.html';
+export default Example(html);
diff --git a/src/render/image_manager.js b/src/render/image_manager.js
index fee826e4155..a3672895d0a 100644
--- a/src/render/image_manager.js
+++ b/src/render/image_manager.js
@@ -2,6 +2,7 @@
import potpack from 'potpack';
+import { Event, Evented } from '../util/evented';
import { RGBAImage } from '../util/image';
import { ImagePosition } from './image_atlas';
import Texture from './texture';
@@ -33,7 +34,7 @@ const padding = 1;
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 {
+class ImageManager extends Evented {
images: {[string]: StyleImage};
loaded: boolean;
requestors: Array<{ids: Array, callback: Callback<{[string]: StyleImage}>}>;
@@ -44,6 +45,7 @@ class ImageManager {
dirty: boolean;
constructor() {
+ super();
this.images = {};
this.loaded = false;
this.requestors = [];
@@ -115,6 +117,9 @@ class ImageManager {
const response = {};
for (const id of ids) {
+ if (!this.images[id]) {
+ this.fire(new Event('styleimageneeded', { id }));
+ }
const image = this.images[id];
if (image) {
// Clone the image so that our own copy of its ArrayBuffer doesn't get transferred.
diff --git a/src/style/style.js b/src/style/style.js
index f78420f3518..53f3c8dcafb 100644
--- a/src/style/style.js
+++ b/src/style/style.js
@@ -136,6 +136,7 @@ class Style extends Evented {
this.map = map;
this.dispatcher = new Dispatcher(getWorkerPool(), this);
this.imageManager = new ImageManager();
+ this.imageManager.setEventedParent(this);
this.glyphManager = new GlyphManager(map._transformRequest, options.localIdeographFontFamily);
this.lineAtlas = new LineAtlas(256, 512);
this.crossTileSymbolIndex = new CrossTileSymbolIndex();
diff --git a/src/ui/events.js b/src/ui/events.js
index 82968ccf8ba..8111f4e184c 100644
--- a/src/ui/events.js
+++ b/src/ui/events.js
@@ -778,6 +778,20 @@ export type MapEvent =
*/
| 'sourcedataloading'
+ /**
+ * Fired when an icon or pattern needed by the style is missing. The missing image can
+ * be added with {@link Map#addImage} within this callback to prevent the image from
+ * being skipped. This event can be used to dynamically generate icons and patterns.
+ *
+ * @event styleimageneeded
+ * @memberof Map
+ * @instance
+ * @property {string} id The id of the missing image.
+ *
+ * @see [Generate and add a missing icon to the map](https://mapbox.com/mapbox-gl-js/example/add-image-missing-generated/)
+ */
+ | 'styleimageneeded'
+
/**
* @event style.load
* @memberof Map
diff --git a/test/unit/ui/map.test.js b/test/unit/ui/map.test.js
index 55fa4eb7b24..3292e715ede 100755
--- a/test/unit/ui/map.test.js
+++ b/test/unit/ui/map.test.js
@@ -1945,6 +1945,26 @@ test('Map', (t) => {
t.end();
});
+ t.test('map fires `styleimageneeded` for missing icons', (t) => {
+ const map = createMap(t);
+
+ const id = "missing-image";
+
+ let called;
+ map.on('styleimageneeded', e => {
+ map.addImage(e.id, {width: 1, height: 1, data: new Uint8Array(4)});
+ called = e.id;
+ });
+
+ t.notok(map.hasImage(id));
+
+ map.style.imageManager.getImages([id], () => {
+ t.equals(called, id);
+ t.ok(map.hasImage(id));
+ t.end();
+ });
+ });
+
t.end();
});