diff --git a/examples/mapbox-vector-tiles-simple.css b/examples/mapbox-vector-tiles-simple.css new file mode 100644 index 0000000000..33e90f7301 --- /dev/null +++ b/examples/mapbox-vector-tiles-simple.css @@ -0,0 +1,3 @@ +.map { + background: #f8f4f0; +} diff --git a/examples/mapbox-vector-tiles-simple.html b/examples/mapbox-vector-tiles-simple.html new file mode 100644 index 0000000000..36da8db103 --- /dev/null +++ b/examples/mapbox-vector-tiles-simple.html @@ -0,0 +1,15 @@ +--- +template: example.html +title: Simple Mapbox vector tiles example +shortdesc: Example of a simple Mapbox vector tiles map. +docs: > + A simple vector tiles map. **Note**: Make sure to get your own Mapbox API key when using this example. No map will be visible when the API key has expired. +tags: "simple, mapbox, vector, tiles" +resources: + - resources/mapbox-streets-v6-style.js +--- +
+
+
+
+
diff --git a/examples/mapbox-vector-tiles-simple.js b/examples/mapbox-vector-tiles-simple.js new file mode 100644 index 0000000000..3d68525c4d --- /dev/null +++ b/examples/mapbox-vector-tiles-simple.js @@ -0,0 +1,44 @@ +goog.require('ol.Attribution'); +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.format.MVT'); +goog.require('ol.layer.VectorTile'); +goog.require('ol.source.VectorTile'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Icon'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); +goog.require('ol.style.Text'); + + +// Mapbox access token - request your own at http://mapbox.com +var accessToken = + 'pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiRk1kMWZaSSJ9.E5BkluenyWQMsBLsuByrmg'; + +var map = new ol.Map({ + layers: [ + new ol.layer.VectorTile({ + source: new ol.source.VectorTile({ + attributions: [new ol.Attribution({ + html: '© Mapbox ' + + '© ' + + 'OpenStreetMap contributors' + })], + format: new ol.format.MVT(), + tileGrid: ol.tilegrid.createXYZ({maxZoom: 22}), + tilePixelRatio: 16, + url: 'http://{a-d}.tiles.mapbox.com/v4/mapbox.mapbox-streets-v6/' + + '{z}/{x}/{y}.vector.pbf?access_token=' + accessToken + }), + style: createMapboxStreetsV6Style() + }) + ], + target: 'map', + view: new ol.View({ + center: [0, 0], + zoom: 2 + }) +}); + +// ol.style.Fill, ol.style.Icon, ol.style.Stroke, ol.style.Style and +// ol.style.Text are required for createMapboxStreetsV6Style() diff --git a/examples/mapbox-vector-tiles.html b/examples/mapbox-vector-tiles.html index ec0744b5f7..c7cb0b45d3 100644 --- a/examples/mapbox-vector-tiles.html +++ b/examples/mapbox-vector-tiles.html @@ -3,7 +3,7 @@ template: example.html title: Mapbox vector tiles example shortdesc: Example of a Mapbox vector tiles map. docs: > - A simple vector tiles map. + A vector tiles map which reuses the same tiles for subsequent zoom levels to save bandwith on mobile devices. **Note**: Make sure to get your own Mapbox API key when using this example. No map will be visible when the API key has expired. tags: "simple, mapbox, vector, tiles" resources: - resources/mapbox-streets-v6-style.js diff --git a/examples/mapbox-vector-tiles.js b/examples/mapbox-vector-tiles.js index ad1a5d8816..4506437370 100644 --- a/examples/mapbox-vector-tiles.js +++ b/examples/mapbox-vector-tiles.js @@ -13,21 +13,36 @@ goog.require('ol.style.Text'); goog.require('ol.tilegrid.TileGrid'); -// Mapbox access token - request your own at http://mabobox.com +// Mapbox access token - request your own at http://mapbox.com var accessToken = 'pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiRk1kMWZaSSJ9.E5BkluenyWQMsBLsuByrmg'; -// For how many zoom levels do we want to use the same vector tile? +// For how many zoom levels do we want to use the same vector tiles? +// 1 means "use tiles from all zoom levels". 2 means "use the same tiles for 2 +// subsequent zoom levels". var reuseZoomLevels = 2; -// Offset from web mercator zoom level 0 -var zOffset = 1; +// Offset of loaded tiles from web mercator zoom level 0. +// 0 means "At map zoom level 0, use tiles from zoom level 0". 1 means "At map +// zoom level 0, use tiles from zoom level 1". +var zoomOffset = 1; + +// Calculation of tile urls var resolutions = []; -for (var z = zOffset / reuseZoomLevels; z <= 22 / reuseZoomLevels; ++z) { +for (var z = zoomOffset / reuseZoomLevels; z <= 22 / reuseZoomLevels; ++z) { resolutions.push(156543.03392804097 / Math.pow(2, z * reuseZoomLevels)); } +function tileUrlFunction(tileCoord) { + return ('http://{a-d}.tiles.mapbox.com/v4/mapbox.mapbox-streets-v6/' + + '{z}/{x}/{y}.vector.pbf?access_token=' + accessToken) + .replace('{z}', String(tileCoord[0] * reuseZoomLevels + zoomOffset)) + .replace('{x}', String(tileCoord[1])) + .replace('{y}', String(-tileCoord[2] - 1)) + .replace('{a-d}', 'abcd'.substr( + ((tileCoord[1] << tileCoord[0]) + tileCoord[2]) % 4, 1)); +} -var map = new ol.Map({ +var map = new ol.Map({ layers: [ new ol.layer.VectorTile({ preload: Infinity, @@ -37,22 +52,13 @@ var map = new ol.Map({ '© ' + 'OpenStreetMap contributors' })], - rightHandedPolygons: true, format: new ol.format.MVT(), tileGrid: new ol.tilegrid.TileGrid({ extent: ol.proj.get('EPSG:3857').getExtent(), resolutions: resolutions }), tilePixelRatio: 16, - tileUrlFunction: function(tileCoord) { - return ('http://{a-d}.tiles.mapbox.com/v4/mapbox.mapbox-streets-v6/' + - '{z}/{x}/{y}.vector.pbf?access_token=' + accessToken) - .replace('{z}', String(tileCoord[0] * reuseZoomLevels + zOffset)) - .replace('{x}', String(tileCoord[1])) - .replace('{y}', String(-tileCoord[2] - 1)) - .replace('{a-d}', 'abcd'.substr( - ((tileCoord[1] << tileCoord[0]) + tileCoord[2]) % 4, 1)); - } + tileUrlFunction: tileUrlFunction }), style: createMapboxStreetsV6Style() }) @@ -64,3 +70,6 @@ var map = new ol.Map({ zoom: 3 }) }); + +// ol.style.Fill, ol.style.Icon, ol.style.Stroke, ol.style.Style and +// ol.style.Text are required for createMapboxStreetsV6Style() diff --git a/src/ol/layer/vectortilelayer.js b/src/ol/layer/vectortilelayer.js index a3330d414f..31797e1170 100644 --- a/src/ol/layer/vectortilelayer.js +++ b/src/ol/layer/vectortilelayer.js @@ -16,7 +16,7 @@ ol.layer.VectorTileProperty = { /** * @classdesc - * Vector tile data that is rendered client-side. + * Layer for vector tile data that is rendered client-side. * Note that any property set in the options is set as a {@link ol.Object} * property on the layer object; for example, setting `title: 'My Title'` in the * options means that `title` is observable, and has get/set accessors. diff --git a/src/ol/render/renderfeature.js b/src/ol/render/renderfeature.js index 21bc706cd9..4a147fed7d 100644 --- a/src/ol/render/renderfeature.js +++ b/src/ol/render/renderfeature.js @@ -10,7 +10,7 @@ goog.require('ol.geom.GeometryType'); /** * Lightweight, read-only, {@link ol.Feature} and {@link ol.geom.Geometry} like - * structure, optimized for rendering and styling. Geometry acces through the + * structure, optimized for rendering and styling. Geometry access through the * API is limited to getting the type and extent of the geometry. * * @constructor diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index d36696c86f..cb708c0891 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -66,7 +66,9 @@ ol.source.VectorEventType = { /** * @classdesc - * Provides a source of features for vector layers. + * Provides a source of features for vector layers. Vector features provided + * by this source are suitable for editing. See {@link ol.source.VectorTile} for + * vector data that is optimized for rendering. * * @constructor * @extends {ol.source.Source} diff --git a/src/ol/source/vectortilesource.js b/src/ol/source/vectortilesource.js index 4b264bcbff..4e8e2b4476 100644 --- a/src/ol/source/vectortilesource.js +++ b/src/ol/source/vectortilesource.js @@ -12,7 +12,13 @@ goog.require('ol.source.UrlTile'); /** * @classdesc - * Base class for sources providing images divided into a tile grid. + * Class for layer sources providing vector data divided into a tile grid, to be + * used with {@link ol.layer.VectorTile}. Although this source receives tiles + * with vector features from the server, it is not meant for feature editing. + * Features are optimized for rendering, their geometries are clipped at or near + * tile boundaries and simplified for a view resolution. See + * {@link ol.source.Vector} for vector sources that are suitable for feature + * editing. * * @constructor * @fires ol.source.TileEvent diff --git a/test/spec/ol/layer/vectortilelayer.test.js b/test/spec/ol/layer/vectortilelayer.test.js new file mode 100644 index 0000000000..4ced5c282e --- /dev/null +++ b/test/spec/ol/layer/vectortilelayer.test.js @@ -0,0 +1,37 @@ +goog.provide('ol.test.layer.VectorTile'); + +describe('ol.layer.VectorTile', function() { + + describe('constructor (defaults)', function() { + + var layer; + + beforeEach(function() { + layer = new ol.layer.VectorTile({ + source: new ol.source.VectorTile({}) + }); + }); + + afterEach(function() { + goog.dispose(layer); + }); + + it('creates an instance', function() { + expect(layer).to.be.a(ol.layer.VectorTile); + }); + + it('provides default preload', function() { + expect(layer.getPreload()).to.be(0); + }); + + it('provides default useInterimTilesOnError', function() { + expect(layer.getUseInterimTilesOnError()).to.be(true); + }); + + }); + +}); + +goog.require('goog.dispose'); +goog.require('ol.layer.VectorTile'); +goog.require('ol.source.VectorTile'); diff --git a/test/spec/ol/render/renderfeature.test.js b/test/spec/ol/render/renderfeature.test.js new file mode 100644 index 0000000000..622cfaddc4 --- /dev/null +++ b/test/spec/ol/render/renderfeature.test.js @@ -0,0 +1,90 @@ +goog.provide('ol.test.render.Feature'); + +describe('ol.render.Feature', function() { + + var renderFeature; + var type = 'Point'; + var flatCoordinates = [0, 0]; + var ends = null; + var properties = {foo: 'bar'}; + + describe('Constructor', function() { + it('creates an instance', function() { + renderFeature = + new ol.render.Feature(type, flatCoordinates, ends, properties); + expect(renderFeature).to.be.a(ol.render.Feature); + }); + }); + + describe('#get()', function() { + it('returns a single property', function() { + expect(renderFeature.get('foo')).to.be('bar'); + }); + }); + + describe('#getEnds()', function() { + it('returns the ends it was created with', function() { + expect(renderFeature.getEnds()).to.equal(ends); + }); + }); + + describe('#getExtent()', function() { + it('returns the correct extent for a point', function() { + expect(renderFeature.getExtent()).to.eql([0, 0, 0, 0]); + }); + it('caches the extent', function() { + expect(renderFeature.getExtent()).to.equal(renderFeature.extent_); + }); + it('returns the correct extent for a linestring', function() { + var feature = + new ol.render.Feature('LineString', [-1, -2, 2, 1], null, {}); + expect(feature.getExtent()).to.eql([-1, -2, 2, 1]); + }); + }); + + describe('#getFlatCoordinates()', function() { + it('returns the flat coordinates it was created with', function() { + expect(renderFeature.getFlatCoordinates()).to.equal(flatCoordinates); + }); + }); + + describe('#getGeometry()', function() { + it('returns itself as geometry', function() { + expect(renderFeature.getGeometry()).to.equal(renderFeature); + }); + }); + + describe('#getProperties()', function() { + it('returns the properties it was created with', function() { + expect(renderFeature.getProperties()).to.equal(properties); + }); + }); + + describe('#getSimplifiedGeometry()', function() { + it('returns itself as simplified geometry', function() { + expect(renderFeature.getSimplifiedGeometry()).to.equal(renderFeature); + }); + }); + + describe('#getStride()', function() { + it('returns 2', function() { + expect(renderFeature.getStride()).to.be(2); + }); + }); + + describe('#getStyleFunction()', function() { + it('returns undefined', function() { + expect(renderFeature.getStyleFunction()).to.be(undefined); + }); + }); + + describe('#getType()', function() { + it('returns the type it was created with', function() { + expect(renderFeature.getType()).to.equal(type); + }); + }); + +}); + + +goog.require('ol.render.Feature'); diff --git a/test/spec/ol/renderer/canvas/canvasvectortilelayerrenderer.test.js b/test/spec/ol/renderer/canvas/canvasvectortilelayerrenderer.test.js new file mode 100644 index 0000000000..da228a61c0 --- /dev/null +++ b/test/spec/ol/renderer/canvas/canvasvectortilelayerrenderer.test.js @@ -0,0 +1,128 @@ +goog.provide('ol.test.renderer.canvas.VectorTileLayer'); + +describe('ol.renderer.canvas.VectorTileLayer', function() { + + describe('constructor', function() { + + it('creates a new instance', function() { + var layer = new ol.layer.VectorTile({ + source: new ol.source.VectorTile({}) + }); + var renderer = new ol.renderer.canvas.VectorTileLayer(layer); + expect(renderer).to.be.a(ol.renderer.canvas.VectorTileLayer); + }); + + it('gives precedence to feature styles over layer styles', function() { + var target = document.createElement('div'); + target.style.width = '256px'; + target.style.height = '256px'; + document.body.appendChild(target); + var map = new ol.Map({ + view: new ol.View({ + center: [0, 0], + zoom: 0 + }), + target: target + }); + var layerStyle = [new ol.style.Style({ + text: new ol.style.Text({ + text: 'layer' + }) + })]; + var featureStyle = [new ol.style.Style({ + text: new ol.style.Text({ + text: 'feature' + }) + })]; + var feature1 = new ol.Feature(new ol.geom.Point([0, 0])); + var feature2 = new ol.Feature(new ol.geom.Point([0, 0])); + feature2.setStyle(featureStyle); + var TileClass = 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({ + format: new ol.format.MVT(), + tileClass: TileClass, + tileGrid: ol.tilegrid.createXYZ() + }); + var layer = new ol.layer.VectorTile({ + source: source, + style: layerStyle + }); + map.addLayer(layer); + var spy = sinon.spy(map.getRenderer().getLayerRenderer(layer), + 'renderFeature'); + map.renderSync(); + expect(spy.getCall(0).args[2]).to.be(layerStyle); + expect(spy.getCall(1).args[2]).to.be(featureStyle); + document.body.removeChild(target); + }); + + }); + + describe('#forEachFeatureAtCoordinate', function() { + var layer, renderer, replayGroup; + var TileClass = function() { + ol.VectorTile.apply(this, arguments); + this.setState('loaded'); + this.setProjection(ol.proj.get('EPSG:3857')); + this.replayState_.replayGroup = replayGroup; + }; + ol.inherits(TileClass, ol.VectorTile); + + beforeEach(function() { + replayGroup = {}; + layer = new ol.layer.VectorTile({ + source: new ol.source.VectorTile({ + tileClass: TileClass, + tileGrid: ol.tilegrid.createXYZ() + }) + }); + renderer = new ol.renderer.canvas.VectorTileLayer(layer); + replayGroup.forEachFeatureAtCoordinate = function(coordinate, + resolution, rotation, skippedFeaturesUids, callback) { + var feature = new ol.Feature(); + callback(feature); + callback(feature); + }; + }); + + it('calls callback once per feature with a layer as 2nd arg', function() { + var spy = sinon.spy(); + var coordinate = [0, 0]; + var frameState = { + layerStates: {}, + skippedFeatureUids: {}, + viewState: { + resolution: 1, + rotation: 0 + } + }; + frameState.layerStates[goog.getUid(layer)] = {}; + renderer.renderedTiles_ = [new TileClass([0, 0, -1])]; + renderer.forEachFeatureAtCoordinate( + coordinate, frameState, spy, undefined); + expect(spy.callCount).to.be(1); + expect(spy.getCall(0).args[1]).to.equal(layer); + }); + }); + +}); + + +goog.require('ol.Feature'); +goog.require('ol.Map'); +goog.require('ol.VectorTile'); +goog.require('ol.View'); +goog.require('ol.format.MVT'); +goog.require('ol.geom.Point'); +goog.require('ol.layer.VectorTile'); +goog.require('ol.proj'); +goog.require('ol.renderer.canvas.VectorTileLayer'); +goog.require('ol.source.VectorTile'); +goog.require('ol.style.Style'); +goog.require('ol.style.Text'); diff --git a/test/spec/ol/source/urltilesource.test.js b/test/spec/ol/source/urltilesource.test.js new file mode 100644 index 0000000000..8273b5956a --- /dev/null +++ b/test/spec/ol/source/urltilesource.test.js @@ -0,0 +1,162 @@ +goog.provide('ol.test.source.UrlTile'); + + +describe('ol.source.UrlTile', function() { + + describe('tileUrlFunction', function() { + + var tileSource, tileGrid; + + beforeEach(function() { + tileSource = new ol.source.UrlTile({ + projection: 'EPSG:3857', + tileGrid: ol.tilegrid.createXYZ({maxZoom: 6}), + url: '{z}/{x}/{y}', + wrapX: true + }); + tileGrid = tileSource.getTileGrid(); + }); + + it('returns the expected URL', function() { + + var coordinate = [829330.2064098881, 5933916.615134273]; + var tileUrl; + + tileUrl = tileSource.tileUrlFunction( + tileGrid.getTileCoordForCoordAndZ(coordinate, 0)); + expect(tileUrl).to.eql('0/0/0'); + + tileUrl = tileSource.tileUrlFunction( + tileGrid.getTileCoordForCoordAndZ(coordinate, 1)); + expect(tileUrl).to.eql('1/1/0'); + + tileUrl = tileSource.tileUrlFunction( + tileGrid.getTileCoordForCoordAndZ(coordinate, 2)); + expect(tileUrl).to.eql('2/2/1'); + + tileUrl = tileSource.tileUrlFunction( + tileGrid.getTileCoordForCoordAndZ(coordinate, 3)); + expect(tileUrl).to.eql('3/4/2'); + + tileUrl = tileSource.tileUrlFunction( + tileGrid.getTileCoordForCoordAndZ(coordinate, 4)); + expect(tileUrl).to.eql('4/8/5'); + + tileUrl = tileSource.tileUrlFunction( + tileGrid.getTileCoordForCoordAndZ(coordinate, 5)); + expect(tileUrl).to.eql('5/16/11'); + + tileUrl = tileSource.tileUrlFunction( + tileGrid.getTileCoordForCoordAndZ(coordinate, 6)); + expect(tileUrl).to.eql('6/33/22'); + + }); + + describe('wrap x', function() { + + it('returns the expected URL', function() { + var projection = tileSource.getProjection(); + var tileUrl = tileSource.tileUrlFunction( + tileSource.getTileCoordForTileUrlFunction( + [6, -31, -23], projection)); + expect(tileUrl).to.eql('6/33/22'); + + tileUrl = tileSource.tileUrlFunction( + tileSource.getTileCoordForTileUrlFunction( + [6, 33, -23], projection)); + expect(tileUrl).to.eql('6/33/22'); + + tileUrl = tileSource.tileUrlFunction( + tileSource.getTileCoordForTileUrlFunction( + [6, 97, -23], projection)); + expect(tileUrl).to.eql('6/33/22'); + }); + + }); + + describe('crop y', function() { + + it('returns the expected URL', function() { + var projection = tileSource.getProjection(); + var tileUrl = tileSource.tileUrlFunction( + tileSource.getTileCoordForTileUrlFunction( + [6, 33, 0], projection)); + expect(tileUrl).to.be(undefined); + + tileUrl = tileSource.tileUrlFunction( + tileSource.getTileCoordForTileUrlFunction( + [6, 33, -23], projection)); + expect(tileUrl).to.eql('6/33/22'); + + tileUrl = tileSource.tileUrlFunction( + tileSource.getTileCoordForTileUrlFunction( + [6, 33, -65], projection)); + expect(tileUrl).to.be(undefined); + }); + + }); + + }); + + describe('#getUrls', function() { + + var sourceOptions; + var source; + var url = 'http://geo.nls.uk/maps/towns/glasgow1857/{z}/{x}/{-y}.png'; + + beforeEach(function() { + sourceOptions = { + tileGrid: ol.tilegrid.createXYZ({ + extent: ol.proj.get('EPSG:4326').getExtent() + }) + }; + }); + + describe('using a "url" option', function() { + beforeEach(function() { + sourceOptions.url = url; + source = new ol.source.UrlTile(sourceOptions); + }); + + it('returns the XYZ URL', function() { + var urls = source.getUrls(); + expect(urls).to.be.eql([url]); + }); + + }); + + describe('using a "urls" option', function() { + beforeEach(function() { + sourceOptions.urls = ['some_xyz_url1', 'some_xyz_url2']; + source = new ol.source.UrlTile(sourceOptions); + }); + + it('returns the XYZ URLs', function() { + var urls = source.getUrls(); + expect(urls).to.be.eql(['some_xyz_url1', 'some_xyz_url2']); + }); + + }); + + describe('using a "tileUrlFunction"', function() { + beforeEach(function() { + sourceOptions.tileUrlFunction = function() { + return 'some_xyz_url'; + }; + source = new ol.source.UrlTile(sourceOptions); + }); + + it('returns null', function() { + var urls = source.getUrls(); + expect(urls).to.be(null); + }); + + }); + + }); + +}); + +goog.require('ol.TileCoord'); +goog.require('ol.proj'); +goog.require('ol.source.UrlTile'); diff --git a/test/spec/ol/source/vectortilesource.test.js b/test/spec/ol/source/vectortilesource.test.js new file mode 100644 index 0000000000..441c4f382d --- /dev/null +++ b/test/spec/ol/source/vectortilesource.test.js @@ -0,0 +1,43 @@ +goog.provide('ol.test.source.VectorTile'); + + +describe('ol.source.VectorTile', function() { + + var format = new ol.format.MVT(); + var source = new ol.source.VectorTile({ + format: format, + tileGrid: ol.tilegrid.createXYZ(), + url: '{z}/{x}/{y}.pbf' + }); + var tile; + + describe('constructor', function() { + it('sets the format on the instance', function() { + expect(source.format_).to.equal(format); + }); + it('uses ol.VectorTile as default tileClass', function() { + expect(source.tileClass).to.equal(ol.VectorTile); + }); + }); + + describe('#getTile()', function() { + it('creates a tile with the correct tile class', function() { + tile = source.getTile(0, 0, 0, 1, ol.proj.get('EPSG:3857')); + expect(tile).to.be.a(ol.VectorTile); + }); + it('sets the correct tileCoord on the created tile', function() { + expect(tile.getTileCoord()).to.eql([0, 0, 0]); + }); + it('fetches tile from cache when requested again', function() { + expect(source.getTile(0, 0, 0, 1, ol.proj.get('EPSG:3857'))) + .to.equal(tile); + }); + }); + +}); + + +goog.require('ol.VectorTile'); +goog.require('ol.format.MVT'); +goog.require('ol.proj'); +goog.require('ol.source.VectorTile');