Mapbox-gl-js: Add update functionality to ImageSource objects

Created on 26 Jan 2017  路  13Comments  路  Source: mapbox/mapbox-gl-js

Motivation


ImageSource objects only have a create mechanism, not an update. So, if you wanted to swap out the image on an image source with a new image, you have to delete the existing source and create a new one. This is inefficient and can result in unwanted popping when re-rendering.

Design Alternatives

Add an update method to ImageSource objects.

Design

Similar to how images are created, the update() method would take as an argument an object with the following optional properties:

{
    url: <the image URL>, // if not provided, the image does not change
    coordinates: <optional coordinates to place the image> // if not provided, the existing coords do not change
} 

Mock-Up

Here's an example of how it would look like in code, based of the documented example:

// add to map
map.addSource('some id', {
   type: 'image',
   url: 'https://www.mapbox.com/images/foo.png',
   coordinates: [
       [-76.54, 39.18],
       [-76.52, 39.18],
       [-76.52, 39.17],
       [-76.54, 39.17]
   ]
});

// update
var myImageSource = map.getSource('some id');

// update the image source with a new image and new coordinates
myImageSource.update({
    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

Concepts

Implementation

Implementation is fairly straightforward as it is similar to addSource(). Care would need to be taken if the image size changes during the update. The following is what it could look like in code (this is based of v0.28.0:

    ImageSource.prototype.updateImage = function(options) {
        if (!this.image || !options.url) {
            return;
        }

        var updateCoords = Boolean(options.coordinates);

        ajax.getImage(options.url, (err, image) => {
            // @TODO handle errors via event.
            if (err) return this.fire('error', {error: err});

            if (image.width != this.image.width ||
                image.height != this.image.height) {
                // set a resized flag
                this._resized = true;
            } else {
                // set an update flag
                this._updated = true;
            }

            this.image = image;

            if (updateCoords && this.map) {
                // update coordinates
                this.setCoordinates(options.coordinates);
                this.fire('change');
            }
        });
    };

    ImageSource.prototype._setTile = function(tile) {
        const gl = this.map.painter.gl;

        // the tile holds onto resources used for rendering
        // images, so if the tile changes with the update
        // then the resources on the previous tile should be removed
        if (this.tile) {
            if (tile !== this.tile) {
                delete this.tile.texture;
                delete this.tile.buckets;
                delete this.tile.boundsBuffer;
                delete this.tile.boundsVAO;
            }
        }

        this.tile = tile;
        const maxInt16 = 32767;
        const array = new RasterBoundsArray();
        array.emplaceBack(this._tileCoords[0].x, this._tileCoords[0].y, 0, 0);
        array.emplaceBack(this._tileCoords[1].x, this._tileCoords[1].y, maxInt16, 0);
        array.emplaceBack(this._tileCoords[3].x, this._tileCoords[3].y, 0, maxInt16);
        array.emplaceBack(this._tileCoords[2].x, this._tileCoords[2].y, maxInt16, maxInt16);

        if (!this.boundsBuffer) {
            this.boundsBuffer = Buffer.fromStructArray(array, Buffer.BufferType.VERTEX);
            this.boundsVAO = new VertexArrayObject();
        } else {
            // the tile didn't change with the updates, so
            // reset the tile's boundsBuffer to the new tile coords
            const bufferObj = this.boundsBuffer;
            const data = array.serialize();
            const type = gl[bufferObj.type];

            // NOTE: when in here, the array structure hasn't changed, just the
            // data underneath, so there's no need to recreate the
            // VAO or anything like that.
            gl.bindBuffer(type, bufferObj.buffer);
            gl.bufferSubData(type, 0, data.arrayBuffer);
        }

        // set the tile's render resources
        this.tile.buckets = {};
        this.tile.boundsBuffer = this.boundsBuffer;
        this.tile.boundsVAO = this.boundsVAO;
    };

    ImageSource.prototype._prepareImage = function(gl, image) {
        if (!this.texture) {
            // create the texture resource if it doesn't exist.
            this.texture = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, this.texture);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        } else if (this._resized) {
            // Image was updated and its dimensions changed.
            gl.bindTexture(gl.TEXTURE_2D, this.texture);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
            this._resized = false;
        } else if (this._updated || image instanceof window.HTMLVideoElement || image instanceof window.ImageData || image instanceof window.HTMLCanvasElement) {
            gl.bindTexture(gl.TEXTURE_2D, this.texture);
            gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, image);
        }

        if (this.tile.state !== 'loaded') {
            this.tile.state = 'loaded';
            this.tile.texture = this.texture;
        }
    };
feature

Most helpful comment

@ryanbaumann Sorry for the long delay. We'll open a PR when we have some time. That will require making sure our changes are in sync with the master. The lower-level rendering code seems to change fairly regularly, which means it could take a bit of time to sync this. Stay tuned.

All 13 comments

@vastcharade looks like you've got a good chunk of the code written for this image update feature - want to open a PR?

@ryanbaumann Sorry for the long delay. We'll open a PR when we have some time. That will require making sure our changes are in sync with the master. The lower-level rendering code seems to change fairly regularly, which means it could take a bit of time to sync this. Stay tuned.

@vastcharade are you still planning to submit a PR? Looks like there are several other devs interested in this feature too.

Yes please. Very interested in this as it is by far more efficient than switching multiple layers on and off :)

I hadn't put it on my road map. I may have a slot to do it this week, but it'd be preferable if someone else could give it a go.

This is a bit beyond me, otherwise, I would help.

We also need this.

This would really help our efforts at working with mapd integration. I'd imagine that there are other companies looking at working with mapd or kinetica and having this added would be quite helpful.

I agree, implementing this will be very useful!

It would be nice if we had this feature

Would be really helpful to have a feature like this in the coming future.

Ya, this feature would be a great addition!

Would love to have this!

Was this page helpful?
0 / 5 - 0 ratings