diff --git a/debug/update_image.html b/debug/update_image.html
new file mode 100644
index 00000000000..9db8b8b54c6
--- /dev/null
+++ b/debug/update_image.html
@@ -0,0 +1,69 @@
+
+
+
+ Mapbox GL JS debug page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/pages/example/animate-images.html b/docs/pages/example/animate-images.html
index 995499b7055..e035015cdc6 100644
--- a/docs/pages/example/animate-images.html
+++ b/docs/pages/example/animate-images.html
@@ -9,39 +9,37 @@
center: [-75.789, 41.874]
});
-map.on('load', function() {
+var frameCount = 5;
+var currentImage = 0;
+
+function getPath() {
+ return "https://www.mapbox.com/mapbox-gl-js/assets/radar" + currentImage + ".gif";
+}
- var frameCount = 5;
- for (var i = 0; i < frameCount; i++) {
+map.on('load', function() {
- map.addLayer({
- id: 'radar' + i,
- source: {
- type: 'image',
- url: 'https://www.mapbox.com/mapbox-gl-js/assets/radar' + i + '.gif',
- coordinates: [
- [-80.425, 46.437],
- [-71.516, 46.437],
- [-71.516, 37.936],
- [-80.425, 37.936]
- ]
- },
- type: 'raster',
- paint: {
- 'raster-opacity': 0,
- 'raster-opacity-transition': {
- duration: 0
- }
- }
- });
- }
+ map.addSource("radar", {
+ type: "image",
+ url: getPath(),
+ coordinates: [
+ [-80.425, 46.437],
+ [-71.516, 46.437],
+ [-71.516, 37.936],
+ [-80.425, 37.936]
+ ]
+ });
+ map.addLayer({
+ id: "radar-layer",
+ "type": "raster",
+ "source": "radar",
+ "paint": {
+ "raster-fade-duration": 0
+ }
+ });
- var frame = frameCount - 1;
setInterval(function() {
- map.setPaintProperty('radar' + frame, 'raster-opacity', 0);
- frame = (frame + 1) % frameCount;
- map.setPaintProperty('radar' + frame, 'raster-opacity', 1);
+ currentImage = (currentImage + 1) % frameCount;
+ map.getSource("radar").updateImage({ url: getPath() });
}, 200);
-
});
diff --git a/src/source/image_source.js b/src/source/image_source.js
index 0306b98ba01..8b7d7ddc323 100644
--- a/src/source/image_source.js
+++ b/src/source/image_source.js
@@ -27,6 +27,8 @@ import type {
VideoSourceSpecification
} from '../style-spec/types';
+type Coordinates = [[number, number], [number, number], [number, number], [number, number]];
+
/**
* A data source containing an image.
* (See the [Style Specification](https://www.mapbox.com/mapbox-gl-style-spec/#sources-image) for detailed documentation of options.)
@@ -44,7 +46,7 @@ import type {
* ]
* });
*
- * // update
+ * // update coordinates
* var mySource = map.getSource('some id');
* mySource.setCoordinates([
* [-76.54335737228394, 39.18579907229748],
@@ -52,7 +54,18 @@ import type {
* [-76.5295386314392, 39.17683392507606],
* [-76.54520273208618, 39.17876344106642]
* ]);
- *
+ *
+ * // update url and coordinates simultaneously
+ * mySource.updateImage({
+ * url: 'https://www.mapbox.com/images/bar.png',
+ * coordinates: [
+ * [-76.54335737228394, 39.18579907229748],
+ * [-76.52803659439087, 39.1838364847587],
+ * [-76.5295386314392, 39.17683392507606],
+ * [-76.54520273208618, 39.17876344106642]
+ * ]
+ * })
+ *
* map.removeSource('some id'); // remove
* @see [Add an image](https://www.mapbox.com/mapbox-gl-js/example/image-on-a-map/)
*/
@@ -64,12 +77,12 @@ class ImageSource extends Evented implements Source {
tileSize: number;
url: string;
- coordinates: [[number, number], [number, number], [number, number], [number, number]];
+ coordinates: Coordinates;
tiles: {[string]: Tile};
options: any;
dispatcher: Dispatcher;
map: Map;
- texture: Texture;
+ texture: Texture | null;
image: ImageData;
centerCoord: Coordinate;
tileID: CanonicalTileID;
@@ -97,7 +110,7 @@ class ImageSource extends Evented implements Source {
this.options = options;
}
- load() {
+ load(newCoordinates?: Coordinates, successCallback?: () => void) {
this.fire(new Event('dataloading', {dataType: 'source'}));
this.url = this.options.url;
@@ -107,11 +120,38 @@ class ImageSource extends Evented implements Source {
this.fire(new ErrorEvent(err));
} else if (image) {
this.image = browser.getImageData(image);
+ if (newCoordinates) {
+ this.coordinates = newCoordinates;
+ }
+ if (successCallback) {
+ successCallback();
+ }
this._finishLoading();
}
});
}
+ /**
+ * Updates the image URL and, optionally, the coordinates. To avoid having the image flash after changing,
+ * set the `raster-fade-duration` paint property on the raster layer to 0.
+ *
+ * @param {Object} options
+ * @param {string} [options.url] Required image URL.
+ * @param {Array>} [options.coordinates] Four geographical coordinates,
+ * represented as arrays of longitude and latitude numbers, which define the corners of the image.
+ * The coordinates start at the top left corner of the image and proceed in clockwise order.
+ * They do not have to represent a rectangle.
+ * @returns {ImageSource} this
+ */
+ updateImage(options: {url: string, coordinates?: Coordinates}) {
+ if (!this.image || !options.url) {
+ return this;
+ }
+ this.options.url = options.url;
+ this.load(options.coordinates, () => { this.texture = null; });
+ return this;
+ }
+
_finishLoading() {
if (this.map) {
this.setCoordinates(this.coordinates);
@@ -133,7 +173,7 @@ class ImageSource extends Evented implements Source {
* They do not have to represent a rectangle.
* @returns {ImageSource} this
*/
- setCoordinates(coordinates: [[number, number], [number, number], [number, number], [number, number]]) {
+ setCoordinates(coordinates: Coordinates) {
this.coordinates = coordinates;
// Calculate which mercator tile is suitable for rendering the video in
diff --git a/test/unit/source/image_source.test.js b/test/unit/source/image_source.test.js
index a2b01313371..27cbaf780d4 100644
--- a/test/unit/source/image_source.test.js
+++ b/test/unit/source/image_source.test.js
@@ -81,6 +81,53 @@ test('ImageSource', (t) => {
t.end();
});
+ t.test('updates url from updateImage', (t) => {
+ const source = createSource({ url : '/image.png' });
+ const map = new StubMap();
+ const spy = t.spy(map, '_transformRequest');
+ source.onAdd(map);
+ respond();
+ t.ok(spy.calledOnce);
+ t.equal(spy.getCall(0).args[0], '/image.png');
+ t.equal(spy.getCall(0).args[1], 'Image');
+ source.updateImage({ url: '/image2.png' });
+ respond();
+ t.ok(spy.calledTwice);
+ t.equal(spy.getCall(1).args[0], '/image2.png');
+ t.equal(spy.getCall(1).args[1], 'Image');
+ t.end();
+ });
+
+ t.test('sets coordinates', (t) => {
+ const source = createSource({ url : '/image.png' });
+ const map = new StubMap();
+ source.onAdd(map);
+ respond();
+ const beforeSerialized = source.serialize();
+ t.deepEqual(beforeSerialized.coordinates, [[0, 0], [1, 0], [1, 1], [0, 1]]);
+ source.setCoordinates([[0, 0], [-1, 0], [-1, -1], [0, -1]]);
+ const afterSerialized = source.serialize();
+ t.deepEqual(afterSerialized.coordinates, [[0, 0], [-1, 0], [-1, -1], [0, -1]]);
+ t.end();
+ });
+
+ t.test('sets coordinates via updateImage', (t) => {
+ const source = createSource({ url : '/image.png' });
+ const map = new StubMap();
+ source.onAdd(map);
+ respond();
+ const beforeSerialized = source.serialize();
+ t.deepEqual(beforeSerialized.coordinates, [[0, 0], [1, 0], [1, 1], [0, 1]]);
+ source.updateImage({
+ url: '/image2.png',
+ coordinates: [[0, 0], [-1, 0], [-1, -1], [0, -1]]
+ });
+ respond();
+ const afterSerialized = source.serialize();
+ t.deepEqual(afterSerialized.coordinates, [[0, 0], [-1, 0], [-1, -1], [0, -1]]);
+ t.end();
+ });
+
t.test('fires data event when content is loaded', (t) => {
const source = createSource({ url : '/image.png' });
source.on('data', (e) => {