diff --git a/externs/olx.js b/externs/olx.js index eef86f69d0..57986ca413 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -1685,6 +1685,40 @@ olx.format.EsriJSONOptions; olx.format.EsriJSONOptions.prototype.geometryName; +/** + * @typedef {{geometryName: (string|undefined), + * layers: (Array.|undefined), + * layerName: (string|undefined)}} + * @api + */ +olx.format.MVTOptions; + + +/** + * Geometry name to use when creating features. Default is 'geometry'. + * @type {string|undefined} + * @api + */ +olx.format.MVTOptions.prototype.geometryName; + + +/** + * Name of the feature attribute that holds the layer name. Default is 'layer'. + * @type {string|undefined} + * @api + */ +olx.format.MVTOptions.prototype.layerName; + + +/** + * Layers to read features from. If not provided, features will be read from all + * layers. + * @type {Array.|undefined} + * @api + */ +olx.format.MVTOptions.prototype.layers; + + /** * @typedef {{factor: (number|undefined), * geometryLayout: (ol.geom.GeometryLayout|undefined)}} diff --git a/package.json b/package.json index bf323b85ae..20c1e81495 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,11 @@ "metalsmith": "1.6.0", "metalsmith-templates": "0.7.0", "nomnom": "1.8.0", + "pbf": "1.3.5", "pixelworks": "1.0.0", "rbush": "1.3.5", "temp": "0.8.1", + "vector-tile": "1.1.3", "walk": "2.3.4" }, "devDependencies": { @@ -64,6 +66,8 @@ }, "ext": [ "rbush", - {"module": "pixelworks", "browserify": true} + {"module": "pbf", "browserify": true}, + {"module": "pixelworks", "browserify": true}, + {"module": "vector-tile", "name": "vectortile", "browserify": true} ] } diff --git a/src/ol/featureloader.js b/src/ol/featureloader.js index 0450108f19..735ca5b67f 100644 --- a/src/ol/featureloader.js +++ b/src/ol/featureloader.js @@ -61,7 +61,10 @@ ol.featureloader.loadFeaturesXhr = function(url, format, success) { */ function(extent, resolution, projection) { var xhrIo = new goog.net.XhrIo(); - xhrIo.setResponseType(goog.net.XhrIo.ResponseType.TEXT); + xhrIo.setResponseType( + format.getType() == ol.format.FormatType.ARRAY_BUFFER ? + goog.net.XhrIo.ResponseType.ARRAY_BUFFER : + goog.net.XhrIo.ResponseType.TEXT); goog.events.listen(xhrIo, goog.net.EventType.COMPLETE, /** * @param {Event} event Event. @@ -87,6 +90,8 @@ ol.featureloader.loadFeaturesXhr = function(url, format, success) { if (!source) { source = ol.xml.parse(xhrIo.getResponseText()); } + } else if (type == ol.format.FormatType.ARRAY_BUFFER) { + source = xhrIo.getResponse(); } else { goog.asserts.fail('unexpected format type'); } diff --git a/src/ol/format/featureformat.js b/src/ol/format/featureformat.js index 59d0c77943..25c10aa79d 100644 --- a/src/ol/format/featureformat.js +++ b/src/ol/format/featureformat.js @@ -95,7 +95,7 @@ ol.format.Feature.prototype.readFeature = goog.abstractMethod; /** * Read all features from a source. * - * @param {Document|Node|Object|string} source Source. + * @param {Document|Node|ArrayBuffer|Object|string} source Source. * @param {olx.format.ReadOptions=} opt_options Read options. * @return {Array.} Features. */ diff --git a/src/ol/format/format.js b/src/ol/format/format.js index e0cca0fdae..3fa3586e3f 100644 --- a/src/ol/format/format.js +++ b/src/ol/format/format.js @@ -5,6 +5,7 @@ goog.provide('ol.format.FormatType'); * @enum {string} */ ol.format.FormatType = { + ARRAY_BUFFER: 'arraybuffer', JSON: 'json', TEXT: 'text', XML: 'xml' diff --git a/src/ol/format/mvtformat.js b/src/ol/format/mvtformat.js new file mode 100644 index 0000000000..97936f1585 --- /dev/null +++ b/src/ol/format/mvtformat.js @@ -0,0 +1,196 @@ +//FIXME Implement projection handling + +goog.provide('ol.format.MVT'); + +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('ol.Feature'); +goog.require('ol.ext.pbf'); +goog.require('ol.ext.vectortile'); +goog.require('ol.format.Feature'); +goog.require('ol.format.FormatType'); +goog.require('ol.geom.Geometry'); +goog.require('ol.geom.GeometryLayout'); +goog.require('ol.geom.LineString'); +goog.require('ol.geom.MultiLineString'); +goog.require('ol.geom.MultiPoint'); +goog.require('ol.geom.Point'); +goog.require('ol.geom.Polygon'); +goog.require('ol.proj'); +goog.require('ol.proj.Projection'); +goog.require('ol.proj.Units'); + + + +/** + * @classdesc + * Feature format for reading data in the Mapbox MVT format. + * + * @constructor + * @extends {ol.format.Feature} + * @param {olx.format.MVTOptions=} opt_options Options. + * @api + */ +ol.format.MVT = function(opt_options) { + + goog.base(this); + + var options = goog.isDef(opt_options) ? opt_options : {}; + + /** + * @type {ol.proj.Projection} + */ + this.defaultDataProjection = new ol.proj.Projection({ + code: 'EPSG:3857', + units: ol.proj.Units.TILE_PIXELS + }); + + /** + * @private + * @type {string} + */ + this.geometryName_ = goog.isDef(options.geometryName) ? + options.geometryName : 'geometry'; + + /** + * @private + * @type {string} + */ + this.layerName_ = goog.isDef(options.layerName) ? options.layerName : 'layer'; + + /** + * @private + * @type {Array.} + */ + this.layers_ = goog.isDef(options.layers) ? options.layers : null; + +}; +goog.inherits(ol.format.MVT, ol.format.Feature); + + +/** + * @inheritDoc + */ +ol.format.MVT.prototype.getType = function() { + return ol.format.FormatType.ARRAY_BUFFER; +}; + + +/** + * @private + * @param {Object} rawFeature Raw Mapbox feature. + * @param {olx.format.ReadOptions=} opt_options Read options. + * @return {ol.Feature} Feature. + */ +ol.format.MVT.prototype.readFeature_ = function(rawFeature, opt_options) { + var feature = new ol.Feature(); + var values = rawFeature.properties; + var geometry = ol.format.Feature.transformWithOptions( + ol.format.MVT.readGeometry_(rawFeature), false, + this.adaptOptions(opt_options)); + if (!goog.isNull(geometry)) { + goog.asserts.assertInstanceof(geometry, ol.geom.Geometry); + values[this.geometryName_] = geometry; + } + feature.setProperties(rawFeature.properties); + feature.setGeometryName(this.geometryName_); + return feature; +}; + + +/** + * @inheritDoc + */ +ol.format.MVT.prototype.readFeatures = function(source, opt_options) { + goog.asserts.assertInstanceof(source, ArrayBuffer); + + var layerName = this.layerName_; + var layers = this.layers_; + + var pbf = new ol.ext.pbf(source); + var tile = new ol.ext.vectortile.VectorTile(pbf); + var features = []; + var layer, feature; + for (var name in tile.layers) { + if (!goog.isNull(layers) && !goog.array.contains(layers, name)) { + continue; + } + layer = tile.layers[name]; + + for (var i = 0, ii = layer.length; i < layer.length; ++i) { + feature = this.readFeature_(layer.feature(i), opt_options); + feature.set(layerName, name); + features.push(feature); + } + } + + return features; +}; + + +/** + * @inheritDoc + */ +ol.format.MVT.prototype.readProjection = function(source) { + return this.defaultDataProjection; +}; + + +/** + * Sets the layers that features will be read from. + * @param {Array.} layers Layers. + * @api + */ +ol.format.MVT.prototype.setLayers = function(layers) { + this.layers_ = layers; +}; + + +/** + * @private + * @param {Object} rawFeature Raw Mapbox feature. + * @return {ol.geom.Geometry} Geometry. + */ +ol.format.MVT.readGeometry_ = function(rawFeature) { + var type = rawFeature.type; + if (type === 0) { + return null; + } + + var coords = rawFeature.loadGeometry(); + + var end = 0; + var ends = []; + var flatCoordinates = []; + var line, coord; + for (var i = 0, ii = coords.length; i < ii; ++i) { + line = coords[i]; + for (var j = 0, jj = line.length; j < jj; ++j) { + coord = line[j]; + // Non-tilespace coords can be calculated here when a TileGrid and + // TileCoord are known. + flatCoordinates.push(coord.x, coord.y); + } + end += 2 * j; + ends.push(end); + } + + var geom; + if (type === 1) { + geom = coords.length === 1 ? + new ol.geom.Point(null) : new ol.geom.MultiPoint(null); + } else if (type === 2) { + if (coords.length === 1) { + geom = new ol.geom.LineString(null); + } else { + geom = new ol.geom.MultiLineString(null); + } + } else { + geom = new ol.geom.Polygon(null); + } + + geom.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates, + ends); + + return geom; +}; diff --git a/test/spec/ol/data/14-8938-5680.vector.pbf b/test/spec/ol/data/14-8938-5680.vector.pbf new file mode 100644 index 0000000000..0ed0c1ee24 Binary files /dev/null and b/test/spec/ol/data/14-8938-5680.vector.pbf differ diff --git a/test/spec/ol/format/mvtformat.test.js b/test/spec/ol/format/mvtformat.test.js new file mode 100644 index 0000000000..9f77d889ac --- /dev/null +++ b/test/spec/ol/format/mvtformat.test.js @@ -0,0 +1,68 @@ +goog.provide('ol.test.format.MVT'); + + +describe('ol.format.MVT', function() { + + var data; + beforeEach(function(done) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'spec/ol/data/14-8938-5680.vector.pbf'); + xhr.responseType = 'arraybuffer'; + xhr.onload = function() { + data = xhr.response; + done(); + }; + xhr.send(); + }); + + describe('#readFeatures', function() { + + it('parses only specified layers', function() { + var format = new ol.format.MVT({layers: ['water']}); + var features = format.readFeatures(data); + expect(features.length).to.be(10); + }); + + it('parses geometries correctly', function() { + var format = new ol.format.MVT({layers: ['poi_label']}); + var pbf = new ol.ext.pbf(data); + var tile = new ol.ext.vectortile.VectorTile(pbf); + var geometry, rawGeometry; + + rawGeometry = tile.layers['poi_label'].feature(0).loadGeometry(); + geometry = format.readFeatures(data)[0] + .getGeometry(); + expect(geometry.getType()).to.be('Point'); + expect(geometry.getCoordinates()) + .to.eql([rawGeometry[0][0].x, rawGeometry[0][0].y]); + + rawGeometry = tile.layers['water'].feature(0).loadGeometry(); + format.setLayers(['water']); + geometry = format.readFeatures(data)[0] + .getGeometry(); + expect(geometry.getType()).to.be('Polygon'); + expect(rawGeometry[0].length) + .to.equal(geometry.getCoordinates()[0].length); + expect(geometry.getCoordinates()[0][0]) + .to.eql([rawGeometry[0][0].x, rawGeometry[0][0].y]); + + rawGeometry = tile.layers['barrier_line'].feature(0).loadGeometry(); + format.setLayers(['barrier_line']); + geometry = format.readFeatures(data)[0] + .getGeometry(); + expect(geometry.getType()).to.be('MultiLineString'); + expect(rawGeometry[1].length) + .to.equal(geometry.getCoordinates()[1].length); + expect(geometry.getCoordinates()[1][0]) + .to.eql([rawGeometry[1][0].x, rawGeometry[1][0].y]); + }); + + }); + +}); + + +goog.require('ol.Feature'); +goog.require('ol.ext.pbf'); +goog.require('ol.ext.vectortile'); +goog.require('ol.format.MVT');