diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index 5912163256..9bc5b8e35b 100644 --- a/changelog/upgrade-notes.md +++ b/changelog/upgrade-notes.md @@ -2,6 +2,35 @@ ### Next Release +#### Minor change for custom `tileLoadFunction` with `ol.source.VectorTile` + +It is no longer necessary to set the projection on the tile. Instead, the `readFeatures` method must be called with the tile's extent as `extent` option and the view's projection as `featureProjection`. + +Before: +```js +tile.setLoader(function() { + var data = // ... fetch data + var format = tile.getFormat(); + tile.setFeatures(format.readFeatures(data)); + tile.setProjection(format.readProjection(data)); + // uncomment the line below for ol.format.MVT only + //tile.setExtent(format.getLastExtent()); +}); +``` + +After: +```js +tile.setLoader(function() { + var data = // ... fetch data + var format = tile.getFormat(); + tile.setFeatures(format.readFeatures(data, { + featureProjection: map.getView().getProjection(), + // uncomment the line below for ol.format.MVT only + //extent: tile.getExtent() + })); +); +``` + ### v4.3.0 #### `ol.source.VectorTile` no longer requires a `tileGrid` option diff --git a/doc/errors/index.md b/doc/errors/index.md index e6b14708a8..4d7770b509 100644 --- a/doc/errors/index.md +++ b/doc/errors/index.md @@ -224,3 +224,7 @@ At least 2 conditions are required. ### 58 Duplicate item added to a unique collection. For example, it may be that you tried to add the same layer to a map twice. Check for calls to `map.addLayer()` or other places where the map's layer collection is modified. + +### 59 + +Invalid command found in the PBF. This indicates that the loaded vector tile may be corrupt. diff --git a/externs/olx.js b/externs/olx.js index a010c68160..d2c8c06a7b 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -1870,6 +1870,7 @@ olx.format; /** * @typedef {{dataProjection: ol.ProjectionLike, + * extent: (ol.Extent|undefined), * featureProjection: ol.ProjectionLike, * rightHanded: (boolean|undefined)}} */ @@ -1888,6 +1889,15 @@ olx.format.ReadOptions; olx.format.ReadOptions.prototype.dataProjection; +/** + * Tile extent of the tile being read. This is only used and required for + * {@link ol.format.MVT}. + * @type {ol.Extent} + * @api + */ +olx.format.ReadOptions.prototype.extent; + + /** * Projection of the feature geometries created by the format reader. If not * provided, features will be returned in the `dataProjection`. @@ -5094,10 +5104,11 @@ olx.source.VectorTileOptions.prototype.tileGrid; * tile.setLoader(function() { * var data = // ... fetch data * var format = tile.getFormat(); - * tile.setFeatures(format.readFeatures(data)); - * tile.setProjection(format.readProjection(data)); - * // uncomment the line below for ol.format.MVT only - * //tile.setExtent(format.getLastExtent()); + * tile.setFeatures(format.readFeatures(data, { + * // uncomment the line below for ol.format.MVT only + * extent: tile.getExtent(), + * featureProjection: map.getView().getProjection() + * })); * }; * }); * ``` diff --git a/externs/topojson.js b/externs/topojson.js index 3a606509a9..111f966ad7 100644 --- a/externs/topojson.js +++ b/externs/topojson.js @@ -75,6 +75,11 @@ TopoJSONGeometry.prototype.type; TopoJSONGeometry.prototype.id; +/** + * @type {Object.|undefined} + */ +TopoJSONGeometry.prototype.properties; + /** * @constructor diff --git a/package.json b/package.json index 238e492a3c..324aa2d40f 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "rollup-plugin-commonjs": "^8.0.2", "rollup-plugin-node-resolve": "^3.0.0", "temp": "0.8.3", - "@mapbox/vector-tile": "1.3.0", "walk": "2.3.9" }, "devDependencies": { @@ -148,11 +147,6 @@ { "module": "pixelworks", "import": "Processor" - }, - { - "module": "@mapbox/vector-tile", - "name": "vectortile", - "import": "VectorTile" } ] } diff --git a/src/ol/format/mvt.js b/src/ol/format/mvt.js index 3b6e98f774..655a73de9d 100644 --- a/src/ol/format/mvt.js +++ b/src/ol/format/mvt.js @@ -3,8 +3,8 @@ goog.provide('ol.format.MVT'); goog.require('ol'); +goog.require('ol.asserts'); goog.require('ol.ext.PBF'); -goog.require('ol.ext.vectortile.VectorTile'); goog.require('ol.format.Feature'); goog.require('ol.format.FormatType'); goog.require('ol.geom.GeometryLayout'); @@ -12,8 +12,10 @@ goog.require('ol.geom.GeometryType'); goog.require('ol.geom.LineString'); goog.require('ol.geom.MultiLineString'); goog.require('ol.geom.MultiPoint'); +goog.require('ol.geom.MultiPolygon'); goog.require('ol.geom.Point'); goog.require('ol.geom.Polygon'); +goog.require('ol.geom.flat.orient'); goog.require('ol.proj.Projection'); goog.require('ol.proj.Units'); goog.require('ol.render.Feature'); @@ -38,7 +40,7 @@ ol.format.MVT = function(opt_options) { * @type {ol.proj.Projection} */ this.defaultDataProjection = new ol.proj.Projection({ - code: '', + code: 'EPSG:3857', units: ol.proj.Units.TILE_PIXELS }); @@ -79,6 +81,256 @@ ol.format.MVT = function(opt_options) { ol.inherits(ol.format.MVT, ol.format.Feature); +/** + * Reader callbacks for parsing the PBF. + * @type {Object.} + */ +ol.format.MVT.pbfReaders_ = { + layers: function(tag, layers, pbf) { + if (tag === 3) { + var layer = { + keys: [], + values: [], + features: [] + }; + var end = pbf.readVarint() + pbf.pos; + pbf.readFields(ol.format.MVT.pbfReaders_.layer, layer, end); + layer.length = layer.features.length; + if (layer.length) { + layers[layer.name] = layer; + } + } + }, + layer: function(tag, layer, pbf) { + if (tag === 15) { + layer.version = pbf.readVarint(); + } else if (tag === 1) { + layer.name = pbf.readString(); + } else if (tag === 5) { + layer.extent = pbf.readVarint(); + } else if (tag === 2) { + layer.features.push(pbf.pos); + } else if (tag === 3) { + layer.keys.push(pbf.readString()); + } else if (tag === 4) { + var value = null; + var end = pbf.readVarint() + pbf.pos; + while (pbf.pos < end) { + tag = pbf.readVarint() >> 3; + value = tag === 1 ? pbf.readString() : + tag === 2 ? pbf.readFloat() : + tag === 3 ? pbf.readDouble() : + tag === 4 ? pbf.readVarint64() : + tag === 5 ? pbf.readVarint() : + tag === 6 ? pbf.readSVarint() : + tag === 7 ? pbf.readBoolean() : null; + } + layer.values.push(value); + } + }, + feature: function(tag, feature, pbf) { + if (tag == 1) { + feature.id = pbf.readVarint(); + } else if (tag == 2) { + var end = pbf.readVarint() + pbf.pos; + while (pbf.pos < end) { + var key = feature.layer.keys[pbf.readVarint()]; + var value = feature.layer.values[pbf.readVarint()]; + feature.properties[key] = value; + } + } else if (tag == 3) { + feature.type = pbf.readVarint(); + } else if (tag == 4) { + feature.geometry = pbf.pos; + } + } +}; + + +/** + * Read a raw feature from the pbf offset stored at index `i` in the raw layer. + * @suppress {missingProperties} + * @private + * @param {ol.ext.PBF} pbf PBF. + * @param {Object} layer Raw layer. + * @param {number} i Index of the feature in the raw layer's `features` array. + * @return {Object} Raw feature. + */ +ol.format.MVT.readRawFeature_ = function(pbf, layer, i) { + pbf.pos = layer.features[i]; + var end = pbf.readVarint() + pbf.pos; + + var feature = { + layer: layer, + type: 0, + properties: {} + }; + pbf.readFields(ol.format.MVT.pbfReaders_.feature, feature, end); + return feature; +}; + + +/** + * Read the raw geometry from the pbf offset stored in a raw feature's geometry + * proeprty. + * @suppress {missingProperties} + * @private + * @param {ol.ext.PBF} pbf PBF. + * @param {Object} feature Raw feature. + * @param {Array.} flatCoordinates Array to store flat coordinates in. + * @param {Array.} ends Array to store ends in. + */ +ol.format.MVT.readRawGeometry_ = function(pbf, feature, flatCoordinates, ends) { + pbf.pos = feature.geometry; + + var end = pbf.readVarint() + pbf.pos; + var cmd = 1; + var length = 0; + var x = 0; + var y = 0; + var coordsLen = 0; + var currentEnd = 0; + + while (pbf.pos < end) { + if (!length) { + var cmdLen = pbf.readVarint(); + cmd = cmdLen & 0x7; + length = cmdLen >> 3; + } + + length--; + + if (cmd === 1 || cmd === 2) { + x += pbf.readSVarint(); + y += pbf.readSVarint(); + + if (cmd === 1) { // moveTo + if (coordsLen > currentEnd) { + ends.push(coordsLen); + currentEnd = coordsLen; + } + } + + flatCoordinates.push(x, y); + coordsLen += 2; + + } else if (cmd === 7) { + + if (coordsLen > currentEnd) { + // close polygon + flatCoordinates.push( + flatCoordinates[currentEnd], flatCoordinates[currentEnd + 1]); + coordsLen += 2; + } + + } else { + ol.asserts.assert(false, 59); // Invalid command found in the PBF + } + } + + if (coordsLen > currentEnd) { + ends.push(coordsLen); + currentEnd = coordsLen; + } + +}; + + +/** + * @suppress {missingProperties} + * @private + * @param {number} type The raw feature's geometry type + * @param {number} numEnds Number of ends of the flat coordinates of the + * geometry. + * @return {ol.geom.GeometryType} The geometry type. + */ +ol.format.MVT.getGeometryType_ = function(type, numEnds) { + /** @type {ol.geom.GeometryType} */ + var geometryType; + if (type === 1) { + geometryType = numEnds === 1 ? + ol.geom.GeometryType.POINT : ol.geom.GeometryType.MULTI_POINT; + } else if (type === 2) { + geometryType = numEnds === 1 ? + ol.geom.GeometryType.LINE_STRING : + ol.geom.GeometryType.MULTI_LINE_STRING; + } else if (type === 3) { + geometryType = ol.geom.GeometryType.POLYGON; + // MultiPolygon not relevant for rendering - winding order determines + // outer rings of polygons. + } + return geometryType; +}; + +/** + * @private + * @param {ol.ext.PBF} pbf PBF + * @param {Object} rawFeature Raw Mapbox feature. + * @param {olx.format.ReadOptions=} opt_options Read options. + * @return {ol.Feature|ol.render.Feature} Feature. + */ +ol.format.MVT.prototype.createFeature_ = function(pbf, rawFeature, opt_options) { + var type = rawFeature.type; + if (type === 0) { + return null; + } + + var feature; + var id = rawFeature.id; + var values = rawFeature.properties; + values[this.layerName_] = rawFeature.layer.name; + + var flatCoordinates = []; + var ends = []; + ol.format.MVT.readRawGeometry_(pbf, rawFeature, flatCoordinates, ends); + + var geometryType = ol.format.MVT.getGeometryType_(type, ends.length); + + if (this.featureClass_ === ol.render.Feature) { + feature = new this.featureClass_(geometryType, flatCoordinates, ends, values, id); + } else { + var geom; + if (geometryType == ol.geom.GeometryType.POLYGON) { + var endss = []; + var offset = 0; + var prevEndIndex = 0; + for (var i = 0, ii = ends.length; i < ii; ++i) { + var end = ends[i]; + if (!ol.geom.flat.orient.linearRingIsClockwise(flatCoordinates, offset, end, 2)) { + endss.push(ends.slice(prevEndIndex, i)); + prevEndIndex = i; + } + offset = end; + } + if (endss.length > 1) { + ends = endss; + geom = new ol.geom.MultiPolygon(null); + } else { + geom = new ol.geom.Polygon(null); + } + } else { + geom = geometryType === ol.geom.GeometryType.POINT ? new ol.geom.Point(null) : + geometryType === ol.geom.GeometryType.LINE_STRING ? new ol.geom.LineString(null) : + geometryType === ol.geom.GeometryType.POLYGON ? new ol.geom.Polygon(null) : + geometryType === ol.geom.GeometryType.MULTI_POINT ? new ol.geom.MultiPoint (null) : + geometryType === ol.geom.GeometryType.MULTI_LINE_STRING ? new ol.geom.MultiLineString(null) : + null; + } + geom.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates, ends); + feature = new this.featureClass_(); + if (this.geometryName_) { + feature.setGeometryName(this.geometryName_); + } + var geometry = ol.format.Feature.transformWithOptions(geom, false, this.adaptOptions(opt_options)); + feature.setGeometry(geometry); + feature.setId(id); + feature.setProperties(values); + } + + return feature; +}; + + /** * @inheritDoc * @api @@ -96,68 +348,6 @@ ol.format.MVT.prototype.getType = function() { }; -/** - * @private - * @param {Object} rawFeature Raw Mapbox feature. - * @param {string} layer Layer. - * @param {olx.format.ReadOptions=} opt_options Read options. - * @return {ol.Feature} Feature. - */ -ol.format.MVT.prototype.readFeature_ = function( - rawFeature, layer, opt_options) { - var feature = new this.featureClass_(); - var id = rawFeature.id; - var values = rawFeature.properties; - values[this.layerName_] = layer; - if (this.geometryName_) { - feature.setGeometryName(this.geometryName_); - } - var geometry = ol.format.Feature.transformWithOptions( - ol.format.MVT.readGeometry_(rawFeature), false, - this.adaptOptions(opt_options)); - feature.setGeometry(geometry); - feature.setId(id); - feature.setProperties(values); - return feature; -}; - - -/** - * @private - * @param {Object} rawFeature Raw Mapbox feature. - * @param {string} layer Layer. - * @return {ol.render.Feature} Feature. - */ -ol.format.MVT.prototype.readRenderFeature_ = function(rawFeature, layer) { - var coords = rawFeature.loadGeometry(); - var ends = []; - var flatCoordinates = []; - ol.format.MVT.calculateFlatCoordinates_(coords, flatCoordinates, ends); - - var type = rawFeature.type; - /** @type {ol.geom.GeometryType} */ - var geometryType; - if (type === 1) { - geometryType = coords.length === 1 ? - ol.geom.GeometryType.POINT : ol.geom.GeometryType.MULTI_POINT; - } else if (type === 2) { - if (coords.length === 1) { - geometryType = ol.geom.GeometryType.LINE_STRING; - } else { - geometryType = ol.geom.GeometryType.MULTI_LINE_STRING; - } - } else if (type === 3) { - geometryType = ol.geom.GeometryType.POLYGON; - } - - var values = rawFeature.properties; - values[this.layerName_] = layer; - var id = rawFeature.id; - - return new this.featureClass_(geometryType, flatCoordinates, ends, values, id); -}; - - /** * @inheritDoc * @api @@ -166,27 +356,22 @@ ol.format.MVT.prototype.readFeatures = function(source, opt_options) { var layers = this.layers_; var pbf = new ol.ext.PBF(/** @type {ArrayBuffer} */ (source)); - var tile = new ol.ext.vectortile.VectorTile(pbf); + var pbfLayers = pbf.readFields(ol.format.MVT.pbfReaders_.layers, {}); + /** @type {Array.} */ var features = []; - var featureClass = this.featureClass_; - var layer, feature; - for (var name in tile.layers) { + var pbfLayer; + for (var name in pbfLayers) { if (layers && layers.indexOf(name) == -1) { continue; } - layer = tile.layers[name]; + pbfLayer = pbfLayers[name]; var rawFeature; - for (var i = 0, ii = layer.length; i < ii; ++i) { - rawFeature = layer.feature(i); - if (featureClass === ol.render.Feature) { - feature = this.readRenderFeature_(rawFeature, name); - } else { - feature = this.readFeature_(rawFeature, name, opt_options); - } - features.push(feature); + for (var i = 0, ii = pbfLayer.length; i < ii; ++i) { + rawFeature = ol.format.MVT.readRawFeature_(pbf, pbfLayer, i); + features.push(this.createFeature_(pbf, rawFeature)); } - this.extent_ = layer ? [0, 0, layer.extent, layer.extent] : null; + this.extent_ = pbfLayer ? [0, 0, pbfLayer.extent, pbfLayer.extent] : null; } return features; @@ -212,68 +397,6 @@ ol.format.MVT.prototype.setLayers = function(layers) { }; -/** - * @private - * @param {Object} coords Raw feature coordinates. - * @param {Array.} flatCoordinates Flat coordinates to be populated by - * this function. - * @param {Array.} ends Ends to be populated by this function. - */ -ol.format.MVT.calculateFlatCoordinates_ = function( - coords, flatCoordinates, ends) { - var end = 0; - for (var i = 0, ii = coords.length; i < ii; ++i) { - var line = coords[i]; - var j, jj; - for (j = 0, jj = line.length; j < jj; ++j) { - var 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); - } -}; - - -/** - * @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 ends = []; - var flatCoordinates = []; - ol.format.MVT.calculateFlatCoordinates_(coords, flatCoordinates, ends); - - 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 if (type === 3) { - geom = new ol.geom.Polygon(null); - } - - geom.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates, - ends); - - return geom; -}; - - /** * Not implemented. * @override diff --git a/src/ol/geom/geometry.js b/src/ol/geom/geometry.js index b73c68e1df..67c5498f0c 100644 --- a/src/ol/geom/geometry.js +++ b/src/ol/geom/geometry.js @@ -4,7 +4,10 @@ goog.require('ol'); goog.require('ol.Object'); goog.require('ol.extent'); goog.require('ol.functions'); +goog.require('ol.geom.flat.transform'); goog.require('ol.proj'); +goog.require('ol.proj.Units'); +goog.require('ol.transform'); /** @@ -55,6 +58,12 @@ ol.geom.Geometry = function() { */ this.simplifiedGeometryRevision = 0; + /** + * @private + * @type {ol.Transform} + */ + this.tmpTransform_ = ol.transform.create(); + }; ol.inherits(ol.geom.Geometry, ol.Object); @@ -244,6 +253,22 @@ ol.geom.Geometry.prototype.translate = function(deltaX, deltaY) {}; * @api */ ol.geom.Geometry.prototype.transform = function(source, destination) { - this.applyTransform(ol.proj.getTransform(source, destination)); + var tmpTransform = this.tmpTransform_; + source = ol.proj.get(source); + var transformFn = source.getUnits() == ol.proj.Units.TILE_PIXELS ? + function(inCoordinates, outCoordinates, stride) { + var pixelExtent = source.getExtent(); + var projectedExtent = source.getWorldExtent(); + var scale = ol.extent.getHeight(projectedExtent) / ol.extent.getHeight(pixelExtent); + ol.transform.compose(tmpTransform, + projectedExtent[0], projectedExtent[3], + scale, -scale, 0, + 0, 0); + ol.geom.flat.transform.transform2D(inCoordinates, 0, inCoordinates.length, stride, + tmpTransform, outCoordinates); + return ol.proj.getTransform(source, destination)(inCoordinates, outCoordinates, stride); + } : + ol.proj.getTransform(source, destination); + this.applyTransform(transformFn); return this; }; diff --git a/src/ol/proj/projection.js b/src/ol/proj/projection.js index 85edc64cef..18b8eb8edf 100644 --- a/src/ol/proj/projection.js +++ b/src/ol/proj/projection.js @@ -43,18 +43,27 @@ ol.proj.Projection = function(options) { this.code_ = options.code; /** + * Units of projected coordinates. When set to `ol.proj.Units.TILE_PIXELS`, a + * `this.extent_` and `this.worldExtent_` must be configured properly for each + * tile. * @private * @type {ol.proj.Units} */ this.units_ = /** @type {ol.proj.Units} */ (options.units); /** + * Validity extent of the projection in projected coordinates. For projections + * with `ol.proj.Units.TILE_PIXELS` units, this is the extent of the tile in + * tile pixel space. * @private * @type {ol.Extent} */ this.extent_ = options.extent !== undefined ? options.extent : null; /** + * Extent of the world in EPSG:4326. For projections with + * `ol.proj.Units.TILE_PIXELS` units, this is the extent of the tile in + * projected coordinate space. * @private * @type {ol.Extent} */ diff --git a/src/ol/render/feature.js b/src/ol/render/feature.js index 702dbeb2c2..d45e085111 100644 --- a/src/ol/render/feature.js +++ b/src/ol/render/feature.js @@ -3,12 +3,14 @@ goog.provide('ol.render.Feature'); goog.require('ol'); goog.require('ol.extent'); goog.require('ol.geom.GeometryType'); +goog.require('ol.geom.flat.transform'); +goog.require('ol.transform'); /** * Lightweight, read-only, {@link ol.Feature} and {@link ol.geom.Geometry} like - * structure, optimized for rendering and styling. Geometry access through the - * API is limited to getting the type and extent of the geometry. + * structure, optimized for vector tile rendering and styling. Geometry access + * through the API is limited to getting the type and extent of the geometry. * * @constructor * @param {ol.geom.GeometryType} type Geometry type. @@ -54,6 +56,13 @@ ol.render.Feature = function(type, flatCoordinates, ends, properties, id) { * @type {Object.} */ this.properties_ = properties; + + + /** + * @private + * @type {ol.Transform} + */ + this.tmpTransform_ = ol.transform.create(); }; @@ -71,7 +80,8 @@ ol.render.Feature.prototype.get = function(key) { /** * @return {Array.|Array.>} Ends or endss. */ -ol.render.Feature.prototype.getEnds = function() { +ol.render.Feature.prototype.getEnds = +ol.render.Feature.prototype.getEndss = function() { return this.ends_; }; @@ -169,3 +179,23 @@ ol.render.Feature.prototype.getStyleFunction = ol.nullFunction; ol.render.Feature.prototype.getType = function() { return this.type_; }; + +/** + * Transform geometry coordinates from tile pixel space to projected. + * The SRS of the source and destination are expected to be the same. + * + * @param {ol.ProjectionLike} source The current projection + * @param {ol.ProjectionLike} destination The desired projection. + */ +ol.render.Feature.prototype.transform = function(source, destination) { + var pixelExtent = source.getExtent(); + var projectedExtent = source.getWorldExtent(); + var scale = ol.extent.getHeight(projectedExtent) / ol.extent.getHeight(pixelExtent); + var transform = this.tmpTransform_; + ol.transform.compose(transform, + projectedExtent[0], projectedExtent[3], + scale, -scale, 0, + 0, 0); + ol.geom.flat.transform.transform2D(this.flatCoordinates_, 0, this.flatCoordinates_.length, 2, + transform, this.flatCoordinates_); +}; diff --git a/src/ol/renderer/canvas/vectortilelayer.js b/src/ol/renderer/canvas/vectortilelayer.js index ed0ffead4e..995ed8a920 100644 --- a/src/ol/renderer/canvas/vectortilelayer.js +++ b/src/ol/renderer/canvas/vectortilelayer.js @@ -15,7 +15,6 @@ goog.require('ol.render.replay'); goog.require('ol.renderer.Type'); goog.require('ol.renderer.canvas.TileLayer'); goog.require('ol.renderer.vector'); -goog.require('ol.size'); goog.require('ol.transform'); @@ -156,36 +155,21 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function( if (sourceTile.getState() == ol.TileState.ERROR) { continue; } - replayState.dirty = false; var sourceTileCoord = sourceTile.tileCoord; - var tileProjection = sourceTile.getProjection(); - var sourceTileResolution = sourceTileGrid.getResolution(sourceTile.tileCoord[0]); var sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord); var sharedExtent = ol.extent.getIntersection(tileExtent, sourceTileExtent); - var extent, reproject, tileResolution; - if (tileProjection.getUnits() == ol.proj.Units.TILE_PIXELS) { - var tilePixelRatio = tileResolution = this.getTilePixelRatio_(source, sourceTile); - var transform = ol.transform.compose(this.tmpTransform_, - 0, 0, - 1 / sourceTileResolution * tilePixelRatio, -1 / sourceTileResolution * tilePixelRatio, - 0, - -sourceTileExtent[0], -sourceTileExtent[3]); - extent = (ol.transform.apply(transform, [sharedExtent[0], sharedExtent[3]]) - .concat(ol.transform.apply(transform, [sharedExtent[2], sharedExtent[1]]))); - } else { - tileResolution = resolution; - extent = sharedExtent; - if (!ol.proj.equivalent(projection, tileProjection)) { - reproject = true; - sourceTile.setProjection(projection); - } + var tileProjection = sourceTile.getProjection(); + var reproject = false; + if (!ol.proj.equivalent(projection, tileProjection)) { + reproject = true; + sourceTile.setProjection(projection); } replayState.dirty = false; - var replayGroup = new ol.render.canvas.ReplayGroup(0, extent, - tileResolution, source.getOverlaps(), layer.getRenderBuffer()); + var replayGroup = new ol.render.canvas.ReplayGroup(0, sharedExtent, + resolution, source.getOverlaps(), layer.getRenderBuffer()); var squaredTolerance = ol.renderer.vector.getSquaredTolerance( - tileResolution, pixelRatio); + resolution, pixelRatio); /** * @param {ol.Feature|ol.render.Feature} feature Feature. @@ -221,6 +205,12 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function( for (var i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; if (reproject) { + if (tileProjection.getUnits() == ol.proj.Units.TILE_PIXELS) { + // projected tile extent + tileProjection.setWorldExtent(sourceTileExtent); + // tile extent in tile pixel space + tileProjection.setExtent(sourceTile.getExtent()); + } feature.getGeometry().transform(tileProjection, projection); } renderFeature.call(this, feature); @@ -263,10 +253,9 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi var source = /** @type {ol.source.VectorTile} */ (layer.getSource()); var tileGrid = source.getTileGridForProjection(frameState.viewState.projection); - var sourceTileGrid = source.getTileGrid(); - var bufferedExtent, found, tileSpaceCoordinate; - var i, ii, origin, replayGroup; - var tile, tileCoord, tileExtent, tilePixelRatio, tileRenderResolution; + var bufferedExtent, found; + var i, ii, replayGroup; + var tile, tileCoord, tileExtent; for (i = 0, ii = renderedTiles.length; i < ii; ++i) { tile = renderedTiles[i]; tileCoord = tile.tileCoord; @@ -280,25 +269,9 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi if (sourceTile.getState() == ol.TileState.ERROR) { continue; } - if (sourceTile.getProjection().getUnits() === ol.proj.Units.TILE_PIXELS) { - var sourceTileCoord = sourceTile.tileCoord; - var sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord, this.tmpExtent); - origin = ol.extent.getTopLeft(sourceTileExtent); - tilePixelRatio = this.getTilePixelRatio_(source, sourceTile); - var sourceTileResolution = sourceTileGrid.getResolution(sourceTileCoord[0]); - tileRenderResolution = sourceTileResolution / tilePixelRatio; - tileSpaceCoordinate = [ - (coordinate[0] - origin[0]) / tileRenderResolution, - (origin[1] - coordinate[1]) / tileRenderResolution - ]; - var upscaling = tileGrid.getResolution(tileCoord[0]) / sourceTileResolution; - resolution = tilePixelRatio * upscaling; - } else { - tileSpaceCoordinate = coordinate; - } replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString()); found = found || replayGroup.forEachFeatureAtCoordinate( - tileSpaceCoordinate, resolution, rotation, hitTolerance, {}, + coordinate, resolution, rotation, hitTolerance, {}, /** * @param {ol.Feature|ol.render.Feature} feature Feature. * @return {?} Callback result. @@ -323,43 +296,26 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi * @private */ ol.renderer.canvas.VectorTileLayer.prototype.getReplayTransform_ = function(tile, frameState) { - if (tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS) { - var layer = this.getLayer(); - var source = /** @type {ol.source.VectorTile} */ (layer.getSource()); - var tileGrid = source.getTileGrid(); - var tileCoord = tile.tileCoord; - var tileResolution = - tileGrid.getResolution(tileCoord[0]) / this.getTilePixelRatio_(source, tile); - var viewState = frameState.viewState; - var pixelRatio = frameState.pixelRatio; - var renderResolution = viewState.resolution / pixelRatio; - var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent); - var center = viewState.center; - var origin = ol.extent.getTopLeft(tileExtent); - var size = frameState.size; - var offsetX = Math.round(pixelRatio * size[0] / 2); - var offsetY = Math.round(pixelRatio * size[1] / 2); - return ol.transform.compose(this.tmpTransform_, - offsetX, offsetY, - tileResolution / renderResolution, tileResolution / renderResolution, - viewState.rotation, - (origin[0] - center[0]) / tileResolution, - (center[1] - origin[1]) / tileResolution); - } else { - return this.getTransform(frameState, 0); - } -}; - - -/** - * @private - * @param {ol.source.VectorTile} source Source. - * @param {ol.VectorTile} tile Tile. - * @return {number} The tile's pixel ratio. - */ -ol.renderer.canvas.VectorTileLayer.prototype.getTilePixelRatio_ = function(source, tile) { - return ol.extent.getWidth(tile.getExtent()) / - ol.size.toSize(source.getTileGrid().getTileSize(tile.tileCoord[0]))[0]; + var layer = this.getLayer(); + var source = /** @type {ol.source.VectorTile} */ (layer.getSource()); + var tileGrid = source.getTileGrid(); + var tileCoord = tile.tileCoord; + var tileResolution = tileGrid.getResolution(tileCoord[0]); + var viewState = frameState.viewState; + var pixelRatio = frameState.pixelRatio; + var renderResolution = viewState.resolution / pixelRatio; + var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent); + var center = viewState.center; + var origin = ol.extent.getTopLeft(tileExtent); + var size = frameState.size; + var offsetX = Math.round(pixelRatio * size[0] / 2); + var offsetY = Math.round(pixelRatio * size[1] / 2); + return ol.transform.compose(this.tmpTransform_, + offsetX, offsetY, + tileResolution / renderResolution, tileResolution / renderResolution, + viewState.rotation, + (origin[0] - center[0]) / tileResolution, + (center[1] - origin[1]) / tileResolution); }; @@ -387,7 +343,6 @@ ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, fra var offsetX = Math.round(pixelRatio * size[0] / 2); var offsetY = Math.round(pixelRatio * size[1] / 2); var tiles = this.renderedTiles; - var sourceTileGrid = source.getTileGrid(); var tileGrid = source.getTileGridForProjection(frameState.viewState.projection); var clips = []; var zs = []; @@ -404,15 +359,12 @@ ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, fra if (sourceTile.getState() == ol.TileState.ERROR) { continue; } - var tilePixelRatio = this.getTilePixelRatio_(source, sourceTile); var replayGroup = sourceTile.getReplayGroup(layer, tileCoord.toString()); if (renderMode != ol.layer.VectorTileRenderType.VECTOR && !replayGroup.hasReplays(replays)) { continue; } var currentZ = sourceTile.tileCoord[0]; - var sourceResolution = sourceTileGrid.getResolution(currentZ); - var transform = this.getReplayTransform_(sourceTile, frameState); - ol.transform.translate(transform, worldOffset * tilePixelRatio / sourceResolution, 0); + var transform = this.getTransform(frameState, worldOffset); var currentClip = replayGroup.getClipCoords(transform); context.save(); context.globalAlpha = layerState.opacity; @@ -492,7 +444,6 @@ ol.renderer.canvas.VectorTileLayer.prototype.renderTileImage_ = function( var z = tileCoord[0]; var pixelRatio = frameState.pixelRatio; var source = /** @type {ol.source.VectorTile} */ (layer.getSource()); - var sourceTileGrid = source.getTileGrid(); var tileGrid = source.getTileGridForProjection(frameState.viewState.projection); var resolution = tileGrid.getResolution(z); var context = tile.getContext(layer); @@ -505,22 +456,10 @@ ol.renderer.canvas.VectorTileLayer.prototype.renderTileImage_ = function( if (sourceTile.getState() == ol.TileState.ERROR) { continue; } - var tilePixelRatio = this.getTilePixelRatio_(source, sourceTile); - var sourceTileCoord = sourceTile.tileCoord; var pixelScale = pixelRatio / resolution; var transform = ol.transform.reset(this.tmpTransform_); - if (sourceTile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS) { - var sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord, this.tmpExtent); - var sourceResolution = sourceTileGrid.getResolution(sourceTileCoord[0]); - var renderPixelRatio = pixelRatio / tilePixelRatio * sourceResolution / resolution; - ol.transform.scale(transform, renderPixelRatio, renderPixelRatio); - var offsetX = (sourceTileExtent[0] - tileExtent[0]) / sourceResolution * tilePixelRatio; - var offsetY = (tileExtent[3] - sourceTileExtent[3]) / sourceResolution * tilePixelRatio; - ol.transform.translate(transform, Math.round(offsetX), Math.round(offsetY)); - } else { - ol.transform.scale(transform, pixelScale, -pixelScale); - ol.transform.translate(transform, -tileExtent[0], -tileExtent[3]); - } + ol.transform.scale(transform, pixelScale, -pixelScale); + ol.transform.translate(transform, -tileExtent[0], -tileExtent[3]); var replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString()); replayGroup.replay(context, pixelRatio, transform, 0, {}, replays, true); } diff --git a/src/ol/source/vectortile.js b/src/ol/source/vectortile.js index cfd8eb1079..95f7e770df 100644 --- a/src/ol/source/vectortile.js +++ b/src/ol/source/vectortile.js @@ -4,7 +4,6 @@ goog.require('ol'); goog.require('ol.TileState'); goog.require('ol.VectorImageTile'); goog.require('ol.VectorTile'); -goog.require('ol.proj'); goog.require('ol.size'); goog.require('ol.tilegrid'); goog.require('ol.source.UrlTile'); @@ -86,10 +85,6 @@ ol.source.VectorTile = function(options) { */ this.tileGrids_ = {}; - if (!this.tileGrid) { - this.tileGrid = this.getTileGridForProjection(ol.proj.get(options.projection || 'EPSG:3857')); - } - }; ol.inherits(ol.source.VectorTile, ol.source.UrlTile); diff --git a/test/spec/ol/format/mvt.test.js b/test/spec/ol/format/mvt.test.js index 64dd238d6b..66b4f14797 100644 --- a/test/spec/ol/format/mvt.test.js +++ b/test/spec/ol/format/mvt.test.js @@ -1,11 +1,11 @@ goog.require('ol.Feature'); -goog.require('ol.ext.PBF'); -goog.require('ol.ext.vectortile.VectorTile'); goog.require('ol.extent'); goog.require('ol.format.MVT'); goog.require('ol.geom.Point'); +goog.require('ol.geom.Polygon'); +goog.require('ol.geom.MultiPolygon'); goog.require('ol.render.Feature'); where('ArrayBuffer.isView').describe('ol.format.MVT', function() { @@ -41,36 +41,23 @@ where('ArrayBuffer.isView').describe('ol.format.MVT', function() { featureClass: ol.Feature, layers: ['poi_label'] }); - var pbf = new ol.ext.PBF(data); - var tile = new ol.ext.vectortile.VectorTile(pbf); - var geometry, rawGeometry; + var geometry; - rawGeometry = tile.layers['poi_label'].feature(0).loadGeometry(); - geometry = format.readFeatures(data)[0] - .getGeometry(); + 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]); + expect(geometry.getCoordinates()).to.eql([-1210, 2681]); - rawGeometry = tile.layers['water'].feature(0).loadGeometry(); format.setLayers(['water']); - geometry = format.readFeatures(data)[0] - .getGeometry(); + 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]); + expect(geometry.getCoordinates()[0].length).to.be(10); + expect(geometry.getCoordinates()[0][0]).to.eql([1007, 2302]); - rawGeometry = tile.layers['barrier_line'].feature(0).loadGeometry(); format.setLayers(['barrier_line']); - geometry = format.readFeatures(data)[0] - .getGeometry(); + 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]); + expect(geometry.getCoordinates()[1].length).to.be(6); + expect(geometry.getCoordinates()[1][0]).to.eql([4160, 3489]); }); it('parses id property', function() { @@ -102,7 +89,7 @@ where('ArrayBuffer.isView').describe('ol.format.MVT', function() { describe('ol.format.MVT', function() { - describe('#readFeature_', function() { + describe('#createFeature_', function() { it('accepts a geometryName', function() { var format = new ol.format.MVT({ featureClass: ol.Feature, @@ -114,16 +101,99 @@ describe('ol.format.MVT', function() { geometry: 'foo' }, type: 1, - loadGeometry: function() { - return [[0, 0]]; + layer: { + name: 'layer1' } }; - var feature = format.readFeature_(rawFeature, 'mapbox'); + var readRawGeometry_ = ol.format.MVT.readRawGeometry_; + ol.format.MVT.readRawGeometry_ = function({}, rawFeature, flatCoordinates, ends) { + flatCoordinates.push(0, 0); + ends.push(2); + }; + var feature = format.createFeature_({}, rawFeature); + ol.format.MVT.readRawGeometry_ = readRawGeometry_; var geometry = feature.getGeometry(); expect(geometry).to.be.a(ol.geom.Point); expect(feature.get('myGeom')).to.equal(geometry); expect(feature.get('geometry')).to.be('foo'); }); + + it('detects a Polygon', function() { + var format = new ol.format.MVT({ + featureClass: ol.Feature + }); + var rawFeature = { + type: 3, + properties: {}, + layer: { + name: 'layer1' + } + }; + var readRawGeometry_ = ol.format.MVT.readRawGeometry_; + ol.format.MVT.readRawGeometry_ = function({}, rawFeature, flatCoordinates, ends) { + flatCoordinates.push(0, 0, 3, 0, 3, 3, 3, 0, 0, 0); + flatCoordinates.push(1, 1, 1, 2, 2, 2, 2, 1, 1, 1); + ends.push(10, 20); + }; + var feature = format.createFeature_({}, rawFeature); + ol.format.MVT.readRawGeometry_ = readRawGeometry_; + var geometry = feature.getGeometry(); + expect(geometry).to.be.a(ol.geom.Polygon); + }); + + it('detects a MultiPolygon', function() { + var format = new ol.format.MVT({ + featureClass: ol.Feature + }); + var rawFeature = { + type: 3, + properties: {}, + layer: { + name: 'layer1' + } + }; + var readRawGeometry_ = ol.format.MVT.readRawGeometry_; + ol.format.MVT.readRawGeometry_ = function({}, rawFeature, flatCoordinates, ends) { + flatCoordinates.push(0, 0, 1, 0, 1, 1, 1, 0, 0, 0); + flatCoordinates.push(1, 1, 2, 1, 2, 2, 2, 1, 1, 1); + ends.push(10, 20); + }; + var feature = format.createFeature_({}, rawFeature); + ol.format.MVT.readRawGeometry_ = readRawGeometry_; + var geometry = feature.getGeometry(); + expect(geometry).to.be.a(ol.geom.MultiPolygon); + }); + + it('creates ol.render.Feature instances', function() { + var format = new ol.format.MVT(); + var rawFeature = { + type: 3, + properties: { + foo: 'bar' + }, + layer: { + name: 'layer1' + } + }; + var readRawGeometry_ = ol.format.MVT.readRawGeometry_; + var createdFlatCoordinates; + var createdEnds; + ol.format.MVT.readRawGeometry_ = function({}, rawFeature, flatCoordinates, ends) { + flatCoordinates.push(0, 0, 1, 0, 1, 1, 1, 0, 0, 0); + flatCoordinates.push(1, 1, 2, 1, 2, 2, 2, 1, 1, 1); + createdFlatCoordinates = flatCoordinates; + ends.push(10, 20); + createdEnds = ends; + }; + var feature = format.createFeature_({}, rawFeature); + ol.format.MVT.readRawGeometry_ = readRawGeometry_; + expect(feature).to.be.a(ol.render.Feature); + expect(feature.getType()).to.be('Polygon'); + expect(feature.getFlatCoordinates()).to.equal(createdFlatCoordinates); + expect(feature.getEnds()).to.equal(createdEnds); + expect(feature.get('foo')).to.be('bar'); + }); + }); }); diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index 8e72d52c27..5c5fa6b589 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -13,6 +13,7 @@ goog.require('ol.geom.Point'); goog.require('ol.layer.VectorTile'); goog.require('ol.proj'); goog.require('ol.proj.Projection'); +goog.require('ol.render.Feature'); goog.require('ol.renderer.canvas.VectorTileLayer'); goog.require('ol.source.VectorTile'); goog.require('ol.style.Style'); @@ -24,7 +25,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { describe('constructor', function() { - var map, layer, source, feature1, feature2, target, tileCallback; + var map, layer, source, feature1, feature2, feature3, target, tileCallback; beforeEach(function() { tileCallback = function() {}; @@ -51,11 +52,12 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { })]; feature1 = new ol.Feature(new ol.geom.Point([1, -1])); feature2 = new ol.Feature(new ol.geom.Point([0, 0])); + feature3 = new ol.render.Feature('Point', [1, -1], []); feature2.setStyle(featureStyle); var TileClass = function() { ol.VectorTile.apply(this, arguments); this.setState('loaded'); - this.setFeatures([feature1, feature2]); + this.setFeatures([feature1, feature2, feature3]); this.setProjection(ol.proj.get('EPSG:4326')); tileCallback(this); }; @@ -106,7 +108,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { it('does not render replays for pure image rendering', function() { layer.renderMode_ = 'image'; var spy = sinon.spy(ol.renderer.canvas.VectorTileLayer.prototype, - 'getReplayTransform_'); + 'getTransform'); map.renderSync(); expect(spy.callCount).to.be(0); spy.restore(); @@ -114,7 +116,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { it('renders both replays and images for hybrid rendering', function() { var spy1 = sinon.spy(ol.renderer.canvas.VectorTileLayer.prototype, - 'getReplayTransform_'); + 'getTransform'); var spy2 = sinon.spy(ol.renderer.canvas.VectorTileLayer.prototype, 'renderTileImage_'); map.renderSync(); @@ -130,7 +132,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { renderer: function() {} })); var spy = sinon.spy(ol.renderer.canvas.VectorTileLayer.prototype, - 'getReplayTransform_'); + 'getTransform'); map.renderSync(); expect(spy.callCount).to.be(1); spy.restore(); @@ -142,6 +144,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { map.renderSync(); expect(spy.getCall(0).args[2]).to.be(layer.getStyle()); expect(spy.getCall(1).args[2]).to.be(feature2.getStyle()); + spy.restore(); }); it('transforms geometries when tile and view projection are different', function() { @@ -155,16 +158,17 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { ol.proj.fromLonLat([1, -1])); }); - it('leaves geometries untouched when units are tile-pixels', function() { - var proj = new ol.proj.Projection({code: '', units: 'tile-pixels'}); + it('Geometries are transformed from tile-pixels', function() { + var proj = new ol.proj.Projection({code: 'EPSG:3857', units: 'tile-pixels'}); var tile; tileCallback = function(t) { t.setProjection(proj); tile = t; }; map.renderSync(); - expect(tile.getProjection()).to.equal(proj); - expect(feature1.getGeometry().getCoordinates()).to.eql([1, -1]); + expect(tile.getProjection()).to.equal(ol.proj.get('EPSG:3857')); + expect(feature1.getGeometry().getCoordinates()).to.eql([-20027724.40316874, 20047292.282409746]); + expect(feature3.flatCoordinates_).to.eql([-20027724.40316874, 20047292.282409746]); }); it('works for multiple layers that use the same source', function() { @@ -189,14 +193,6 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { spy2.restore(); }); - it('uses the extent of the source tile', function() { - var renderer = map.getRenderer().getLayerRenderer(layer); - var tile = new ol.VectorTile([0, 0, 0], 2); - tile.setExtent([0, 0, 4096, 4096]); - var tilePixelRatio = renderer.getTilePixelRatio_(source, tile); - expect(tilePixelRatio).to.be(16); - }); - }); describe('#prepareFrame', function() {