From a940cdd6a777dce20f1e0ba448c908b08e8a08c0 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 4 Dec 2015 16:46:44 +0100 Subject: [PATCH] Add support for OSM vector tiles --- examples/osm-vector-tiles.css | 3 + examples/osm-vector-tiles.html | 9 ++ examples/osm-vector-tiles.js | 142 ++++++++++++++++++ src/ol/featureloader.js | 24 ++- src/ol/proj/proj.js | 8 +- src/ol/source/vectortilesource.js | 5 +- src/ol/vectortile.js | 13 +- test/spec/ol/featureloader.test.js | 40 +++++ test/spec/ol/proj/proj.test.js | 1 + .../canvasvectortilelayerrenderer.test.js | 1 - 10 files changed, 229 insertions(+), 17 deletions(-) create mode 100644 examples/osm-vector-tiles.css create mode 100644 examples/osm-vector-tiles.html create mode 100644 examples/osm-vector-tiles.js diff --git a/examples/osm-vector-tiles.css b/examples/osm-vector-tiles.css new file mode 100644 index 0000000000..33e90f7301 --- /dev/null +++ b/examples/osm-vector-tiles.css @@ -0,0 +1,3 @@ +.map { + background: #f8f4f0; +} diff --git a/examples/osm-vector-tiles.html b/examples/osm-vector-tiles.html new file mode 100644 index 0000000000..0c565c5a9e --- /dev/null +++ b/examples/osm-vector-tiles.html @@ -0,0 +1,9 @@ +--- +layout: example.html +title: OSM Vector Tiles +shortdesc: Using OpenStreetMap vector tiles. +docs: > + A simple vector tiles map with OpenStreetMap vector tiles. **Note**: The tiles used in this example are not optimized for rendering - they clip tiles exactly at the tile boundary instead of adding a buffer, and use geographic coordinates instead of tile relative pixel coordinates in view projection. +tags: "vector, tiles, osm" +--- +
diff --git a/examples/osm-vector-tiles.js b/examples/osm-vector-tiles.js new file mode 100644 index 0000000000..e1d2fda37b --- /dev/null +++ b/examples/osm-vector-tiles.js @@ -0,0 +1,142 @@ +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.format.TopoJSON'); +goog.require('ol.layer.VectorTile'); +goog.require('ol.proj'); +goog.require('ol.source.VectorTile'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); + + +var format = new ol.format.TopoJSON(); +var tileGrid = ol.tilegrid.createXYZ({ maxZoom: 19 }); +var roadStyleCache = {}; +var roadColor = { + 'major_road': '#776', + 'minor_road': '#ccb', + 'highway': '#f39' +}; +var landuseStyleCache = {}; +var buildingStyle = [ + new ol.style.Style({ + fill: new ol.style.Fill({ + color: '#666', + opacity: 0.4 + }), + stroke: new ol.style.Stroke({ + color: '#444', + width: 1 + }) + }) +]; + +var map = new ol.Map({ + layers: [ + new ol.layer.VectorTile({ + source: new ol.source.VectorTile({ + format: format, + tileGrid: tileGrid, + url: 'http://{a-c}.tile.openstreetmap.us/' + + 'vectiles-water-areas/{z}/{x}/{y}.topojson' + }), + style: new ol.style.Style({ + fill: new ol.style.Fill({ + color: '#9db9e8' + }) + }) + }), + new ol.layer.VectorTile({ + source: new ol.source.VectorTile({ + format: format, + tileGrid: tileGrid, + url: 'http://{a-c}.tile.openstreetmap.us/' + + 'vectiles-highroad/{z}/{x}/{y}.topojson' + }), + style: function(feature, resolution) { + 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; + } + }), + new ol.layer.VectorTile({ + source: new ol.source.VectorTile({ + format: format, + tileGrid: tileGrid, + url: 'http://{a-c}.tile.openstreetmap.us/' + + 'vectiles-buildings/{z}/{x}/{y}.topojson' + }), + style: function(f, resolution) { + return (resolution < 10) ? buildingStyle : null; + } + }), + new ol.layer.VectorTile({ + source: new ol.source.VectorTile({ + format: format, + tileGrid: tileGrid, + url: 'http://{a-c}.tile.openstreetmap.us/' + + 'vectiles-land-usages/{z}/{x}/{y}.topojson' + }), + visible: false, + style: function(feature, resolution) { + var kind = feature.get('kind'); + var styleKey = kind; + var style = landuseStyleCache[styleKey]; + if (!style) { + var color, width; + color = { + 'parking': '#ddd', + 'industrial': '#aaa', + 'urban area': '#aaa', + 'park': '#76C759', + 'school': '#DA10E7', + 'garden': '#76C759', + 'pitch': '#D58F8D', + 'scrub': '#3E7D28', + 'residential': '#4C9ED9' + }[kind]; + width = kind == 'highway' ? 1.5 : 1; + style = new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: color, + width: width + }), + fill: new ol.style.Fill({ + color: color, + opacity: 0.5 + }) + }); + landuseStyleCache[styleKey] = style; + } + return style; + } + }) + ], + target: 'map', + view: new ol.View({ + center: ol.proj.fromLonLat([-74.0064, 40.7142]), + maxZoom: 19, + zoom: 15 + }) +}); diff --git a/src/ol/featureloader.js b/src/ol/featureloader.js index f3659a8dc7..2c64633cea 100644 --- a/src/ol/featureloader.js +++ b/src/ol/featureloader.js @@ -9,6 +9,9 @@ goog.require('goog.net.XhrIo'); goog.require('goog.net.XhrIo.ResponseType'); goog.require('ol.TileState'); goog.require('ol.format.FormatType'); +goog.require('ol.proj'); +goog.require('ol.proj.Projection'); +goog.require('ol.proj.Units'); goog.require('ol.xml'); @@ -100,12 +103,21 @@ ol.featureloader.loadFeaturesXhr = function(url, format, success, failure) { goog.asserts.fail('unexpected format type'); } if (source) { - var features = format.readFeatures(source, - {featureProjection: projection}); if (ol.ENABLE_VECTOR_TILE && success.length == 2) { - success.call(this, features, format.readProjection(source)); + var dataProjection = format.readProjection(source); + var units = dataProjection.getUnits(); + if (units === ol.proj.Units.TILE_PIXELS) { + projection = new ol.proj.Projection({ + code: projection.getCode(), + units: units + }); + this.setProjection(projection); + } + success.call(this, format.readFeatures(source, + {featureProjection: projection}), dataProjection); } else { - success.call(this, features); + success.call(this, format.readFeatures(source, + {featureProjection: projection})); } } else { goog.asserts.fail('undefined or null source'); @@ -142,7 +154,9 @@ ol.featureloader.tile = function(url, format) { * @this {ol.VectorTile} */ function(features, projection) { - this.setProjection(projection); + if (ol.proj.equivalent(projection, this.getProjection())) { + this.setProjection(projection); + } this.setFeatures(features); }, /** diff --git a/src/ol/proj/proj.js b/src/ol/proj/proj.js index 4501e60c14..852ce7639e 100644 --- a/src/ol/proj/proj.js +++ b/src/ol/proj/proj.js @@ -677,12 +677,14 @@ ol.proj.get = function(projectionLike) { ol.proj.equivalent = function(projection1, projection2) { if (projection1 === projection2) { return true; - } else if (projection1.getCode() === projection2.getCode()) { - return projection1.getUnits() === projection2.getUnits(); + } + var equalUnits = projection1.getUnits() === projection2.getUnits(); + if (projection1.getCode() === projection2.getCode()) { + return equalUnits; } else { var transformFn = ol.proj.getTransformFromProjections( projection1, projection2); - return transformFn === ol.proj.cloneTransform; + return transformFn === ol.proj.cloneTransform && equalUnits; } }; diff --git a/src/ol/source/vectortilesource.js b/src/ol/source/vectortilesource.js index 5fb8ae4435..f482e64ef7 100644 --- a/src/ol/source/vectortilesource.js +++ b/src/ol/source/vectortilesource.js @@ -56,7 +56,7 @@ ol.source.VectorTile = function(options) { /** * @protected * @type {function(new: ol.VectorTile, ol.TileCoord, ol.TileState, string, - * ol.format.Feature, ol.TileLoadFunctionType)} + * ol.format.Feature, ol.TileLoadFunctionType, ol.proj.Projection)} */ this.tileClass = options.tileClass ? options.tileClass : ol.VectorTile; @@ -83,8 +83,7 @@ ol.source.VectorTile.prototype.getTile = tileCoord, tileUrl !== undefined ? ol.TileState.IDLE : ol.TileState.EMPTY, tileUrl !== undefined ? tileUrl : '', - this.format_, - this.tileLoadFunction); + this.format_, this.tileLoadFunction, projection); goog.events.listen(tile, goog.events.EventType.CHANGE, this.handleTileChange, false, this); diff --git a/src/ol/vectortile.js b/src/ol/vectortile.js index a533a1c92a..5a5e1efe2c 100644 --- a/src/ol/vectortile.js +++ b/src/ol/vectortile.js @@ -4,6 +4,7 @@ goog.require('ol.Tile'); goog.require('ol.TileCoord'); goog.require('ol.TileLoadFunctionType'); goog.require('ol.TileState'); +goog.require('ol.proj.Projection'); /** @@ -25,8 +26,10 @@ ol.TileReplayState; * @param {string} src Data source url. * @param {ol.format.Feature} format Feature format. * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function. + * @param {ol.proj.Projection} projection Feature projection. */ -ol.VectorTile = function(tileCoord, state, src, format, tileLoadFunction) { +ol.VectorTile = + function(tileCoord, state, src, format, tileLoadFunction, projection) { goog.base(this, tileCoord, state); @@ -52,7 +55,7 @@ ol.VectorTile = function(tileCoord, state, src, format, tileLoadFunction) { * @private * @type {ol.proj.Projection} */ - this.projection_ = null; + this.projection_ = projection; /** * @private @@ -124,7 +127,7 @@ ol.VectorTile.prototype.getKey = function() { /** - * @return {ol.proj.Projection} Projection. + * @return {ol.proj.Projection} Feature projection. */ ol.VectorTile.prototype.getProjection = function() { return this.projection_; @@ -138,7 +141,7 @@ ol.VectorTile.prototype.load = function() { if (this.state == ol.TileState.IDLE) { this.setState(ol.TileState.LOADING); this.tileLoadFunction_(this, this.url_); - this.loader_(null, NaN, null); + this.loader_(null, NaN, this.projection_); } }; @@ -153,7 +156,7 @@ ol.VectorTile.prototype.setFeatures = function(features) { /** - * @param {ol.proj.Projection} projection Projection. + * @param {ol.proj.Projection} projection Feature projection. */ ol.VectorTile.prototype.setProjection = function(projection) { this.projection_ = projection; diff --git a/test/spec/ol/featureloader.test.js b/test/spec/ol/featureloader.test.js index f9be87a4dc..3d634b540f 100644 --- a/test/spec/ol/featureloader.test.js +++ b/test/spec/ol/featureloader.test.js @@ -1,6 +1,7 @@ goog.provide('ol.test.featureloader'); describe('ol.featureloader', function() { + describe('ol.featureloader.xhr', function() { var loader; var source; @@ -53,9 +54,48 @@ describe('ol.featureloader', function() { }); }); + + describe('ol.featureloader.tile', function() { + var loader; + var tile; + + beforeEach(function() { + tile = new ol.VectorTile([0, 0, 0], undefined, undefined, undefined, + undefined, ol.proj.get('EPSG:3857')); + }); + + it('sets features on the tile', function(done) { + var url = 'spec/ol/data/point.json'; + var format = new ol.format.GeoJSON(); + loader = ol.featureloader.tile(url, format); + goog.events.listen(tile, 'change', function(e) { + expect(tile.getFeatures().length).to.be.greaterThan(0); + done(); + }); + loader.call(tile, [], 1, ol.proj.get('EPSG:3857')); + }); + + it('sets features on the tile and updates proj units', function(done) { + var url = 'spec/ol/data/14-8938-5680.vector.pbf'; + var format = new ol.format.MVT(); + loader = ol.featureloader.tile(url, format); + goog.events.listen(tile, 'change', function(e) { + expect(tile.getFeatures().length).to.be.greaterThan(0); + expect(tile.getProjection().getUnits()).to.be('tile-pixels'); + done(); + }); + loader.call(tile, [], 1, ol.proj.get('EPSG:3857')); + }); + + }); + }); +goog.require('goog.events'); +goog.require('ol.VectorTile'); goog.require('ol.featureloader'); goog.require('ol.format.GeoJSON'); +goog.require('ol.format.MVT'); +goog.require('ol.proj'); goog.require('ol.source.Vector'); goog.require('ol.source.VectorEventType'); diff --git a/test/spec/ol/proj/proj.test.js b/test/spec/ol/proj/proj.test.js index d792b4b07a..672625040c 100644 --- a/test/spec/ol/proj/proj.test.js +++ b/test/spec/ol/proj/proj.test.js @@ -69,6 +69,7 @@ describe('ol.proj', function() { }); expect(ol.proj.equivalent(proj1, proj2)).to.not.be.ok(); }); + }); describe('identify transform', function() { diff --git a/test/spec/ol/renderer/canvas/canvasvectortilelayerrenderer.test.js b/test/spec/ol/renderer/canvas/canvasvectortilelayerrenderer.test.js index da228a61c0..7fb60ef0c8 100644 --- a/test/spec/ol/renderer/canvas/canvasvectortilelayerrenderer.test.js +++ b/test/spec/ol/renderer/canvas/canvasvectortilelayerrenderer.test.js @@ -41,7 +41,6 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { ol.VectorTile.apply(this, arguments); this.setState('loaded'); this.setFeatures([feature1, feature2]); - this.setProjection(ol.proj.get('EPSG:3857')); }; ol.inherits(TileClass, ol.VectorTile); var source = new ol.source.VectorTile({