From 2f90e3337d633e269a8e48547a9743a8c20451a2 Mon Sep 17 00:00:00 2001 From: Aymeric DUTREMBLE Date: Wed, 20 Sep 2023 12:20:28 +0200 Subject: [PATCH 1/4] feature(VectorTileSource): add support for multiple source --- src/Source/Source.js | 6 +- src/Source/VectorTilesSource.js | 86 ++++- test/functional/vector_tile_3d_mesh_mapbox.js | 6 +- test/unit/vectortiles.js | 356 +++++++++++------- 4 files changed, 307 insertions(+), 147 deletions(-) diff --git a/src/Source/Source.js b/src/Source/Source.js index cb6b0f33aa..c8183e0ca6 100644 --- a/src/Source/Source.js +++ b/src/Source/Source.js @@ -68,7 +68,6 @@ class /* istanbul ignore next */ ParsingOptions {} function fetchSourceData(source, extent) { const url = source.urlFromExtent(extent); - return source.fetcher(url, source.networkOptions).then((f) => { f.extent = extent; return f; @@ -190,8 +189,9 @@ class Source extends InformationsData { let features = cache.getByArray(key); if (!features) { // otherwise fetch/parse the data - features = cache.setByArray(fetchSourceData(this, extent).then(file => this.parser(file, { out, in: this }), - err => this.handlingError(err)), key); + features = cache.setByArray(fetchSourceData(this, extent) + .then(file => this.parser(file, { out, in: this }), + err => this.handlingError(err)), key); /* istanbul ignore next */ if (this.onParsedFile) { features.then((feat) => { diff --git a/src/Source/VectorTilesSource.js b/src/Source/VectorTilesSource.js index 9d2f69192d..77c1a0531b 100644 --- a/src/Source/VectorTilesSource.js +++ b/src/Source/VectorTilesSource.js @@ -1,6 +1,7 @@ import { featureFilter } from '@mapbox/mapbox-gl-style-spec'; import Style from 'Core/Style'; import TMSSource from 'Source/TMSSource'; +import URLBuilder from 'Provider/URLBuilder'; import Fetcher from 'Provider/Fetcher'; import urlParser from 'Parser/MapBoxUrlParser'; @@ -8,6 +9,22 @@ function toTMSUrl(url) { return url.replace(/\{/g, '${'); } +function fetchSourceData(source, url) { + return source.fetcher(url, source.networkOptions) + .then(f => f, err => source.handlingError(err)); +} + +function mergeCollections(collections) { + const collection = collections[0]; + collections.forEach((col, index) => { + if (index === 0) { return; } + col.features.forEach((feature) => { + collection.features.push(feature); + }); + }); + return collection; +} + /** * @classdesc * VectorTilesSource are object containing informations on how to fetch vector @@ -86,9 +103,6 @@ class VectorTilesSource extends TMSSource { return style; }).then((style) => { - const s = Object.keys(style.sources)[0]; - const os = style.sources[s]; - style.layers.forEach((layer, order) => { layer.sourceUid = this.uid; if (layer.type === 'background') { @@ -113,17 +127,34 @@ class VectorTilesSource extends TMSSource { }); if (this.url == '.') { - if (os.url) { - const urlSource = urlParser.normalizeSourceURL(os.url, this.accessToken); - return Fetcher.json(urlSource, this.networkOptions).then((tileJSON) => { - if (tileJSON.tiles[0]) { - this.url = toTMSUrl(tileJSON.tiles[0]); - } - }); - } else if (os.tiles[0]) { - this.url = toTMSUrl(os.tiles[0]); - } + const TMSUrlList = Object.values(style.sources).map((source) => { + if (source.url) { + const urlSource = urlParser.normalizeSourceURL(source.url, this.accessToken); + return Fetcher.json(urlSource, this.networkOptions).then((tileJSON) => { + if (tileJSON.tiles[0]) { + return toTMSUrl(tileJSON.tiles[0]); + } + }); + } else if (source.tiles) { + return Promise.resolve(toTMSUrl(source.tiles[0])); + } + return Promise.reject(); + }); + return Promise.all(TMSUrlList); } + return (Promise.resolve([this.url])); + }).then((TMSUrlList) => { + this.url = new Set(TMSUrlList); + }); + } + + urlFromExtent(extent) { + return this.url.map((url) => { + const options = { + tileMatrixCallback: this.tileMatrixCallback, + url, + }; + return URLBuilder.xyz(extent, options); }); } @@ -136,6 +167,35 @@ class VectorTilesSource extends TMSSource { } } } + + loadData(extent, out) { + const cache = this._featuresCaches[out.crs]; + const key = this.requestToKey(extent); + // try to get parsed data from cache + let features = cache.getByArray(key); + if (!features) { + // otherwise fetch/parse the data + features = cache.setByArray( + Promise.all(this.urlFromExtent(extent).map(url => + fetchSourceData(this, url) + .then((file) => { + file.extent = extent; + return this.parser(file, { out, in: this }); + }), + )).then(collections => mergeCollections(collections), + err => this.handlingError(err)), key); + + /* istanbul ignore next */ + if (this.onParsedFile) { + features.then((feat) => { + this.onParsedFile(feat); + console.warn('Source.onParsedFile was deprecated'); + return feat; + }); + } + } + return features; + } } export default VectorTilesSource; diff --git a/test/functional/vector_tile_3d_mesh_mapbox.js b/test/functional/vector_tile_3d_mesh_mapbox.js index 786bb98edb..903514c45b 100644 --- a/test/functional/vector_tile_3d_mesh_mapbox.js +++ b/test/functional/vector_tile_3d_mesh_mapbox.js @@ -1,6 +1,6 @@ const assert = require('assert'); -describe('vector_tile_3d_mesh_mapbox', function _() { +describe('vector_tile_3d_mesh_mapbox', function _describe() { let result; before(async () => { result = await loadExample('examples/vector_tile_3d_mesh_mapbox.html', this.fullTitle()); @@ -10,8 +10,8 @@ describe('vector_tile_3d_mesh_mapbox', function _() { assert.ok(result); }); - it('should correctly load building features on a given TMS tile', async function _2() { - const featuresCollection = await page.evaluate(async function _3() { + it('should correctly load building features on a given TMS tile', async function _it() { + const featuresCollection = await page.evaluate(async function _() { const layers = view.getLayers(l => l.source && l.source.isVectorSource); const res = await layers[0].source.loadData({ zoom: 15, row: 11634, col: 16859 }, { crs: 'EPSG:4978', source: { crs: 'TMS:3857' } }); return res; diff --git a/test/unit/vectortiles.js b/test/unit/vectortiles.js index 4464ab319d..c473a10e81 100644 --- a/test/unit/vectortiles.js +++ b/test/unit/vectortiles.js @@ -10,11 +10,13 @@ import sinon from 'sinon'; import style from '../data/vectortiles/style.json'; import tilejson from '../data/vectortiles/tilejson.json'; import sprite from '../data/vectortiles/sprite.json'; +import mapboxStyle from '../data/mapboxMulti.json'; const resources = { 'test/data/vectortiles/style.json': style, 'https://test/tilejson.json': tilejson, 'https://test/sprite.json': sprite, + 'https://api.mapbox.com/v4/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v7.json': mapboxStyle, }; function parse(pbf, layers) { @@ -27,7 +29,7 @@ function parse(pbf, layers) { crs: 'EPSG:3857', }, }); -} +}; describe('Vector tiles', function () { let stub; @@ -89,159 +91,257 @@ describe('Vector tiles', function () { done(); }).catch(done); }); +}); - describe('VectorTilesSource', function () { - it('throws an error because no style was provided', () => { - assert.throws(() => new VectorTilesSource({}), { - name: 'Error', - message: 'New VectorTilesSource: style is required', +describe('VectorTilesSource', function () { + let stub; + before(function () { + stub = sinon.stub(Fetcher, 'json') + .callsFake((url) => { + url = url.split('?')[0]; + return Promise.resolve(JSON.parse(resources[url])); }); + }); + after(function () { + stub.restore(); + }); + + it('throws an error because no style was provided', () => { + assert.throws(() => new VectorTilesSource({}), { + name: 'Error', + message: 'New VectorTilesSource: style is required', }); + }); - it('reads tiles URL from the style', (done) => { - const source = new VectorTilesSource({ - style: { - sources: { tilesurl: { tiles: ['http://server.geo/{z}/{x}/{y}.pbf'] } }, - layers: [], - }, - }); - source.whenReady.then(() => { - // eslint-disable-next-line no-template-curly-in-string - assert.equal(source.url, 'http://server.geo/${z}/${x}/${y}.pbf'); - done(); - }).catch(done); + it('reads tiles URL directly from the style', (done) => { + const source = new VectorTilesSource({ + style: { + sources: { sourceTiles: { tiles: ['http://server.geo/{z}/{x}/{y}.pbf'] } }, + layers: [], + }, }); + source.whenReady.then(() => { + assert.equal(source.url.size, 1); + // eslint-disable-next-line no-template-curly-in-string + assert.ok(source.url.has('http://server.geo/${z}/${x}/${y}.pbf')); + done(); + }) + .catch(done); + }); - it('reads the background layer', (done) => { - const source = new VectorTilesSource({ - url: 'fakeurl', - style: { - sources: { tilejson: {} }, - layers: [{ type: 'background' }], - }, - }); - source.whenReady.then(() => { - assert.ok(source.backgroundLayer); + it('reads tiles URL from an url', (done) => { + const source = new VectorTilesSource({ + style: { + sources: { sourceUrl: { url: 'mapbox://mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v7' } }, + layers: [{ + id: 'building', + source: 'sourceUrl', + 'source-layer': 'building', + type: 'fill', + paint: { + 'fill-color': 'red', + }, + }], + }, + accessToken: 'pk.eyJ1IjoiZnRvcm9tYW5vZmYiLCJhIjoiY2xramhzM2xrMDVibjNpcGNvdGRlZWQ5YyJ9.NibhjJNVTxArsNSH4v_kIA', + }); + source.whenReady + .then(() => { + assert.equal(source.url, '.'); + assert.equal(source.urls.size, 1); done(); - }).catch(done); + }) + .catch(done); + }); + + it('reads the background layer', (done) => { + const source = new VectorTilesSource({ + url: 'fakeurl', + style: { + sources: { tilejson: {} }, + layers: [{ type: 'background' }], + }, }); + source.whenReady.then(() => { + assert.ok(source.backgroundLayer); + done(); + }) + .catch(done); + }); - it('creates styles and assigns filters', (done) => { - const source = new VectorTilesSource({ - url: 'fakeurl', - style: { - sources: { tilejson: {} }, - layers: [{ - id: 'land', - type: 'fill', - paint: { - 'fill-color': 'rgb(255, 0, 0)', - }, - }], - }, - }); - source.whenReady.then(() => { - assert.ok(source.styles.land); + it('creates styles and assigns filters', (done) => { + const source = new VectorTilesSource({ + url: 'fakeurl', + style: { + sources: { tilejson: {} }, + layers: [{ + id: 'land', + type: 'fill', + paint: { + 'fill-color': 'rgb(255, 0, 0)', + }, + }], + }, + }); + source.whenReady.then(() => { + assert.ok(source.styles.land); + assert.equal(source.styles.land.fill.color, 'rgb(255,0,0)'); + done(); + }) + .catch(done); + }); + + it('loads the style from a file', function _it(done) { + const source = new VectorTilesSource({ + style: 'test/data/vectortiles/style.json', + networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {}, + }); + source.whenReady + .then(() => { assert.equal(source.styles.land.fill.color, 'rgb(255,0,0)'); + assert.equal(source.styles.land.fill.opacity, 1); + assert.equal(source.styles.land.zoom.min, 5); + assert.equal(source.styles.land.zoom.max, 13); done(); }).catch(done); + }); + + it('sets the correct Style#zoom.min', (done) => { + const source = new VectorTilesSource({ + url: 'fakeurl', + style: { + sources: { tilejson: {} }, + layers: [{ + // minzoom is 0 (default value) + id: 'first', + type: 'fill', + paint: { + 'fill-color': 'rgb(255, 0, 0)', + }, + }, { + // minzoom is 5 (specified) + id: 'second', + type: 'fill', + paint: { + 'fill-color': 'rgb(255, 0, 0)', + }, + minzoom: 5, + }, { + // minzoom is 4 (first stop) + // If a style have `stops` expression, should it be used to determine the min zoom? + id: 'third', + type: 'fill', + paint: { + 'fill-color': 'rgb(255, 0, 0)', + 'fill-opacity': { stops: [[4, 1], [7, 0.5]] }, + }, + }, { + // minzoom is 1 (first stop and no specified minzoom) + id: 'fourth', + type: 'fill', + paint: { + 'fill-color': 'rgb(255, 0, 0)', + 'fill-opacity': { stops: [[1, 1], [7, 0.5]] }, + }, + }, { + // minzoom is 4 (first stop is higher than specified) + id: 'fifth', + type: 'fill', + paint: { + 'fill-color': 'rgb(255, 0, 0)', + 'fill-opacity': { stops: [[4, 1], [7, 0.5]] }, + }, + minzoom: 3, + }], + }, }); - it('loads the style from a file', function _it(done) { + source.whenReady.then(() => { + assert.equal(source.styles.first.zoom.min, 0); + assert.equal(source.styles.second.zoom.min, 5); + assert.equal(source.styles.third.zoom.min, 0); + assert.equal(source.styles.fourth.zoom.min, 0); + assert.equal(source.styles.fifth.zoom.min, 3); + done(); + }) + .catch(done); + }); + + it('Vector tile source mapbox url', () => { + const accessToken = 'pk.xxxxx'; + const baseurl = 'mapbox://styles/mapbox/outdoors-v11'; + + const styleUrl = urlParser.normalizeStyleURL(baseurl, accessToken); + assert.ok(styleUrl.startsWith('https://api.mapbox.com')); + assert.ok(styleUrl.endsWith(accessToken)); + + const spriteUrl = urlParser.normalizeSpriteURL(baseurl, '', '.json', accessToken); + assert.ok(spriteUrl.startsWith('https')); + assert.ok(spriteUrl.endsWith(accessToken)); + assert.ok(spriteUrl.includes('sprite.json')); + + const imgUrl = urlParser.normalizeSpriteURL(baseurl, '', '.png', accessToken); + assert.ok(imgUrl.includes('sprite.png')); + + const url = 'mapbox://mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2'; + const urlSource = urlParser.normalizeSourceURL(url, accessToken); + assert.ok(urlSource.startsWith('https')); + assert.ok(urlSource.endsWith(accessToken)); + assert.ok(urlSource.includes('.json')); + }); + + describe('multisource', function () { + it('2 sources with different url tiles', (done) => { const source = new VectorTilesSource({ - style: 'test/data/vectortiles/style.json', + style: { + sources: { + source1: { + type: 'vector', + tiles: ['http://server.geo/{z}/{x}/{y}.pbf'], + }, + source2: { + type: 'vector', + tiles: ['http://server.geo2/{z}/{x}/{y}.pbf'], + }, + }, + layers: [], + }, }); source.whenReady .then(() => { - assert.equal(source.styles.land.fill.color, 'rgb(255,0,0)'); - assert.equal(source.styles.land.fill.opacity, 1); - assert.equal(source.styles.land.zoom.min, 5); - assert.equal(source.styles.land.zoom.max, 13); + assert.equal(source.url.size, 2); + // eslint-disable-next-line no-template-curly-in-string + assert.ok(source.url.has('http://server.geo/${z}/${x}/${y}.pbf')); + // eslint-disable-next-line no-template-curly-in-string + assert.ok(source.url.has('http://server.geo2/${z}/${x}/${y}.pbf')); done(); - }).catch(done); + }) + .catch(done); }); - - it('sets the correct Style#zoom.min', (done) => { + it('2 sources with same url tiles', (done) => { const source = new VectorTilesSource({ - url: 'fakeurl', style: { - sources: { tilejson: {} }, - layers: [{ - // minzoom is 0 (default value) - id: 'first', - type: 'fill', - paint: { - 'fill-color': 'rgb(255, 0, 0)', - }, - }, { - // minzoom is 5 (specified) - id: 'second', - type: 'fill', - paint: { - 'fill-color': 'rgb(255, 0, 0)', - }, - minzoom: 5, - }, { - // minzoom is 4 (first stop) - // If a style have `stops` expression, should it be used to determine the min zoom? - id: 'third', - type: 'fill', - paint: { - 'fill-color': 'rgb(255, 0, 0)', - 'fill-opacity': { stops: [[4, 1], [7, 0.5]] }, - }, - }, { - // minzoom is 1 (first stop and no specified minzoom) - id: 'fourth', - type: 'fill', - paint: { - 'fill-color': 'rgb(255, 0, 0)', - 'fill-opacity': { stops: [[1, 1], [7, 0.5]] }, + sources: { + source1: { + type: 'vector', + tiles: ['http://server.geo/{z}/{x}/{y}.pbf'], }, - }, { - // minzoom is 4 (first stop is higher than specified) - id: 'fifth', - type: 'fill', - paint: { - 'fill-color': 'rgb(255, 0, 0)', - 'fill-opacity': { stops: [[4, 1], [7, 0.5]] }, + source2: { + type: 'vector', + tiles: ['http://server.geo/{z}/{x}/{y}.pbf'], }, - minzoom: 3, - }], + }, + layers: [], }, }); - - source.whenReady.then(() => { - assert.equal(source.styles.first.zoom.min, 0); - assert.equal(source.styles.second.zoom.min, 5); - assert.equal(source.styles.third.zoom.min, 0); - assert.equal(source.styles.fourth.zoom.min, 0); - assert.equal(source.styles.fifth.zoom.min, 3); - done(); - }).catch(done); - }); - - it('Vector tile source mapbox url', () => { - const accessToken = 'pk.xxxxx'; - const baseurl = 'mapbox://styles/mapbox/outdoors-v11'; - - const styleUrl = urlParser.normalizeStyleURL(baseurl, accessToken); - assert.ok(styleUrl.startsWith('https://api.mapbox.com')); - assert.ok(styleUrl.endsWith(accessToken)); - - const spriteUrl = urlParser.normalizeSpriteURL(baseurl, '', '.json', accessToken); - assert.ok(spriteUrl.startsWith('https')); - assert.ok(spriteUrl.endsWith(accessToken)); - assert.ok(spriteUrl.includes('sprite.json')); - - const imgUrl = urlParser.normalizeSpriteURL(baseurl, '', '.png', accessToken); - assert.ok(imgUrl.includes('sprite.png')); - - const url = 'mapbox://mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2'; - const urlSource = urlParser.normalizeSourceURL(url, accessToken); - assert.ok(urlSource.startsWith('https')); - assert.ok(urlSource.endsWith(accessToken)); - assert.ok(urlSource.includes('.json')); + source.whenReady + .then(() => { + assert.equal(source.url.size, 1); + // eslint-disable-next-line no-template-curly-in-string + assert.ok(source.url.has('http://server.geo/${z}/${x}/${y}.pbf')); + done(); + }) + .catch(done); }); }); }); From 521a85064fac7bd9754c013990ceb6463dc766c7 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 7 Feb 2024 15:07:34 +0100 Subject: [PATCH 2/4] refactor(test): use sinon for multisource --- src/Source/VectorTilesSource.js | 2 +- test/data/mapboxMulti.json | 438 ++++++++++++++++++++++++++++++++ test/unit/vectortiles.js | 73 +++++- 3 files changed, 501 insertions(+), 12 deletions(-) create mode 100644 test/data/mapboxMulti.json diff --git a/src/Source/VectorTilesSource.js b/src/Source/VectorTilesSource.js index 77c1a0531b..979f42e829 100644 --- a/src/Source/VectorTilesSource.js +++ b/src/Source/VectorTilesSource.js @@ -144,7 +144,7 @@ class VectorTilesSource extends TMSSource { } return (Promise.resolve([this.url])); }).then((TMSUrlList) => { - this.url = new Set(TMSUrlList); + this.urls = Array.from(new Set(TMSUrlList)); }); } diff --git a/test/data/mapboxMulti.json b/test/data/mapboxMulti.json new file mode 100644 index 0000000000..c51f51804d --- /dev/null +++ b/test/data/mapboxMulti.json @@ -0,0 +1,438 @@ +{ + "attribution": "© Mapbox © OpenStreetMap Improve this map", + "bounds": [ + -180, + -85, + 180, + 85 + ], + "center": [ + 0, + 0, + 0 + ], + "created": 1527888859555, + "filesize": 0, + "fillzoom": 8, + "format": "pbf", + "id": "mapbox.mapbox-streets-v8", + "language_options": { + "ar": "Arabic", + "ca": "Catalan", + "cs": "Czech", + "da": "Danish", + "de": "German", + "el": "Greek", + "en": "English", + "es": "Spanish", + "fa": "Farsi", + "fi": "Finnish", + "fr": "French", + "he": "Hebrew", + "hu": "Hungarian", + "id": "Indonesian", + "it": "Italian", + "ja": "Japanese", + "ka": "Georgian", + "ko": "Korean", + "local": "Renderable local language", + "lv": "Latvian", + "ms": "Malay", + "nb": "Norwegian BokmÃ¥l", + "nl": "Dutch", + "no": "Norwegian", + "pl": "Polish", + "pt": "Portuguese", + "ro": "Romanian", + "ru": "Russian", + "sk": "Slovak", + "sl": "Slovenian", + "sr": "Serbian", + "sv": "Swedish", + "th": "Thai", + "tl": "Tagalog", + "tr": "Turkish", + "uk": "Ukrainian", + "vi": "Vietnamese", + "zh-Hans": "Simplified Chinese", + "zh-Hant": "Traditional Chinese" + }, + "mapbox_logo": true, + "maxzoom": 16, + "minzoom": 0, + "modified": 1692949181684, + "name": "Mapbox Streets v8", + "private": false, + "scheme": "xyz", + "tilejson": "2.2.0", + "tiles": [ + "https://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v8/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1IjoiZnRvcm9tYW5vZmYiLCJhIjoiY2xramhzM2xrMDVibjNpcGNvdGRlZWQ5YyJ9.NibhjJNVTxArsNSH4v_kIA", + "https://b.tiles.mapbox.com/v4/mapbox.mapbox-streets-v8/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1IjoiZnRvcm9tYW5vZmYiLCJhIjoiY2xramhzM2xrMDVibjNpcGNvdGRlZWQ5YyJ9.NibhjJNVTxArsNSH4v_kIA" + ], + "vector_layers": [ + { + "description": "", + "fields": { + "class": "One of: aboriginal_lands, agriculture, airport, cemetery, commercial_area, facility, glacier, grass, hospital, industrial, park, parking, piste, pitch, residential, rock, sand, school, scrub, wood", + "type": "OSM tag, more specific than class" + }, + "id": "landuse", + "minzoom": 5, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "class": "One of: river, canal, stream, stream_intermittent, ditch, drain", + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the country/territory the feature is in.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the feature is in.", + "type": "One of: river, canal, stream, ditch, drain" + }, + "id": "waterway", + "minzoom": 7, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": {}, + "id": "water", + "minzoom": 0, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the country/territory the feature is in.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the feature is in.", + "ref": "Text. Identifier of the runway or taxiway", + "type": "One of: runway, taxiway, apron, helipad" + }, + "id": "aeroway", + "minzoom": 9, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "class": "One of: cliff, crosswalk, entrance, fence, gate, hedge, land", + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the country/territory the feature is in.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the feature is in.", + "type": "The value of either the 'barrier' or 'man_made' tag from OSM, or for cliffs either cliff or earth_bank." + }, + "id": "structure", + "minzoom": 13, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "extrude": "String. Whether building should be extruded when rendering in 3D. One of: 'true', 'false'", + "height": "Number. Height of building or part of building.", + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the country/territory the feature is in.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the feature is in.", + "min_height": "Number. Height of bottom of building or part of building, if it does not start at ground level.", + "type": "In most cases, values will be that of the primary key from OpenStreetMap tags.", + "underground": "Text. Whether building is underground. One of: 'true', 'false'" + }, + "id": "building", + "minzoom": 12, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "class": "One of: national_park, wetland, wetland_noveg", + "type": "OSM tag, more specific than class" + }, + "id": "landuse_overlay", + "minzoom": 5, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "bike_lane": "Text. Has a value if there is a bike lane that is part of the road itself. This is different from a separated cycle track, which will be shown as its own line. Possible values are 'right', 'left', 'both' (bike lane on right, left, or both sides of the street respectively), 'yes' (bike lane present but location not specified), 'no' (area was surveyed and confirmed to not have a bike lane), and null (presence of bike lane unknown).", + "class": "One of: 'motorway', 'motorway_link', 'trunk', 'primary', 'secondary', 'tertiary', 'trunk_link', 'primary_link', 'secondary_link', 'tertiary_link', 'street', 'street_limited', 'pedestrian', 'construction', 'track', 'service', 'ferry', 'path', 'golf', 'level_crossing', 'turning_circle', 'roundabout', 'mini_roundabout', 'turning_loop', 'traffic_signals'", + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the country/territory the feature is in.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the feature is in.", + "lane_count": "Number. Number of lanes in the road", + "layer": "Number. Specifies z-ordering in the case of overlapping road segments. Common range is -5 to 5. Available from zoom level 13+.", + "len": "Number. Approximate length of the road segment in Mercator meters.", + "name": "Local name of the road", + "name_ar": "Arabic name of the road", + "name_de": "German name of the road", + "name_en": "English name of the road", + "name_es": "Spanish name of the road", + "name_fr": "French name of the road", + "name_it": "Italian name of the road", + "name_ja": "Japanese name of the road", + "name_ko": "Korean name of the road", + "name_pt": "Portuguese name of the road", + "name_ru": "Russian name of the road", + "name_script": "Primary written script of the local name", + "name_vi": "Vietnamese name of the road", + "name_zh-Hans": "Simplified Chinese name of the road", + "name_zh-Hant": "Traditional Chinese name of the road", + "oneway": "Text. Whether traffic on the road is one-way. One of: 'true', 'false'.", + "ref": "Text. Route number/code of the road.", + "reflen": "Number. How many characters long the ref tag is. Useful for shield styling.", + "shield": "Text. The shield style to use. See the vector tile documentation for a list of possible values.", + "shield_beta": "Text. The shield style to use if it doesn't exist in default shield values.", + "shield_text_color": "Text. The color of the text to use on the highway shield.", + "shield_text_color_beta": "Text. The color of the text to use on the beta highway shield.", + "structure": "Text. One of: 'none', 'bridge', 'tunnel', 'ford'. Available from zoom level 13+.", + "surface": "Whether the road is paved or not (if known). One of: 'paved', 'unpaved'", + "toll": "Whether a road is a toll road or not.", + "type": "In most cases, values will be that of the primary key from OpenStreetMap tags." + }, + "id": "road", + "minzoom": 3, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "admin_level": "Number, 0-2. The administrative level of the boundary", + "disputed": "Disputed boundaries are 'true', all others are 'false'.", + "iso_3166_1": "The ISO 3166-1 alpha-2 code(s) of the state(s) a boundary is part of. Format: 'AA' or 'AA-BB'", + "maritime": "Maritime boundaries are 'true', all others are 'false'.", + "worldview": "One of 'all', 'CN', 'IN', 'US'. Use for filtering boundaries to match different worldviews." + }, + "id": "admin", + "minzoom": 0, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "abbr": "Text. Local abbreviation of the place (available for type=state).", + "capital": "Admin level the city is a capital of, if any. One of: 2, 3, 4, 5, 6, null", + "class": "One of: country, state, settlement, or settlement_subdivision", + "filterrank": "Number, 0-5. Priority relative to nearby places. Useful for limiting label density.", + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the place.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the road is in.", + "name": "Local name of the place", + "name_ar": "Arabic name of the place", + "name_de": "German name of the place", + "name_en": "English name of the place", + "name_es": "Spanish name of the place", + "name_fr": "French name of the place", + "name_it": "Italian name of the place", + "name_ja": "Japanese name of the place", + "name_ko": "Korean name of the place", + "name_pt": "Portuguese name of the place", + "name_ru": "Russian name of the place", + "name_script": "Primary written script of the local name", + "name_vi": "Vietnamese name of the place", + "name_zh-Hans": "Simplified Chinese name of the place", + "name_zh-Hant": "Traditional Chinese name of the place", + "symbolrank": "Number, 1-18. Useful for styling text & marker sizes.", + "text_anchor": "A hint for label placement at low zoom levels.", + "type": "One of: country, territory, sar, disputed_territory, state, city, town, village, hamlet, suburb, quarter, neighbourhood, island, islet, archipelago, residential, aboriginal_lands", + "worldview": "One of 'all', 'CN', 'IN', 'US'. Use for filtering boundaries to match different worldviews." + }, + "id": "place_label", + "minzoom": 0, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "class": "One of: military, civil", + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the country/territory the feature is in.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the feature is in.", + "maki": "One of: airport, heliport, rocket", + "name": "Local name of the airport", + "name_ar": "Arabic name of the airport", + "name_de": "German name of the airport", + "name_en": "English name of the airport", + "name_es": "Spanish name of the airport", + "name_fr": "French name of the airport", + "name_it": "Italian name of the airport", + "name_ja": "Japanese name of the airport", + "name_ko": "Korean name of the airport", + "name_pt": "Portuguese name of the airport", + "name_ru": "Russian name of the airport", + "name_script": "Primary written script of the local name", + "name_vi": "Vietnamese name of the airport", + "name_zh-Hans": "Simplified Chinese name of the airport", + "name_zh-Hant": "Traditional Chinese name of the airport", + "ref": "A 3-4 character IATA, FAA, ICAO, or other reference code", + "sizerank": "A scale-dependent feature size ranking from 0 (large) to 16 (small)", + "worldview": "One of 'all', 'CN', 'IN', 'US'. Use for filtering boundaries to match different worldviews." + }, + "id": "airport_label", + "minzoom": 8, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "filterrank": "Number, 0-5. Priority relative to nearby features. Useful for limiting label density.", + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the country/territory the feature is in.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the feature is in.", + "maki": "One of: rail, rail-metro, rail-light, entrance, bus, bicycle-share, ferry", + "mode": "One of: rail, metro_rail, light_rail, tram, bus, monorail, funicular, bicycle, ferry, narrow_gauge, preserved, miniature", + "name": "Local name of the transit stop", + "name_ar": "Arabic name of the transit stop", + "name_de": "German name of the transit stop", + "name_en": "English name of the transit stop", + "name_es": "Spanish name of the transit stop", + "name_fr": "French name of the transit stop", + "name_it": "Italian name of the transit stop", + "name_ja": "Japanese name of the transit stop", + "name_ko": "Korean name of the transit stop", + "name_pt": "Portuguese name of the transit stop", + "name_ru": "Russian name of the transit stop", + "name_script": "Primary written script of the local name", + "name_vi": "Vietnamese name of the transit stop", + "name_zh-Hans": "Simplified Chinese name of the transit stop", + "name_zh-Hant": "Traditional Chinese name of the transit stop", + "network": "The network(s) that the station serves. Useful for icon styling.", + "network_beta": "One of: jp-shinkansen, jp-shinkansen.jp-jr, jp-shinkansen.tokyo-metro, jp-shinkansen.osaka-subway, jp-shinkansen.jp-jr.tokyo-metro, jp-shinkansen.jp-jr.osaka-subway, jp-jr, jp-jr.tokyo-metro, jp-jr.osaka-subway", + "stop_type": "One of: station, stop, entrance" + }, + "id": "transit_stop_label", + "minzoom": 5, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "class": "One of: glacier, landform, water_feature, wetland, ocean, sea, river, water, reservoir, dock, canal, drain, ditch, stream, continent", + "elevation_ft": "Integer elevation in feet", + "elevation_m": "Integer elevation in meters", + "filterrank": "Number, 0-5. Priority relative to nearby features. Useful for limiting label density.", + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the country/territory the feature is in.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the feature is in.", + "maki": "One of: 'mountain', 'volcano', 'waterfall'", + "name": "Local name of the natural feature", + "name_ar": "Arabic name of the natural feature", + "name_de": "German name of the natural feature", + "name_en": "English name of the natural feature", + "name_es": "Spanish name of the natural feature", + "name_fr": "French name of the natural feature", + "name_it": "Italian name of the natural feature", + "name_ja": "Japanese name of the natural feature", + "name_ko": "Korean name of the natural feature", + "name_pt": "Portuguese name of the natural feature", + "name_ru": "Russian name of the natural feature", + "name_script": "Primary written script of the local name", + "name_vi": "Vietnamese name of the natural feature", + "name_zh-Hans": "Simplified Chinese name of the natural feature", + "name_zh-Hant": "Traditional Chinese name of the natural feature", + "sizerank": "A scale-dependent feature size ranking from 0 (large) to 16 (small)", + "worldview": "One of 'all', 'CN', 'IN', 'US'. Use for filtering boundaries to match different worldviews." + }, + "id": "natural_label", + "minzoom": 0, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "brand": "String", + "category_en": "English category description of the POI", + "category_zh-Hans": "Simplified Chinese category description of the POI", + "class": "Text. Thematic groupings of POIs for filtering & styling.", + "filterrank": "Number, 0-5. Priority relative to nearby POIs. Useful for limiting label density.", + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the country/territory the feature is in.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the feature is in.", + "maki": "The name of the Maki icon that should be used for the POI", + "maki_beta": "", + "maki_modifier": "", + "name": "Local name of the POI", + "name_ar": "Arabic name of the POI", + "name_de": "German name of the POI", + "name_en": "English name of the POI", + "name_es": "Spanish name of the POI", + "name_fr": "French name of the POI", + "name_it": "Italian name of the POI", + "name_ja": "Japanese name of the POI", + "name_ko": "Korean name of the POI", + "name_pt": "Portuguese name of the POI", + "name_ru": "Russian name of the POI", + "name_script": "Primary written script of the local name", + "name_vi": "Vietnamese name of the POI", + "name_zh-Hans": "Simplified Chinese name of the POI", + "name_zh-Hant": "Traditional Chinese name of the POI", + "sizerank": "A scale-dependent feature size ranking from 0 (large) to 16 (small)", + "type": "The original OSM tag value" + }, + "id": "poi_label", + "minzoom": 6, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "class": "The class of road the junction is on. Subset of classes in the road layer. One of: motorway, motorway_link, trunk, trunk_link, primary, secondary, tertiary, primary_link, secondary_link, tertiary_link, street, street_limited, construction, track, service, path, major_rail, minor_rail, service_rail.", + "filterrank": "Number, 0-5", + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the country/territory the feature is in.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the feature is in.", + "maki_beta": "", + "name": "Local name of the motorway junction", + "name_ar": "Arabic name of the motorway junction", + "name_de": "German name of the motorway junction", + "name_en": "English name of the motorway junction", + "name_es": "Spanish name of the motorway junction", + "name_fr": "French name of the motorway junction", + "name_it": "Italian name of the motorway junction", + "name_ja": "Japanese name of the motorway junction", + "name_ko": "Korean name of the motorway junction", + "name_pt": "Portuguese name of the motorway junction", + "name_ru": "Russian name of the motorway junction", + "name_script": "Primary written script of the local name", + "name_vi": "Vietnamese name of the motorway junction", + "name_zh-Hans": "Simplified Chinese name of the motorway junction", + "name_zh-Hant": "Traditional Chinese name of the motorway junction", + "ref": "A short identifier", + "reflen": "The number of characters in the ref field.", + "type": "The type of road the junction is on. Subset of types in the road layer." + }, + "id": "motorway_junction", + "minzoom": 9, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + }, + { + "description": "", + "fields": { + "house_num": "House number", + "iso_3166_1": "Text. The ISO 3166-1 alpha-2 code of the country/territory the feature is in.", + "iso_3166_2": "Text. The ISO 3166-2 code of the state/province/region the feature is in." + }, + "id": "housenum_label", + "minzoom": 16, + "source": "mapbox.mapbox-streets-v8", + "source_name": "Mapbox Streets v8" + } + ], + "webpage": "https://studio.mapbox.com/tilesets/mapbox.mapbox-streets-v8", + "worldview_default": "US", + "worldview_options": { + "AR": "Argentina", + "CN": "China", + "IN": "India", + "JP": "Japan", + "MA": "Morocco", + "RS": "Serbia", + "RU": "Russia", + "TR": "Turkey", + "US": "United States" + } +} \ No newline at end of file diff --git a/test/unit/vectortiles.js b/test/unit/vectortiles.js index c473a10e81..5a55db0272 100644 --- a/test/unit/vectortiles.js +++ b/test/unit/vectortiles.js @@ -4,6 +4,7 @@ import VectorTileParser from 'Parser/VectorTileParser'; import VectorTilesSource from 'Source/VectorTilesSource'; import Extent from 'Core/Geographic/Extent'; import urlParser from 'Parser/MapBoxUrlParser'; +import { supportedFetchers } from 'Source/Source'; import Fetcher from 'Provider/Fetcher'; import sinon from 'sinon'; @@ -94,15 +95,23 @@ describe('Vector tiles', function () { }); describe('VectorTilesSource', function () { + let stubFetcherjson; let stub; before(function () { - stub = sinon.stub(Fetcher, 'json') + stubFetcherjson = sinon.stub(Fetcher, 'json') .callsFake((url) => { url = url.split('?')[0]; return Promise.resolve(JSON.parse(resources[url])); }); + const multipolygon = fs.readFileSync('test/data/pbf/multipolygon.pbf'); + const stubSupportedFetchers = new Map([ + ['application/x-protobuf;type=mapbox-vector', () => Promise.resolve(multipolygon)], + ]); + stub = sinon.stub(supportedFetchers, 'get') + .callsFake(format => stubSupportedFetchers.get(format)); }); after(function () { + stubFetcherjson.restore(); stub.restore(); }); @@ -121,9 +130,9 @@ describe('VectorTilesSource', function () { }, }); source.whenReady.then(() => { - assert.equal(source.url.size, 1); + assert.equal(source.urls.length, 1); // eslint-disable-next-line no-template-curly-in-string - assert.ok(source.url.has('http://server.geo/${z}/${x}/${y}.pbf')); + assert.ok(source.urls.includes('http://server.geo/${z}/${x}/${y}.pbf')); done(); }) .catch(done); @@ -148,7 +157,7 @@ describe('VectorTilesSource', function () { source.whenReady .then(() => { assert.equal(source.url, '.'); - assert.equal(source.urls.size, 1); + assert.equal(source.urls.length, 1); done(); }) .catch(done); @@ -194,7 +203,6 @@ describe('VectorTilesSource', function () { it('loads the style from a file', function _it(done) { const source = new VectorTilesSource({ style: 'test/data/vectortiles/style.json', - networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {}, }); source.whenReady .then(() => { @@ -301,7 +309,7 @@ describe('VectorTilesSource', function () { }, source2: { type: 'vector', - tiles: ['http://server.geo2/{z}/{x}/{y}.pbf'], + tiles: ['http://server2.geo/{z}/{x}/{y}.pbf'], }, }, layers: [], @@ -309,11 +317,11 @@ describe('VectorTilesSource', function () { }); source.whenReady .then(() => { - assert.equal(source.url.size, 2); + assert.equal(source.urls.length, 2); // eslint-disable-next-line no-template-curly-in-string - assert.ok(source.url.has('http://server.geo/${z}/${x}/${y}.pbf')); + assert.ok(source.urls.includes('http://server.geo/${z}/${x}/${y}.pbf')); // eslint-disable-next-line no-template-curly-in-string - assert.ok(source.url.has('http://server.geo2/${z}/${x}/${y}.pbf')); + assert.ok(source.urls.includes('http://server2.geo/${z}/${x}/${y}.pbf')); done(); }) .catch(done); @@ -336,12 +344,55 @@ describe('VectorTilesSource', function () { }); source.whenReady .then(() => { - assert.equal(source.url.size, 1); + assert.equal(source.urls.length, 1); // eslint-disable-next-line no-template-curly-in-string - assert.ok(source.url.has('http://server.geo/${z}/${x}/${y}.pbf')); + assert.ok(source.urls.includes('http://server.geo/${z}/${x}/${y}.pbf')); done(); }) .catch(done); }); }); + + describe('loadData', function () { + it('with multisource', (done) => { + const source = new VectorTilesSource({ + style: { + sources: { + source1: { + type: 'vector', + tiles: ['http://server.geo/{z}/{x}/{y}.pbf'], + }, + source2: { + type: 'vector', + tiles: ['http://server2.geo/{z}/{x}/{y}.pbf'], + }, + }, + layers: [{ + id: 'geojson', + source: 'source1', + 'source-layer': 'geojson', + type: 'fill', + paint: { + 'fill-color': 'red', + }, + }], + }, + }); + + source.whenReady + .then(() => { + source.onLayerAdded({ out: { crs: 'EPSG:4326' } }); + const extent = new Extent('TMS', 1, 1, 1); + source.loadData(extent, { crs: 'EPSG:4326' }) + .then((featureCollection) => { + assert.equal(featureCollection.features[0].vertices.length, 20); + done(); + }) + .catch((err) => { + done(err); + }); + }) + .catch(done); + }); + }); }); From 23e3c1d8189622c143a6a9c9dc8c503e7cea2532 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Fri, 15 Mar 2024 16:06:26 +0100 Subject: [PATCH 3/4] refactor(Fetcher): supp extent in parsed file --- src/Parser/GeoJsonParser.js | 2 +- src/Parser/VectorTileParser.js | 10 +++---- src/Renderer/RasterTile.js | 2 +- src/Source/OrientedImageSource.js | 1 - src/Source/Source.js | 26 +++++++++---------- src/Source/TMSSource.js | 1 - src/Source/VectorTilesSource.js | 40 ++++++++++------------------ test/unit/dataSourceProvider.js | 26 +++++++++---------- test/unit/vectortiles.js | 43 ++++++++++++------------------- 9 files changed, 61 insertions(+), 90 deletions(-) diff --git a/src/Parser/GeoJsonParser.js b/src/Parser/GeoJsonParser.js index 41905f4dc2..3c31645c5b 100644 --- a/src/Parser/GeoJsonParser.js +++ b/src/Parser/GeoJsonParser.js @@ -217,7 +217,7 @@ export default { if (out.filteringExtent) { if (typeof out.filteringExtent == 'boolean') { - out.filterExtent = json.extent.as(_in.crs); + out.filterExtent = options.extent.as(_in.crs); } else if (out.filteringExtent.isExtent) { out.filterExtent = out.filteringExtent; } diff --git a/src/Parser/VectorTileParser.js b/src/Parser/VectorTileParser.js index 5dc7986602..0a54d64b79 100644 --- a/src/Parser/VectorTileParser.js +++ b/src/Parser/VectorTileParser.js @@ -123,12 +123,12 @@ function readPBF(file, options) { } // x,y,z tile coordinates - const x = file.extent.col; - const z = file.extent.zoom; + const x = options.extent.col; + const z = options.extent.zoom; // We need to move from TMS to Google/Bing/OSM coordinates // https://alastaira.wordpress.com/2011/07/06/converting-tms-tile-coordinates-to-googlebingosm-tile-coordinates/ // Only if the layer.origin is top - const y = options.in.isInverted ? file.extent.row : (1 << z) - file.extent.row - 1; + const y = options.in.isInverted ? options.extent.row : (1 << z) - options.extent.row - 1; const collection = new FeatureCollection(options.out); @@ -148,7 +148,7 @@ function readPBF(file, options) { for (let i = sourceLayer.length - 1; i >= 0; i--) { const vtFeature = sourceLayer.feature(i); - vtFeature.tileNumbers = { x, y: file.extent.row, z }; + vtFeature.tileNumbers = { x, y: options.extent.row, z }; const layers = options.in.layers[layer_id].filter(l => l.filterExpression.filter({ zoom: z }, vtFeature) && z >= l.zoom.min && z < l.zoom.max); let feature; @@ -174,7 +174,7 @@ function readPBF(file, options) { collection.features.sort((a, b) => a.order - b.order); // TODO verify if is needed to updateExtent for previous features. collection.updateExtent(); - collection.extent = file.extent; + collection.extent = options.extent; collection.isInverted = options.in.isInverted; return Promise.resolve(collection); } diff --git a/src/Renderer/RasterTile.js b/src/Renderer/RasterTile.js index 0aa6a35572..b652d122be 100644 --- a/src/Renderer/RasterTile.js +++ b/src/Renderer/RasterTile.js @@ -99,7 +99,7 @@ class RasterTile extends THREE.EventDispatcher { } setTexture(index, texture, offsetScale) { - this.level = (texture && (index == 0)) ? texture.extent.zoom : this.level; + this.level = (texture && texture.extent && (index == 0)) ? texture.extent.zoom : this.level; this.textures[index] = texture || null; this.offsetScales[index] = offsetScale; this.material.layersNeedUpdate = true; diff --git a/src/Source/OrientedImageSource.js b/src/Source/OrientedImageSource.js index 06addadee9..77385a117c 100644 --- a/src/Source/OrientedImageSource.js +++ b/src/Source/OrientedImageSource.js @@ -17,7 +17,6 @@ class OrientedImageSource extends Source { * to find the good texture for each camera for each panoramic. */ constructor(source) { - source.format = source.format || 'json'; super(source); this.isOrientedImageSource = true; diff --git a/src/Source/Source.js b/src/Source/Source.js index c8183e0ca6..133b4f91ed 100644 --- a/src/Source/Source.js +++ b/src/Source/Source.js @@ -11,7 +11,6 @@ import Cache from 'Core/Scheduler/Cache'; import CRS from 'Core/Geographic/Crs'; export const supportedFetchers = new Map([ - ['image/x-bil;bits=32', Fetcher.textureFloat], ['geojson', Fetcher.json], ['application/json', Fetcher.json], ['application/kml', Fetcher.xml], @@ -20,6 +19,10 @@ export const supportedFetchers = new Map([ ['application/gtx', Fetcher.arrayBuffer], ['application/isg', Fetcher.text], ['application/gdf', Fetcher.text], + ['image/x-bil;bits=32', Fetcher.textureFloat], + ['image/jpeg', Fetcher.texture], + ['image/png', Fetcher.texture], + [undefined, Fetcher.texture], ]); export const supportedParsers = new Map([ @@ -66,14 +69,6 @@ class InformationsData { // eslint-disable-next-line class /* istanbul ignore next */ ParsingOptions {} -function fetchSourceData(source, extent) { - const url = source.urlFromExtent(extent); - return source.fetcher(url, source.networkOptions).then((f) => { - f.extent = extent; - return f; - }, err => source.handlingError(err)); -} - let uid = 0; /** @@ -139,8 +134,8 @@ class Source extends InformationsData { this.url = source.url; this.format = source.format; - this.fetcher = source.fetcher || supportedFetchers.get(source.format) || Fetcher.texture; - this.parser = source.parser || supportedParsers.get(source.format) || (d => d); + this.fetcher = source.fetcher || supportedFetchers.get(source.format); + this.parser = source.parser || supportedParsers.get(source.format) || ((d, opt) => { d.extent = opt.extent; return d; }); this.isVectorSource = (source.parser || supportedParsers.get(source.format)) != undefined; this.networkOptions = source.networkOptions || { crossOrigin: 'anonymous' }; this.attribution = source.attribution; @@ -189,9 +184,12 @@ class Source extends InformationsData { let features = cache.getByArray(key); if (!features) { // otherwise fetch/parse the data - features = cache.setByArray(fetchSourceData(this, extent) - .then(file => this.parser(file, { out, in: this }), - err => this.handlingError(err)), key); + features = cache.setByArray( + this.fetcher(this.urlFromExtent(extent), this.networkOptions) + .then(file => this.parser(file, { out, in: this, extent })) + .catch(err => this.handlingError(err)), + key); + /* istanbul ignore next */ if (this.onParsedFile) { features.then((feat) => { diff --git a/src/Source/TMSSource.js b/src/Source/TMSSource.js index b86301f714..3604ccb21b 100644 --- a/src/Source/TMSSource.js +++ b/src/Source/TMSSource.js @@ -96,7 +96,6 @@ class TMSSource extends Source { this.zoom = source.zoom; this.isInverted = source.isInverted || false; - this.url = source.url; this.crs = CRS.formatToTms(source.crs); this.tileMatrixSetLimits = source.tileMatrixSetLimits; this.extentSetlimits = {}; diff --git a/src/Source/VectorTilesSource.js b/src/Source/VectorTilesSource.js index 979f42e829..601bbc9d6e 100644 --- a/src/Source/VectorTilesSource.js +++ b/src/Source/VectorTilesSource.js @@ -9,11 +9,6 @@ function toTMSUrl(url) { return url.replace(/\{/g, '${'); } -function fetchSourceData(source, url) { - return source.fetcher(url, source.networkOptions) - .then(f => f, err => source.handlingError(err)); -} - function mergeCollections(collections) { const collection = collections[0]; collections.forEach((col, index) => { @@ -70,6 +65,7 @@ class VectorTilesSource extends TMSSource { source.url = source.url || '.'; super(source); const ffilter = source.filter || (() => true); + this.urls = []; this.layers = {}; this.styles = {}; let promise; @@ -127,16 +123,16 @@ class VectorTilesSource extends TMSSource { }); if (this.url == '.') { - const TMSUrlList = Object.values(style.sources).map((source) => { - if (source.url) { - const urlSource = urlParser.normalizeSourceURL(source.url, this.accessToken); + const TMSUrlList = Object.values(style.sources).map((sourceVT) => { + if (sourceVT.url) { + const urlSource = urlParser.normalizeSourceURL(sourceVT.url, this.accessToken); return Fetcher.json(urlSource, this.networkOptions).then((tileJSON) => { if (tileJSON.tiles[0]) { return toTMSUrl(tileJSON.tiles[0]); } }); - } else if (source.tiles) { - return Promise.resolve(toTMSUrl(source.tiles[0])); + } else if (sourceVT.tiles) { + return Promise.resolve(toTMSUrl(sourceVT.tiles[0])); } return Promise.reject(); }); @@ -148,14 +144,8 @@ class VectorTilesSource extends TMSSource { }); } - urlFromExtent(extent) { - return this.url.map((url) => { - const options = { - tileMatrixCallback: this.tileMatrixCallback, - url, - }; - return URLBuilder.xyz(extent, options); - }); + urlFromExtent(extent, url) { + return URLBuilder.xyz(extent, { tileMatrixCallback: this.tileMatrixCallback, url }); } onLayerAdded(options) { @@ -176,14 +166,12 @@ class VectorTilesSource extends TMSSource { if (!features) { // otherwise fetch/parse the data features = cache.setByArray( - Promise.all(this.urlFromExtent(extent).map(url => - fetchSourceData(this, url) - .then((file) => { - file.extent = extent; - return this.parser(file, { out, in: this }); - }), - )).then(collections => mergeCollections(collections), - err => this.handlingError(err)), key); + Promise.all(this.urls.map(url => + this.fetcher(this.urlFromExtent(extent, url), this.networkOptions) + .then(file => this.parser(file, { out, in: this, extent })))) + .then(collections => mergeCollections(collections)) + .catch(err => this.handlingError(err)), + key); /* istanbul ignore next */ if (this.onParsedFile) { diff --git a/test/unit/dataSourceProvider.js b/test/unit/dataSourceProvider.js index e43b216295..cec13cee3d 100644 --- a/test/unit/dataSourceProvider.js +++ b/test/unit/dataSourceProvider.js @@ -151,9 +151,8 @@ describe('Provide in Sources', function () { updateLayeredMaterialNodeImagery(context, colorlayer, tile, tile.parent); DataSourceProvider.executeCommand(context.scheduler.commands[0]) .then((textures) => { - assert.equal(textures[0].extent.zoom, zoom); - assert.equal(textures[0].extent.row, 511); - assert.equal(textures[0].extent.col, 512); + assert.equal(textures.length, 1); + assert.equal(textures[0].isTexture, true); done(); }).catch(done); }); @@ -181,12 +180,12 @@ describe('Provide in Sources', function () { updateLayeredMaterialNodeElevation(context, elevationlayer, tile, tile.parent); DataSourceProvider.executeCommand(context.scheduler.commands[0]) .then((textures) => { - assert.equal(textures[0].extent.zoom, zoom); - assert.equal(textures[0].extent.row, 511); - assert.equal(textures[0].extent.col, 512); + assert.equal(textures.length, 1); + assert.equal(textures[0].isTexture, true); done(); }).catch(done); }); + it('should get wms texture with DataSourceProvider', (done) => { colorlayer.source = new WMSSource({ url: 'http://', @@ -211,15 +210,12 @@ describe('Provide in Sources', function () { updateLayeredMaterialNodeImagery(context, colorlayer, tile, tile.parent); DataSourceProvider.executeCommand(context.scheduler.commands[0]) .then((textures) => { - const e = textures[0].extent.as(tile.extent.crs); - assert.equal(e.zoom, zoom); - assert.equal(e.west, tile.extent.west); - assert.equal(e.east, tile.extent.east); - assert.equal(e.north, tile.extent.north); - assert.equal(e.south, tile.extent.south); + assert.equal(textures.length, 1); + assert.equal(textures[0].isTexture, true); done(); }).catch(done); }); + it('should get 4 TileMesh from TileProvider', (done) => { const tile = new TileMesh(geom, material, planarlayer, extent, zoom); material.visible = true; @@ -237,6 +233,7 @@ describe('Provide in Sources', function () { done(); }).catch(done); }); + it('should get 3 meshs with WFS source and DataSourceProvider', (done) => { const tile = new TileMesh(geom, material, planarlayer, extent, featureLayer.zoom.min); material.visible = true; @@ -280,6 +277,7 @@ describe('Provide in Sources', function () { done(); }).catch(done); }); + it('should get 1 texture with WFS source and DataSourceProvider', (done) => { const tile = new TileMesh( geom, @@ -345,9 +343,9 @@ describe('Provide in Sources', function () { .then((result) => { tile.material.setSequence([colorlayer.id]); tile.material.getLayer(colorlayer.id).setTextures(result, [new THREE.Vector4()]); - assert.equal(tile.material.uniforms.colorTextures.value[0].extent, undefined); + assert.equal(tile.material.uniforms.colorTextures.value[0].anisotropy, 1); tile.material.updateLayersUniforms(); - assert.equal(tile.material.uniforms.colorTextures.value[0].extent.zoom, 10); + assert.equal(tile.material.uniforms.colorTextures.value[0].anisotropy, 16); done(); }).catch(done); }); diff --git a/test/unit/vectortiles.js b/test/unit/vectortiles.js index 5a55db0272..9b5b119847 100644 --- a/test/unit/vectortiles.js +++ b/test/unit/vectortiles.js @@ -20,35 +20,24 @@ const resources = { 'https://api.mapbox.com/v4/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v7.json': mapboxStyle, }; -function parse(pbf, layers) { - return VectorTileParser.parse(pbf, { - in: { - layers, - styles: [[]], - }, - out: { - crs: 'EPSG:3857', - }, - }); -}; - describe('Vector tiles', function () { - let stub; - let multipolygon; - - before(function () { - // this PBF file comes from https://github.com/mapbox/vector-tile-js - // it contains two square polygons - multipolygon = fs.readFileSync('test/data/pbf/multipolygon.pbf'); - multipolygon.extent = new Extent('TMS', 1, 1, 1); - - stub = sinon.stub(Fetcher, 'json') - .callsFake(url => Promise.resolve(JSON.parse(resources[url]))); - }); + // this PBF file comes from https://github.com/mapbox/vector-tile-js + // it contains two square polygons + const multipolygon = fs.readFileSync('test/data/pbf/multipolygon.pbf'); + const extent = new Extent('TMS', 1, 1, 1); - after(function () { - stub.restore(); - }); + function parse(pbf, layers) { + return VectorTileParser.parse(pbf, { + in: { + layers, + styles: [[]], + }, + out: { + crs: 'EPSG:3857', + }, + extent, + }); + } it('returns two squares', (done) => { parse(multipolygon, { From 5662290f53509f856cf480fef7e7baabad40c257 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 19 Mar 2024 11:27:17 +0100 Subject: [PATCH 4/4] refactor(source): supp supportedFetchers and add Fetcher.get(format) --- src/Provider/Fetcher.js | 31 +++++++++++++++++++++++++++++++ src/Source/Source.js | 19 ++----------------- test/unit/dataSourceProvider.js | 19 +++++++++---------- test/unit/demutils.js | 12 +++++------- test/unit/featuregeometrylayer.js | 11 +++++------ test/unit/geoidlayer.js | 11 +++++------ test/unit/source.js | 11 +++++------ test/unit/vectortiles.js | 18 +++++++----------- 8 files changed, 69 insertions(+), 63 deletions(-) diff --git a/src/Provider/Fetcher.js b/src/Provider/Fetcher.js index 227ef11750..9e483ac977 100644 --- a/src/Provider/Fetcher.js +++ b/src/Provider/Fetcher.js @@ -205,4 +205,35 @@ export default { return Promise.resolve(all); }); }, + + get(format = '') { + const [type, subtype] = format.split('/'); + switch (type) { + case 'application': + switch (subtype) { + case 'geo+json': + case 'json': + return this.json; + case 'kml': + case 'gpx': + return this.xml; + case 'x-protobuf;type=mapbox-vector': + case 'gtx': + return this.arrayBuffer; + case 'isg': + case 'gdf': + default: + return this.text; + } + case 'image': + switch (subtype) { + case 'x-bil;bits=32': + return this.textureFloat; + default: + return this.texture; + } + default: + return this.texture; + } + }, }; diff --git a/src/Source/Source.js b/src/Source/Source.js index 133b4f91ed..21fe5ca5d5 100644 --- a/src/Source/Source.js +++ b/src/Source/Source.js @@ -10,23 +10,8 @@ import Fetcher from 'Provider/Fetcher'; import Cache from 'Core/Scheduler/Cache'; import CRS from 'Core/Geographic/Crs'; -export const supportedFetchers = new Map([ - ['geojson', Fetcher.json], - ['application/json', Fetcher.json], - ['application/kml', Fetcher.xml], - ['application/gpx', Fetcher.xml], - ['application/x-protobuf;type=mapbox-vector', Fetcher.arrayBuffer], - ['application/gtx', Fetcher.arrayBuffer], - ['application/isg', Fetcher.text], - ['application/gdf', Fetcher.text], - ['image/x-bil;bits=32', Fetcher.textureFloat], - ['image/jpeg', Fetcher.texture], - ['image/png', Fetcher.texture], - [undefined, Fetcher.texture], -]); - export const supportedParsers = new Map([ - ['geojson', GeoJsonParser.parse], + ['application/geo+json', GeoJsonParser.parse], ['application/json', GeoJsonParser.parse], ['application/kml', KMLParser.parse], ['application/gpx', GpxParser.parse], @@ -134,7 +119,7 @@ class Source extends InformationsData { this.url = source.url; this.format = source.format; - this.fetcher = source.fetcher || supportedFetchers.get(source.format); + this.fetcher = source.fetcher || Fetcher.get(source.format); this.parser = source.parser || supportedParsers.get(source.format) || ((d, opt) => { d.extent = opt.extent; return d; }); this.isVectorSource = (source.parser || supportedParsers.get(source.format)) != undefined; this.networkOptions = source.networkOptions || { crossOrigin: 'anonymous' }; diff --git a/test/unit/dataSourceProvider.js b/test/unit/dataSourceProvider.js index cec13cee3d..59562924da 100644 --- a/test/unit/dataSourceProvider.js +++ b/test/unit/dataSourceProvider.js @@ -6,7 +6,7 @@ import TileMesh from 'Core/TileMesh'; import Extent, { globalExtentTMS } from 'Core/Geographic/Extent'; import OBB from 'Renderer/OBB'; import DataSourceProvider from 'Provider/DataSourceProvider'; -import { supportedFetchers } from 'Source/Source'; +import Fetcher from 'Provider/Fetcher'; import TileProvider from 'Provider/TileProvider'; import WMTSSource from 'Source/WMTSSource'; import WMSSource from 'Source/WMSSource'; @@ -24,11 +24,6 @@ import sinon from 'sinon'; import holes from '../data/geojson/holesPoints.geojson'; -const stubSupportedFetchers = new Map([ - ['application/json', () => Promise.resolve(JSON.parse(holes))], - ['image/png', () => Promise.resolve(new THREE.Texture())], -]); - describe('Provide in Sources', function () { // TODO We should mock the creation of all layers creation. @@ -41,7 +36,8 @@ describe('Provide in Sources', function () { const sizeTile = globalExtent.planarDimensions().x / 2 ** zoom; const extent = new Extent('EPSG:3857', 0, sizeTile, 0, sizeTile); - let stub; + let stubFetcherJson; + let stubFetcherTexture; let planarlayer; let elevationlayer; let colorlayer; @@ -66,8 +62,10 @@ describe('Provide in Sources', function () { }; before(function () { - stub = sinon.stub(supportedFetchers, 'get') - .callsFake(format => stubSupportedFetchers.get(format)); + stubFetcherJson = sinon.stub(Fetcher, 'json') + .callsFake(() => Promise.resolve(JSON.parse(holes))); + stubFetcherTexture = sinon.stub(Fetcher, 'texture') + .callsFake(() => Promise.resolve(new THREE.Texture())); planarlayer = new PlanarLayer('globe', globalExtent, new THREE.Group()); colorlayer = new ColorLayer('color', { crs: 'EPSG:3857', source: false }); @@ -117,7 +115,8 @@ describe('Provide in Sources', function () { }); after(function () { - stub.restore(); + stubFetcherJson.restore(); + stubFetcherTexture.restore(); }); diff --git a/test/unit/demutils.js b/test/unit/demutils.js index cfa5193688..d48e36187a 100644 --- a/test/unit/demutils.js +++ b/test/unit/demutils.js @@ -1,7 +1,7 @@ import * as THREE from 'three'; import ElevationLayer from 'Layer/ElevationLayer'; import WMTSSource from 'Source/WMTSSource'; -import { supportedFetchers } from 'Source/Source'; +import Fetcher from 'Provider/Fetcher'; import assert from 'assert'; import GlobeView from 'Core/Prefab/GlobeView'; import Coordinates from 'Core/Geographic/Coordinates'; @@ -28,19 +28,17 @@ describe('DemUtils', function () { let elevationlayer; let context; - let stubSuppFetcher; + let stubFetcherTextFloat; const ELEVATION = 300; before(function () { - stubSuppFetcher = sinon.stub(supportedFetchers, 'get'); - stubSuppFetcher.withArgs('image/x-bil;bits=32') + stubFetcherTextFloat = sinon.stub(Fetcher, 'textureFloat') .callsFake(() => { const floatArray = createBilData(ELEVATION); - const texture = new THREE.DataTexture(floatArray, 256, 256, THREE.RedFormat, THREE.FloatType); texture.internalFormat = 'R32F'; texture.needsUpdate = false; - return () => Promise.resolve(texture); + return Promise.resolve(texture); }); const source = new WMTSSource({ @@ -67,7 +65,7 @@ describe('DemUtils', function () { }); after(() => { - stubSuppFetcher.restore(); + stubFetcherTextFloat.restore(); }); it('add elevation layer', (done) => { diff --git a/test/unit/featuregeometrylayer.js b/test/unit/featuregeometrylayer.js index 062f1ae9f8..84e23dd8e4 100644 --- a/test/unit/featuregeometrylayer.js +++ b/test/unit/featuregeometrylayer.js @@ -3,7 +3,7 @@ import assert from 'assert'; import GlobeView from 'Core/Prefab/GlobeView'; import FeatureGeometryLayer from 'Layer/FeatureGeometryLayer'; import FileSource from 'Source/FileSource'; -import { supportedFetchers } from 'Source/Source'; +import Fetcher from 'Provider/Fetcher'; import Extent from 'Core/Geographic/Extent'; import Coordinates from 'Core/Geographic/Coordinates'; import OBB from 'Renderer/OBB'; @@ -22,12 +22,11 @@ describe('Layer with Feature process', function () { let ariegeNoProj4; let tile; let context; - let stubSuppFetcher; + let stubFetcherJson; before(function () { - stubSuppFetcher = sinon.stub(supportedFetchers, 'get'); - stubSuppFetcher.withArgs('application/json') - .callsFake(() => () => Promise.resolve(JSON.parse(feature))); + stubFetcherJson = sinon.stub(Fetcher, 'json') + .callsFake(() => Promise.resolve(JSON.parse(feature))); const source = new FileSource({ url: 'https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/departements/09-ariege/departement-09-ariege.geojson', @@ -75,7 +74,7 @@ describe('Layer with Feature process', function () { }); after(function () { - stubSuppFetcher.restore(); + stubFetcherJson.restore(); }); it('add layer', function (done) { diff --git a/test/unit/geoidlayer.js b/test/unit/geoidlayer.js index ad76fca779..687a8553b5 100644 --- a/test/unit/geoidlayer.js +++ b/test/unit/geoidlayer.js @@ -3,7 +3,7 @@ import * as THREE from 'three'; import assert from 'assert'; import GeoidLayer from 'Layer/GeoidLayer'; import FileSource from 'Source/FileSource'; -import { supportedFetchers } from 'Source/Source'; +import Fetcher from 'Provider/Fetcher'; import Coordinates from 'Core/Geographic/Coordinates'; import GlobeView from 'Core/Prefab/GlobeView'; import Extent from 'Core/Geographic/Extent'; @@ -22,13 +22,12 @@ describe('GlobeView', function () { let geoidLayer; let context; let tile; - let stubSuppFetcher; + let stubFetcherArrayBuf; before(function () { const buffer = createGtxBuffer(ELEVATION); - stubSuppFetcher = sinon.stub(supportedFetchers, 'get'); - stubSuppFetcher.withArgs('application/gtx') - .callsFake(() => () => Promise.resolve(buffer)); + stubFetcherArrayBuf = sinon.stub(Fetcher, 'arrayBuffer') + .callsFake(() => Promise.resolve(buffer)); const url = 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/' + 'altitude-conversion-grids/RAF20_float.gtx'; @@ -57,7 +56,7 @@ describe('GlobeView', function () { }); after(function () { - stubSuppFetcher.restore(); + stubFetcherArrayBuf.restore(); }); it('add geoid layer', function (done) { diff --git a/test/unit/source.js b/test/unit/source.js index 50a1208315..dee8cb5325 100644 --- a/test/unit/source.js +++ b/test/unit/source.js @@ -1,6 +1,6 @@ import { Matrix4 } from 'three'; import assert from 'assert'; -import Source, { supportedFetchers } from 'Source/Source'; +import Source from 'Source/Source'; import Layer from 'Layer/Layer'; import WFSSource from 'Source/WFSSource'; import WMTSSource from 'Source/WMTSSource'; @@ -213,15 +213,14 @@ describe('Sources', function () { let fetchedData; describe('FileSource', function () { - let stubSuppFetcher; + let stubFetcherJson; before(function () { - stubSuppFetcher = sinon.stub(supportedFetchers, 'get'); - stubSuppFetcher.withArgs('application/json') - .callsFake(() => () => Promise.resolve(JSON.parse(fileSource))); + stubFetcherJson = sinon.stub(Fetcher, 'json') + .callsFake(() => Promise.resolve(JSON.parse(fileSource))); }); after(function () { - stubSuppFetcher.restore(); + stubFetcherJson.restore(); }); it('should instance FileSource with no source.fetchedData', function _it(done) { diff --git a/test/unit/vectortiles.js b/test/unit/vectortiles.js index 9b5b119847..46912bd648 100644 --- a/test/unit/vectortiles.js +++ b/test/unit/vectortiles.js @@ -4,7 +4,6 @@ import VectorTileParser from 'Parser/VectorTileParser'; import VectorTilesSource from 'Source/VectorTilesSource'; import Extent from 'Core/Geographic/Extent'; import urlParser from 'Parser/MapBoxUrlParser'; -import { supportedFetchers } from 'Source/Source'; import Fetcher from 'Provider/Fetcher'; import sinon from 'sinon'; @@ -84,24 +83,21 @@ describe('Vector tiles', function () { }); describe('VectorTilesSource', function () { - let stubFetcherjson; - let stub; + let stubFetcherJson; + let stubFetcherArrayBuf; before(function () { - stubFetcherjson = sinon.stub(Fetcher, 'json') + stubFetcherJson = sinon.stub(Fetcher, 'json') .callsFake((url) => { url = url.split('?')[0]; return Promise.resolve(JSON.parse(resources[url])); }); const multipolygon = fs.readFileSync('test/data/pbf/multipolygon.pbf'); - const stubSupportedFetchers = new Map([ - ['application/x-protobuf;type=mapbox-vector', () => Promise.resolve(multipolygon)], - ]); - stub = sinon.stub(supportedFetchers, 'get') - .callsFake(format => stubSupportedFetchers.get(format)); + stubFetcherArrayBuf = sinon.stub(Fetcher, 'arrayBuffer') + .callsFake(() => Promise.resolve(multipolygon)); }); after(function () { - stubFetcherjson.restore(); - stub.restore(); + stubFetcherJson.restore(); + stubFetcherArrayBuf.restore(); }); it('throws an error because no style was provided', () => {