mapbox-gl
Version:
A WebGL interactive maps library
241 lines (210 loc) • 8.88 kB
JavaScript
// @flow
const util = require('../util/util');
const window = require('../util/window');
const TileCoord = require('./tile_coord');
const LngLat = require('../geo/lng_lat');
const Point = require('point-geometry');
const Evented = require('../util/evented');
const ajax = require('../util/ajax');
const EXTENT = require('../data/extent');
const RasterBoundsArray = require('../data/raster_bounds_array');
const Buffer = require('../data/buffer');
const VertexArrayObject = require('../render/vertex_array_object');
import type {Source} from './source';
import type Map from '../ui/map';
import type Dispatcher from '../util/dispatcher';
import type Tile from './tile';
import type Coordinate from '../geo/coordinate';
/**
* 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.)
*
* @interface ImageSource
* @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 mySource = map.getSource('some id');
* mySource.setCoordinates([
* [-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/)
*/
class ImageSource extends Evented implements Source {
type: string;
id: string;
minzoom: number;
maxzoom: number;
tileSize: number;
url: string;
coordinates: [[number, number], [number, number], [number, number], [number, number]];
tiles: {[string]: Tile};
options: any;
dispatcher: Dispatcher;
map: Map;
texture: WebGLTexture;
textureLoaded: boolean;
image: HTMLImageElement;
centerCoord: Coordinate;
coord: TileCoord;
_tileCoords: Array<Point>;
constructor(id: string, options: ImageSourceSpecification | VideoSourceSpecification | CanvasSourceSpecification, dispatcher: Dispatcher, eventedParent: Evented) {
super();
this.id = id;
this.dispatcher = dispatcher;
this.coordinates = options.coordinates;
this.type = 'image';
this.minzoom = 0;
this.maxzoom = 22;
this.tileSize = 512;
this.tiles = {};
this.setEventedParent(eventedParent);
this.options = options;
this.textureLoaded = false;
}
load() {
this.fire('dataloading', {dataType: 'source'});
this.url = this.options.url;
ajax.getImage(this.options.url, (err, image) => {
if (err) {
this.fire('error', {error: err});
} else if (image) {
this.image = image;
this._finishLoading();
}
});
}
_finishLoading() {
if (this.map) {
this.setCoordinates(this.coordinates);
this.fire('data', {dataType: 'source', sourceDataType: 'metadata'});
}
}
onAdd(map: Map) {
this.load();
this.map = map;
if (this.image) {
this.setCoordinates(this.coordinates);
}
}
/**
* Sets the image's coordinates and re-renders the map.
*
* @param {Array<Array<number>>} 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
*/
setCoordinates(coordinates: [[number, number], [number, number], [number, number], [number, number]]) {
this.coordinates = coordinates;
// Calculate which mercator tile is suitable for rendering the video in
// and create a buffer with the corner coordinates. These coordinates
// may be outside the tile, because raster tiles aren't clipped when rendering.
const map = this.map;
// transform the geo coordinates into (zoom 0) tile space coordinates
const cornerZ0Coords = coordinates.map((coord) => {
return map.transform.locationCoordinate(LngLat.convert(coord)).zoomTo(0);
});
// Compute the coordinates of the tile we'll use to hold this image's
// render data
const centerCoord = this.centerCoord = util.getCoordinatesCenter(cornerZ0Coords);
// `column` and `row` may be fractional; round them down so that they
// represent integer tile coordinates
centerCoord.column = Math.floor(centerCoord.column);
centerCoord.row = Math.floor(centerCoord.row);
this.coord = new TileCoord(centerCoord.zoom, centerCoord.column, centerCoord.row);
// Constrain min/max zoom to our tile's zoom level in order to force
// SourceCache to request this tile (no matter what the map's zoom
// level)
this.minzoom = this.maxzoom = centerCoord.zoom;
// Transform the corner coordinates into the coordinate space of our
// tile.
this._tileCoords = cornerZ0Coords.map((coord) => {
const zoomedCoord = coord.zoomTo(centerCoord.zoom);
return new Point(
Math.round((zoomedCoord.column - centerCoord.column) * EXTENT),
Math.round((zoomedCoord.row - centerCoord.row) * EXTENT));
});
this.fire('data', {dataType:'source', sourceDataType: 'content'});
return this;
}
_setTile(tile: Tile) {
this.tiles[String(tile.coord.w)] = 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);
tile.buckets = {};
tile.boundsBuffer = Buffer.fromStructArray(array, Buffer.BufferType.VERTEX);
tile.boundsVAO = new VertexArrayObject();
}
prepare() {
if (Object.keys(this.tiles).length === 0 === 0 || !this.image) return;
this._prepareImage(this.map.painter.gl, this.image);
}
_prepareImage(gl: WebGLRenderingContext, image: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement, resize?: boolean) {
if (!this.textureLoaded) {
this.textureLoaded = true;
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 (resize) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
} else if (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);
}
for (const w in this.tiles) {
const tile = this.tiles[w];
if (tile.state !== 'loaded') {
tile.state = 'loaded';
tile.texture = this.texture;
}
}
}
loadTile(tile: Tile, callback: Callback<void>) {
// We have a single tile -- whoose coordinates are this.coord -- that
// covers the image we want to render. If that's the one being
// requested, set it up with the image; otherwise, mark the tile as
// `errored` to indicate that we have no data for it.
// If the world wraps, we may have multiple "wrapped" copies of the
// single tile.
if (this.coord && this.coord.toString() === tile.coord.toString()) {
this._setTile(tile);
callback(null);
} else {
tile.state = 'errored';
callback(null);
}
}
serialize(): Object {
return {
type: 'image',
urls: this.url,
coordinates: this.coordinates
};
}
}
module.exports = ImageSource;