Repository: iTowns/itowns (JavaScript)

Issue:
Mapbox Style with multiple sources not being rendered correctly
Hi,
    I'm trying to use a Mapbox vector style that is working fine for a 2D map using Openalyers + ol-maplibre-layer.
    Using the same json file as style leads to only 1 layer being shown.

<!--- Provide a general summary of the issue in the Title above -->

## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used: 2.38.2
* Browser Name and version: chrome 106.0.5249.119
* Operating System and version (desktop or mobile): Linux Desktop
* Link to your project: 

## Context
<!--- For bugs, Provide a more detailed introduction to the issue itself, and why you consider it to be a bug -->
  The style json file has multiple sources as below :-
```
  "sources": {
    "physical": {
      "type": "vector",
      "tiles": [
        "http://localhost:8000/services/physical/tiles/{z}/{x}/{y}.pbf"
      ],
      "minZoom": 2,
      "maxZoom": 14
    },
    "land": {
      "type": "vector",
      "tiles": [
        "http://localhost:8000/services/land/tiles/{z}/{x}/{y}.pbf"
      ],
      "minZoom": 2,
      "maxZoom": 14
    }
  }
  "layers":[
    { 
      {
      "id": "marine",
      "type": "fill",
      "source": "physical",
      "source-layer": "marine",
     },
   },
   {
      "id": "land",
      "type": "fill",
      "source": "land",
      "source-layer": "land",
      "paint": {
        "fill-color": "#ccffff",
        "fill-antialias": true
      }
    },
 ]
```

<!--- How has this bug affected you? -->
<!--- If you have errors or a stacktrace in the console, here is a good place to put it. -->
<!--- Please consider using a pastebin service or create a gist.-->
<!--- Some screenshots are always greatly appreciated here! -->

<!--- For improvement suggestions, please tell us what you are trying to accomplish -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->

## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.  Use a mapbox style with multiple sources 

## Expected Behavior
<!--- If you're describing a bug, tell us what you think should happen -->
All the layers referencing all the sources should be displayed.
<!--- If you're suggesting a change/improvement, tell us how it should work -->

## Actual Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
Only the first layer referencing the first source is displayed
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
All the layers referencing all the sources should be displayed.

## Possible Cause/Fix/Solution
<!--- Not obligatory, but suggest a cause or reason for the bug if relevant, -->
<!--- or ideas how to implement the addition or change -->
<!--- In any case, please consider opening a PR and become an iTowns developer! -->




Discussion: Hi, and thanks for this feedback !

Indeed, iTowns `VectorTileSource` only considers the first `source` specified in Mapbox vector style. This is partly due to [this piece of code](https://github.com/iTowns/itowns/blob/9ee991c3cd6595de6c058cd2cf81976a3daab7e0/src/Source/VectorTilesSource.js#L90). 

The issue is that `VectorTileSource` has a single `url` property, therefore referencing a single `source` and not an array of `sources` for example. Implementing the support for Mapbox style with multiple sources would require some change on this, and also on fetching and parsing methods used for Mapbox data.

@PhilippeMarc-IGN and @LoicUTC, maybe we could add this feature to our next releases plannings ?
Hi ! I am currently working on this issue.

Can you send me an example of data that works ? If the data is private, you can send it to me by mail if you want at aymeric.dutremble@gmail.com.



Fix (patch):
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/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/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 cb6b0f33aa..21fe5ca5d5 100644
--- a/src/Source/Source.js
+++ b/src/Source/Source.js
@@ -10,20 +10,8 @@ import Fetcher from 'Provider/Fetcher';
 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],
-    ['application/gpx', Fetcher.xml],
-    ['application/x-protobuf;type=mapbox-vector', Fetcher.arrayBuffer],
-    ['application/gtx', Fetcher.arrayBuffer],
-    ['application/isg', Fetcher.text],
-    ['application/gdf', Fetcher.text],
-]);
-
 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],
@@ -66,15 +54,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;
 
 /**
@@ -140,8 +119,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 || 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' };
         this.attribution = source.attribution;
@@ -190,8 +169,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 9d2f69192d..601bbc9d6e 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,17 @@ function toTMSUrl(url) {
     return url.replace(/\{/g, '${');
 }
 
+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
@@ -53,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;
@@ -86,9 +99,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,20 +123,31 @@ 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((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 (sourceVT.tiles) {
+                        return Promise.resolve(toTMSUrl(sourceVT.tiles[0]));
+                    }
+                    return Promise.reject();
+                });
+                return Promise.all(TMSUrlList);
             }
+            return (Promise.resolve([this.url]));
+        }).then((TMSUrlList) => {
+            this.urls = Array.from(new Set(TMSUrlList));
         });
     }
 
+    urlFromExtent(extent, url) {
+        return URLBuilder.xyz(extent, { tileMatrixCallback: this.tileMatrixCallback, url });
+    }
+
     onLayerAdded(options) {
         super.onLayerAdded(options);
         if (options.out.style) {
@@ -136,6 +157,33 @@ 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.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) {
+                features.then((feat) => {
+                    this.onParsedFile(feat);
+                    console.warn('Source.onParsedFile was deprecated');
+                    return feat;
+                });
+            }
+        }
+        return features;
+    }
 }
 
 export default VectorTilesSource;