diff --git a/examples/osm-vector-tiles.html b/examples/osm-vector-tiles.html index 4abfab50ab..37085e4bf0 100644 --- a/examples/osm-vector-tiles.html +++ b/examples/osm-vector-tiles.html @@ -3,7 +3,7 @@ layout: example.html title: OSM Vector Tiles shortdesc: Using OpenStreetMap vector tiles. docs: > - A simple vector tiles map with Mapzen vector tiles. **Note**: TopoJSON vector tiles are not optimized for rendering - they might clip geometries exactly at the tile boundary instead of adding a buffer, and use geographic coordinates instead of tile relative pixel coordinates in view projection. + A simple vector tiles map with Mapzen vector tiles. This example uses the TopoJSON format's `layerName` option to determine the layer ("water", "roads", "buildings") for styling. **Note**: [`ol.format.MVT`](../apidoc/ol.format.MVT.html) is an even more efficient format for vector tiles. tags: "vector, tiles, osm, mapzen" cloak: vector-tiles-5eJz6JX: Your Mapzen API key from https://mapzen.com/developers diff --git a/examples/osm-vector-tiles.js b/examples/osm-vector-tiles.js index c2fa8a35d2..6df8ba0460 100644 --- a/examples/osm-vector-tiles.js +++ b/examples/osm-vector-tiles.js @@ -1,4 +1,3 @@ -goog.require('ol.Attribution'); goog.require('ol.Map'); goog.require('ol.View'); goog.require('ol.format.TopoJSON'); @@ -13,11 +12,6 @@ goog.require('ol.tilegrid'); var key = 'vector-tiles-5eJz6JX'; -var attribution = [new ol.Attribution({ - html: '© OpenStreetMap contributors, Who’s On First, Natural Earth, and openstreetmapdata.com' -})]; -var format = new ol.format.TopoJSON(); -var tileGrid = ol.tilegrid.createXYZ({maxZoom: 19}); var roadStyleCache = {}; var roadColor = { 'major_road': '#776', @@ -34,65 +28,58 @@ var buildingStyle = new ol.style.Style({ width: 1 }) }); +var waterStyle = new ol.style.Style({ + fill: new ol.style.Fill({ + color: '#9db9e8' + }) +}); +var roadStyle = function(feature) { + var kind = feature.get('kind'); + var railway = feature.get('railway'); + var sort_key = feature.get('sort_key'); + var styleKey = kind + '/' + railway + '/' + sort_key; + var style = roadStyleCache[styleKey]; + if (!style) { + var color, width; + if (railway) { + color = '#7de'; + width = 1; + } else { + color = roadColor[kind]; + width = kind == 'highway' ? 1.5 : 1; + } + style = new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: color, + width: width + }), + zIndex: sort_key + }); + roadStyleCache[styleKey] = style; + } + return style; +}; var map = new ol.Map({ layers: [ new ol.layer.VectorTile({ source: new ol.source.VectorTile({ - attributions: attribution, - format: format, - tileGrid: tileGrid, - url: 'https://tile.mapzen.com/mapzen/vector/v1/water/{z}/{x}/{y}.topojson?api_key=' + key + attributions: '© OpenStreetMap contributors, Who’s On First, ' + + 'Natural Earth, and openstreetmapdata.com', + format: new ol.format.TopoJSON({ + layerName: 'layer', + layers: ['water', 'roads', 'buildings'] + }), + tileGrid: ol.tilegrid.createXYZ({maxZoom: 19}), + url: 'https://tile.mapzen.com/mapzen/vector/v1/all/{z}/{x}/{y}.topojson?api_key=' + key }), - style: new ol.style.Style({ - fill: new ol.style.Fill({ - color: '#9db9e8' - }) - }) - }), - new ol.layer.VectorTile({ - source: new ol.source.VectorTile({ - attributions: attribution, - format: format, - tileGrid: tileGrid, - url: 'https://tile.mapzen.com/mapzen/vector/v1/roads/{z}/{x}/{y}.topojson?api_key=' + key - }), - style: function(feature) { - var kind = feature.get('kind'); - var railway = feature.get('railway'); - var sort_key = feature.get('sort_key'); - var styleKey = kind + '/' + railway + '/' + sort_key; - var style = roadStyleCache[styleKey]; - if (!style) { - var color, width; - if (railway) { - color = '#7de'; - width = 1; - } else { - color = roadColor[kind]; - width = kind == 'highway' ? 1.5 : 1; - } - style = new ol.style.Style({ - stroke: new ol.style.Stroke({ - color: color, - width: width - }), - zIndex: sort_key - }); - roadStyleCache[styleKey] = style; + style: function(feature, resolution) { + switch (feature.get('layer')) { + case 'water': return waterStyle; + case 'roads': return roadStyle(feature); + case 'buildings': return (resolution < 10) ? buildingStyle : null; + default: return null; } - return style; - } - }), - new ol.layer.VectorTile({ - source: new ol.source.VectorTile({ - attributions: attribution, - format: format, - tileGrid: tileGrid, - url: 'https://tile.mapzen.com/mapzen/vector/v1/buildings/{z}/{x}/{y}.topojson?api_key=' + key - }), - style: function(f, resolution) { - return (resolution < 10) ? buildingStyle : null; } }) ], diff --git a/examples/topojson.js b/examples/topojson.js index 427da9db83..9a61751023 100644 --- a/examples/topojson.js +++ b/examples/topojson.js @@ -29,13 +29,14 @@ var style = new ol.style.Style({ var vector = new ol.layer.Vector({ source: new ol.source.Vector({ url: 'data/topojson/world-110m.json', - format: new ol.format.TopoJSON(), + format: new ol.format.TopoJSON({ + // don't want to render the full world polygon (stored as 'land' layer), + // which repeats all countries + layers: ['countries'] + }), overlaps: false }), - style: function(feature) { - // don't want to render the full world polygon, which repeats all countries - return feature.getId() !== undefined ? style : null; - } + style: style }); var map = new ol.Map({ diff --git a/externs/olx.js b/externs/olx.js index 934db39a05..e7fefe5804 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -2040,7 +2040,11 @@ olx.format.PolylineOptions.prototype.geometryLayout; /** - * @typedef {{defaultDataProjection: ol.ProjectionLike}} + * @typedef {{ + * defaultDataProjection: ol.ProjectionLike, + * layerName: (string|undefined), + * layers: (Array.|undefined) + * }} */ olx.format.TopoJSONOptions; @@ -2053,6 +2057,38 @@ olx.format.TopoJSONOptions; olx.format.TopoJSONOptions.prototype.defaultDataProjection; +/** + * Set the name of the TopoJSON topology `objects`'s children as feature + * property with the specified name. This means that when set to `'layer'`, a + * topology like + * ``` + * { + * "type": "Topology", + * "objects": { + * "example": { + * "type": "GeometryCollection", + * "geometries": [] + * } + * } + * } + * ``` + * will result in features that have a property `'layer'` set to `'example'`. + * When not set, no property will be added to features. + * @type {string|undefined} + * @api + */ +olx.format.TopoJSONOptions.prototype.layerName; + + +/** + * Names of the TopoJSON topology's `objects`'s children to read features from. + * If not provided, features will be read from all children. + * @type {Array.|undefined} + * @api + */ +olx.format.TopoJSONOptions.prototype.layers; + + /** * @typedef {{altitudeMode: (ol.format.IGCZ|undefined)}} */ diff --git a/src/ol/format/topojson.js b/src/ol/format/topojson.js index 8e9f4fb1f1..5355c749b2 100644 --- a/src/ol/format/topojson.js +++ b/src/ol/format/topojson.js @@ -10,7 +10,6 @@ goog.require('ol.geom.MultiPoint'); goog.require('ol.geom.MultiPolygon'); goog.require('ol.geom.Point'); goog.require('ol.geom.Polygon'); -goog.require('ol.obj'); goog.require('ol.proj'); @@ -29,6 +28,18 @@ ol.format.TopoJSON = function(opt_options) { ol.format.JSONFeature.call(this); + /** + * @private + * @type {string|undefined} + */ + this.layerName_ = options.layerName; + + /** + * @private + * @type {Array.} + */ + this.layers_ = options.layers ? options.layers : null; + /** * @inheritDoc */ @@ -202,18 +213,21 @@ ol.format.TopoJSON.readMultiPolygonGeometry_ = function(object, arcs) { * @param {Array.>} arcs Array of arcs. * @param {Array.} scale Scale for each dimension. * @param {Array.} translate Translation for each dimension. + * @param {string|undefined} property Property to set the `GeometryCollection`'s parent + * object to. + * @param {string} name Name of the `Topology`'s child object. * @param {olx.format.ReadOptions=} opt_options Read options. * @return {Array.} Array of features. * @private */ ol.format.TopoJSON.readFeaturesFromGeometryCollection_ = function( - collection, arcs, scale, translate, opt_options) { + collection, arcs, scale, translate, property, name, opt_options) { var geometries = collection.geometries; var features = []; var i, ii; for (i = 0, ii = geometries.length; i < ii; ++i) { features[i] = ol.format.TopoJSON.readFeatureFromGeometry_( - geometries[i], arcs, scale, translate, opt_options); + geometries[i], arcs, scale, translate, property, name, opt_options); } return features; }; @@ -226,12 +240,15 @@ ol.format.TopoJSON.readFeaturesFromGeometryCollection_ = function( * @param {Array.>} arcs Array of arcs. * @param {Array.} scale Scale for each dimension. * @param {Array.} translate Translation for each dimension. + * @param {string|undefined} property Property to set the `GeometryCollection`'s parent + * object to. + * @param {string} name Name of the `Topology`'s child object. * @param {olx.format.ReadOptions=} opt_options Read options. * @return {ol.Feature} Feature. * @private */ ol.format.TopoJSON.readFeatureFromGeometry_ = function(object, arcs, - scale, translate, opt_options) { + scale, translate, property, name, opt_options) { var geometry; var type = object.type; var geometryReader = ol.format.TopoJSON.GEOMETRY_READERS_[type]; @@ -246,8 +263,15 @@ ol.format.TopoJSON.readFeatureFromGeometry_ = function(object, arcs, if (object.id !== undefined) { feature.setId(object.id); } - if (object.properties) { - feature.setProperties(object.properties); + var properties = object.properties; + if (property) { + if (!properties) { + properties = {}; + } + properties[property] = name; + } + if (properties) { + feature.setProperties(properties); } return feature; }; @@ -283,21 +307,24 @@ ol.format.TopoJSON.prototype.readFeaturesFromObject = function( } /** @type {Array.} */ var features = []; - var topoJSONFeatures = ol.obj.getValues(topoJSONTopology.objects); - var i, ii; - var feature; - for (i = 0, ii = topoJSONFeatures.length; i < ii; ++i) { - if (topoJSONFeatures[i].type === 'GeometryCollection') { + var topoJSONFeatures = topoJSONTopology.objects; + var property = this.layerName_; + var objectName, feature; + for (objectName in topoJSONFeatures) { + if (this.layers_ && this.layers_.indexOf(objectName) == -1) { + continue; + } + if (topoJSONFeatures[objectName].type === 'GeometryCollection') { feature = /** @type {TopoJSONGeometryCollection} */ - (topoJSONFeatures[i]); + (topoJSONFeatures[objectName]); features.push.apply(features, ol.format.TopoJSON.readFeaturesFromGeometryCollection_( - feature, arcs, scale, translate, opt_options)); + feature, arcs, scale, translate, property, objectName, opt_options)); } else { feature = /** @type {TopoJSONGeometry} */ - (topoJSONFeatures[i]); + (topoJSONFeatures[objectName]); features.push(ol.format.TopoJSON.readFeatureFromGeometry_( - feature, arcs, scale, translate, opt_options)); + feature, arcs, scale, translate, property, objectName, opt_options)); } } return features; diff --git a/test/spec/ol/format/topojson.test.js b/test/spec/ol/format/topojson.test.js index 4874c7a894..a89980c683 100644 --- a/test/spec/ol/format/topojson.test.js +++ b/test/spec/ol/format/topojson.test.js @@ -176,6 +176,29 @@ describe('ol.format.TopoJSON', function() { }); }); + it('sets the topology\'s child names as feature property', function(done) { + afterLoadText('spec/ol/format/topojson/world-110m.json', function(text) { + var format = new ol.format.TopoJSON({ + layerName: 'layer' + }); + var features = format.readFeatures(text); + expect(features[0].get('layer')).to.be('land'); + expect(features[177].get('layer')).to.be('countries'); + done(); + }); + }); + + it('only parses features from specified topology\'s children', function(done) { + afterLoadText('spec/ol/format/topojson/world-110m.json', function(text) { + var format = new ol.format.TopoJSON({ + layers: ['land'] + }); + var features = format.readFeatures(text); + expect(features.length).to.be(1); + done(); + }); + }); + }); });