Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add update functionality to ImageSource objects #4050

Closed
christopher-w-root opened this issue Jan 25, 2017 · 13 comments
Closed

Add update functionality to ImageSource objects #4050

christopher-w-root opened this issue Jan 25, 2017 · 13 comments

Comments

@christopher-w-root
Copy link

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;
        }
    };
@ryanbaumann
Copy link
Contributor

ryanbaumann commented Apr 9, 2017

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

@christopher-w-root
Copy link
Author

@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.

@ryanbaumann
Copy link
Contributor

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

@jordandalley
Copy link

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

@christopher-w-root
Copy link
Author

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.

@jordandalley
Copy link

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

@jplante
Copy link

jplante commented Dec 14, 2017

We also need this.

@d3cay1-zz
Copy link

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.

@jorge-vasquez-tx
Copy link

I agree, implementing this will be very useful!

@ccson
Copy link

ccson commented Mar 20, 2018

It would be nice if we had this feature

@hugo-siles
Copy link

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

@ababian
Copy link

ababian commented Mar 30, 2018

Ya, this feature would be a great addition!

@kevinduke10
Copy link

Would love to have this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests