diff --git a/examples/tileutfgrid.html b/examples/tileutfgrid.html new file mode 100644 index 0000000000..23c39c577c --- /dev/null +++ b/examples/tileutfgrid.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + TileUTFGrid example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

TileUTFGrid example

+

This example shows how to read data from a TileUTFGrid layer.

+

Point to a country to see its name and flag.

+
+

Tiles made with TileMill. Hosting on MapBox.com or with open-source TileServer.

+

See the tileutfgrid.js source to see how this is done.

+
+
utfgrid, tileutfgrid, tilejson
+
+ +
+ +
+ +
+ +
+
 
+ +
+
+ + + + + + + diff --git a/examples/tileutfgrid.js b/examples/tileutfgrid.js new file mode 100644 index 0000000000..b6462e37dd --- /dev/null +++ b/examples/tileutfgrid.js @@ -0,0 +1,68 @@ +goog.require('ol.Map'); +goog.require('ol.Overlay'); +goog.require('ol.View'); +goog.require('ol.layer.Tile'); +goog.require('ol.source.TileJSON'); +goog.require('ol.source.TileUTFGrid'); + +var mapLayer = new ol.layer.Tile({ + source: new ol.source.TileJSON({ + url: 'http://api.tiles.mapbox.com/v3/mapbox.geography-class.json' + }) +}); + +var gridSource = new ol.source.TileUTFGrid({ + url: 'http://api.tiles.mapbox.com/v3/mapbox.geography-class.json' +}); + +var gridLayer = new ol.layer.Tile({source: gridSource}); + +var view = new ol.View({ + center: [0, 0], + zoom: 1 +}); + +var mapElement = document.getElementById('map'); +var map = new ol.Map({ + layers: [mapLayer, gridLayer], + target: mapElement, + view: view +}); + +var infoElement = document.getElementById('country-info'); +var flagElement = document.getElementById('country-flag'); +var nameElement = document.getElementById('country-name'); + +var infoOverlay = new ol.Overlay({ + element: infoElement, + offset: [15, 15], + stopEvent: false +}); +map.addOverlay(infoOverlay); + +var displayCountryInfo = function(coordinate) { + var viewResolution = /** @type {number} */ (view.getResolution()); + gridSource.forDataAtCoordinateAndResolution(coordinate, viewResolution, + function(data) { + // If you want to use the template from the TileJSON, + // load the mustache.js library separately and call + // info.innerHTML = Mustache.render(gridSource.getTemplate(), data); + mapElement.style.cursor = data ? 'pointer' : ''; + if (data) { + /* jshint -W069 */ + flagElement.src = 'data:image/png;base64,' + data['flag_png']; + nameElement.innerHTML = data['admin']; + /* jshint +W069 */ + } + infoOverlay.setPosition(data ? coordinate : undefined); + }); +}; + +$(map.getViewport()).on('mousemove', function(evt) { + var coordinate = map.getEventCoordinate(evt.originalEvent); + displayCountryInfo(coordinate); +}); + +map.on('click', function(evt) { + displayCountryInfo(evt.coordinate); +}); diff --git a/externs/olx.js b/externs/olx.js index 7a29812e6b..6872d4d8a7 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -3549,6 +3549,34 @@ olx.source.GPXOptions.prototype.url; olx.source.GPXOptions.prototype.urls; +/** + * @typedef {{preemptive: (boolean|undefined), + * url: string}} + * @api + */ +olx.source.TileUTFGridOptions; + + +/** + * If `true` the TileUTFGrid source loads the tiles based on their "visibility". + * This improves the speed of response, but increases traffic. + * Note that if set to `false`, you need to pass `true` as `opt_request` + * to the `forDataAtCoordinateAndResolution` method otherwise no data + * will ever be loaded. + * Default is `true`. + * @type {boolean|undefined} + * @api + */ +olx.source.TileUTFGridOptions.prototype.preemptive; + + +/** + * @type {string} + * @api + */ +olx.source.TileUTFGridOptions.prototype.url; + + /** * @typedef {{attributions: (Array.|undefined), * crossOrigin: (null|string|undefined), diff --git a/externs/tilejson.js b/externs/tilejson.js index 6e982cc325..00ca83a8ee 100644 --- a/externs/tilejson.js +++ b/externs/tilejson.js @@ -1,6 +1,7 @@ /** * @externs * @see https://github.com/mapbox/tilejson-spec + * @see https://github.com/mapbox/utfgrid-spec */ @@ -93,3 +94,28 @@ TileJSON.prototype.bounds; * @type {!Array.|undefined} */ TileJSON.prototype.center; + + + +/** + * @constructor + */ +var UTFGridJSON = function() {}; + + +/** + * @type {!Array.} + */ +UTFGridJSON.prototype.grid; + + +/** + * @type {!Array.} + */ +UTFGridJSON.prototype.keys; + + +/** + * @type {!Object.|undefined} + */ +UTFGridJSON.prototype.data; diff --git a/src/ol/source/tileutfgridsource.js b/src/ol/source/tileutfgridsource.js new file mode 100644 index 0000000000..4a58b2bdfa --- /dev/null +++ b/src/ol/source/tileutfgridsource.js @@ -0,0 +1,406 @@ +goog.provide('ol.source.TileUTFGrid'); + +goog.require('goog.asserts'); +goog.require('goog.async.nextTick'); +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('goog.net.Jsonp'); +goog.require('ol.Attribution'); +goog.require('ol.Tile'); +goog.require('ol.TileCache'); +goog.require('ol.TileState'); +goog.require('ol.TileUrlFunction'); +goog.require('ol.extent'); +goog.require('ol.proj'); +goog.require('ol.source.State'); +goog.require('ol.source.Tile'); +goog.require('ol.tilegrid.XYZ'); + + + +/** + * @classdesc + * Layer source for UTFGrid interaction data loaded from TileJSON format. + * + * @constructor + * @extends {ol.source.Tile} + * @param {olx.source.TileUTFGridOptions} options Source options. + * @api + */ +ol.source.TileUTFGrid = function(options) { + goog.base(this, { + projection: ol.proj.get('EPSG:3857'), + state: ol.source.State.LOADING + }); + + /** + * @private + * @type {boolean} + */ + this.preemptive_ = goog.isDef(options.preemptive) ? + options.preemptive : true; + + /** + * @private + * @type {!ol.TileUrlFunctionType} + */ + this.tileUrlFunction_ = ol.TileUrlFunction.nullTileUrlFunction; + + /** + * @private + * @type {!ol.TileCache} + */ + this.tileCache_ = new ol.TileCache(); + + /** + * @private + * @type {string|undefined} + */ + this.template_ = undefined; + + var request = new goog.net.Jsonp(options.url); + request.send(undefined, goog.bind(this.handleTileJSONResponse, this)); +}; +goog.inherits(ol.source.TileUTFGrid, ol.source.Tile); + + +/** + * @inheritDoc + */ +ol.source.TileUTFGrid.prototype.canExpireCache = function() { + return this.tileCache_.canExpireCache(); +}; + + +/** + * @inheritDoc + */ +ol.source.TileUTFGrid.prototype.expireCache = function(usedTiles) { + this.tileCache_.expireCache(usedTiles); +}; + + +/** + * @return {string|undefined} The template from TileJSON. + * @api + */ +ol.source.TileUTFGrid.prototype.getTemplate = function() { + return this.template_; +}; + + +/** + * Calls the callback (synchronously by default) with the available data + * for given coordinate and resolution (or `null` if not yet loaded or + * in case of an error). + * @param {ol.Coordinate} coordinate Coordinate. + * @param {number} resolution Resolution. + * @param {function(this: T, Object)} callback Callback. + * @param {T=} opt_this The object to use as `this` in the callback. + * @param {boolean=} opt_request If `true` the callback is always async. + * The tile data is requested if not yet loaded. + * @template T + * @api + */ +ol.source.TileUTFGrid.prototype.forDataAtCoordinateAndResolution = function( + coordinate, resolution, callback, opt_this, opt_request) { + if (!goog.isNull(this.tileGrid)) { + var tileCoord = this.tileGrid.getTileCoordForCoordAndResolution( + coordinate, resolution); + var tile = /** @type {!ol.source.TileUTFGridTile_} */(this.getTile( + tileCoord[0], tileCoord[1], tileCoord[2], 1, this.getProjection())); + tile.forDataAtCoordinate(coordinate, callback, opt_this, opt_request); + } else { + if (opt_request === true) { + goog.async.nextTick(function() { + callback.call(opt_this, null); + }); + } else { + callback.call(opt_this, null); + } + } +}; + + +/** + * TODO: very similar to ol.source.TileJSON#handleTileJSONResponse + * @protected + * @param {TileJSON} tileJSON Tile JSON. + */ +ol.source.TileUTFGrid.prototype.handleTileJSONResponse = function(tileJSON) { + + var epsg4326Projection = ol.proj.get('EPSG:4326'); + + var sourceProjection = this.getProjection(); + var extent; + if (goog.isDef(tileJSON.bounds)) { + var transform = ol.proj.getTransformFromProjections( + epsg4326Projection, sourceProjection); + extent = ol.extent.applyTransform(tileJSON.bounds, transform); + } + + if (goog.isDef(tileJSON.scheme)) { + goog.asserts.assert(tileJSON.scheme == 'xyz'); + } + var minZoom = tileJSON.minzoom || 0; + var maxZoom = tileJSON.maxzoom || 22; + var tileGrid = new ol.tilegrid.XYZ({ + extent: ol.tilegrid.extentFromProjection(sourceProjection), + maxZoom: maxZoom, + minZoom: minZoom + }); + this.tileGrid = tileGrid; + + this.template_ = tileJSON.template; + + var grids = tileJSON.grids; + if (!goog.isDefAndNotNull(grids)) { + this.setState(ol.source.State.ERROR); + return; + } + + this.tileUrlFunction_ = ol.TileUrlFunction.withTileCoordTransform( + tileGrid.createTileCoordTransform({ + extent: extent + }), + ol.TileUrlFunction.createFromTemplates(grids)); + + if (goog.isDef(tileJSON.attribution)) { + var attributionExtent = goog.isDef(extent) ? + extent : epsg4326Projection.getExtent(); + /** @type {Object.>} */ + var tileRanges = {}; + var z, zKey; + for (z = minZoom; z <= maxZoom; ++z) { + zKey = z.toString(); + tileRanges[zKey] = + [tileGrid.getTileRangeForExtentAndZ(attributionExtent, z)]; + } + this.setAttributions([ + new ol.Attribution({ + html: tileJSON.attribution, + tileRanges: tileRanges + }) + ]); + } + + this.setState(ol.source.State.READY); + +}; + + +/** + * @inheritDoc + */ +ol.source.TileUTFGrid.prototype.getTile = + function(z, x, y, pixelRatio, projection) { + var tileCoordKey = this.getKeyZXY(z, x, y); + if (this.tileCache_.containsKey(tileCoordKey)) { + return /** @type {!ol.Tile} */ (this.tileCache_.get(tileCoordKey)); + } else { + goog.asserts.assert(projection); + var tileCoord = [z, x, y]; + var tileUrl = this.tileUrlFunction_(tileCoord, pixelRatio, projection); + var tile = new ol.source.TileUTFGridTile_( + tileCoord, + goog.isDef(tileUrl) ? ol.TileState.IDLE : ol.TileState.EMPTY, + goog.isDef(tileUrl) ? tileUrl : '', + this.tileGrid.getTileCoordExtent(tileCoord), + this.preemptive_); + this.tileCache_.set(tileCoordKey, tile); + return tile; + } +}; + + +/** + * @inheritDoc + */ +ol.source.TileUTFGrid.prototype.useTile = function(z, x, y) { + var tileCoordKey = this.getKeyZXY(z, x, y); + if (this.tileCache_.containsKey(tileCoordKey)) { + this.tileCache_.get(tileCoordKey); + } +}; + + + +/** + * @constructor + * @extends {ol.Tile} + * @param {ol.TileCoord} tileCoord Tile coordinate. + * @param {ol.TileState} state State. + * @param {string} src Image source URI. + * @param {ol.Extent} extent Extent of the tile. + * @param {boolean} preemptive Load the tile when visible (before it's needed). + * @private + */ +ol.source.TileUTFGridTile_ = + function(tileCoord, state, src, extent, preemptive) { + + goog.base(this, tileCoord, state); + + /** + * @private + * @type {string} + */ + this.src_ = src; + + /** + * @private + * @type {ol.Extent} + */ + this.extent_ = extent; + + /** + * @private + * @type {boolean} + */ + this.preemptive_ = preemptive; + + /** + * @private + * @type {Array.} + */ + this.grid_ = null; + + /** + * @private + * @type {Array.} + */ + this.keys_ = null; + + /** + * @private + * @type {Object.|undefined} + */ + this.data_ = null; +}; +goog.inherits(ol.source.TileUTFGridTile_, ol.Tile); + + +/** + * @inheritDoc + */ +ol.source.TileUTFGridTile_.prototype.getImage = function(opt_context) { + return null; +}; + + +/** + * Synchronously returns data at given coordinate (if available). + * @param {ol.Coordinate} coordinate Coordinate. + * @return {Object} + */ +ol.source.TileUTFGridTile_.prototype.getData = function(coordinate) { + if (goog.isNull(this.grid_) || goog.isNull(this.keys_) || + goog.isNull(this.data_)) { + return null; + } + var xRelative = (coordinate[0] - this.extent_[0]) / + (this.extent_[2] - this.extent_[0]); + var yRelative = (coordinate[1] - this.extent_[1]) / + (this.extent_[3] - this.extent_[1]); + + var row = this.grid_[Math.floor((1 - yRelative) * this.grid_.length)]; + + if (!goog.isString(row)) { + return null; + } + + var code = row.charCodeAt(Math.floor(xRelative * row.length)); + if (code >= 93) { + code--; + } + if (code >= 35) { + code--; + } + code -= 32; + + var key = this.keys_[code]; + + return goog.isDefAndNotNull(key) ? this.data_[key] : null; +}; + + +/** + * Calls the callback (synchronously by default) with the available data + * for given coordinate (or `null` if not yet loaded). + * @param {ol.Coordinate} coordinate Coordinate. + * @param {function(this: T, Object)} callback Callback. + * @param {T=} opt_this The object to use as `this` in the callback. + * @param {boolean=} opt_request If `true` the callback is always async. + * The tile data is requested if not yet loaded. + * @template T + */ +ol.source.TileUTFGridTile_.prototype.forDataAtCoordinate = + function(coordinate, callback, opt_this, opt_request) { + if (this.state == ol.TileState.IDLE && opt_request === true) { + goog.events.listenOnce(this, goog.events.EventType.CHANGE, function(e) { + callback.call(opt_this, this.getData(coordinate)); + }, false, this); + this.loadInternal_(); + } else { + if (opt_request === true) { + goog.async.nextTick(function() { + callback.call(opt_this, this.getData(coordinate)); + }, this); + } else { + callback.call(opt_this, this.getData(coordinate)); + } + } +}; + + +/** + * @inheritDoc + */ +ol.source.TileUTFGridTile_.prototype.getKey = function() { + return this.src_; +}; + + +/** + * @private + */ +ol.source.TileUTFGridTile_.prototype.handleError_ = function() { + this.state = ol.TileState.ERROR; + this.changed(); +}; + + +/** + * @param {!UTFGridJSON} json + * @private + */ +ol.source.TileUTFGridTile_.prototype.handleLoad_ = function(json) { + this.grid_ = json.grid; + this.keys_ = json.keys; + this.data_ = json.data; + + this.state = ol.TileState.EMPTY; + this.changed(); +}; + + +/** + * @private + */ +ol.source.TileUTFGridTile_.prototype.loadInternal_ = function() { + if (this.state == ol.TileState.IDLE) { + this.state = ol.TileState.LOADING; + var request = new goog.net.Jsonp(this.src_); + request.send(undefined, goog.bind(this.handleLoad_, this), + goog.bind(this.handleError_, this)); + } +}; + + +/** + * Load not yet loaded URI. + */ +ol.source.TileUTFGridTile_.prototype.load = function() { + if (this.preemptive_) { + this.loadInternal_(); + } +};