• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

iTowns / itowns / 17292947856

28 Aug 2025 10:21AM UTC coverage: 86.774% (-0.2%) from 86.927%
17292947856

Pull #2593

github

web-flow
Merge 3396d4fd1 into 737a8f3de
Pull Request #2593: Refactoring sources

2792 of 3749 branches covered (74.47%)

Branch coverage included in aggregate %.

459 of 531 new or added lines in 18 files covered. (86.44%)

10 existing lines in 5 files now uncovered.

26155 of 29610 relevant lines covered (88.33%)

1094.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.12
/packages/Main/src/Source/FileSource.js
1
import Source from 'Source/Source';
1✔
2
import { LRUCache } from 'lru-cache';
1✔
3
import Fetcher from 'Provider/Fetcher';
1✔
4
import GeoJsonParser from 'Parser/GeoJsonParser';
1✔
5
import KMLParser from 'Parser/KMLParser';
1✔
6
import GpxParser from 'Parser/GpxParser';
1✔
7
import VectorTileParser from 'Parser/VectorTileParser';
1✔
8
import GTXParser from 'Parser/GTXParser';
1✔
9
import ISGParser from 'Parser/ISGParser';
1✔
10
import GDFParser from 'Parser/GDFParser';
1✔
11

1✔
12
const supportedParsers = new Map([
1✔
13
    ['application/geo+json', GeoJsonParser.parse],
1✔
14
    ['application/json', GeoJsonParser.parse],
1✔
15
    ['application/kml', KMLParser.parse],
1✔
16
    ['application/gpx', GpxParser.parse],
1✔
17
    ['application/x-protobuf;type=mapbox-vector', VectorTileParser.parse],
1✔
18
    ['application/gtx', GTXParser.parse],
1✔
19
    ['application/isg', ISGParser.parse],
1✔
20
    ['application/gdf', GDFParser.parse],
1✔
21
]);
1✔
22

1✔
23
/**
1✔
24
 * An object defining the source of a single resource to get from a direct
1✔
25
 * access. It inherits from {@link Source}. There is multiple ways of adding a
1✔
26
 * resource here:
1✔
27
 * <ul>
1✔
28
 *  <li>add the file like any other sources, using the `url` property.</li>
1✔
29
 *  <li>fetch the file, and give the data to the source using the `fetchedData`
1✔
30
 *  property.</li>
1✔
31
 *  <li>fetch the file, parse it and git the parsed data to the source using the
1✔
32
 *  `features` property.</li>
1✔
33
 * </ul>
1✔
34
 * See the examples below for real use cases.
1✔
35
 *
1✔
36
 * @extends Source
1✔
37
 *
1✔
38
 * @property {boolean} isFileSource - Used to checkout whether this source is a
1✔
39
 * FileSource. Default is true. You should not change this, as it is used
1✔
40
 * internally for optimisation.
1✔
41
 * @property {*} fetchedData - Once the file has been loaded, the resulting data
1✔
42
 * is stored in this property.
1✔
43
 * @property {*} features - Once the file has been loaded and parsed, the
1✔
44
 * resulting data is stored in this property.
1✔
45
 *
1✔
46
 * @example <caption>Simple: create a source, a layer, and let iTowns taking
1✔
47
 * care of everything.</caption>
1✔
48
 * const kmlSource = new itowns.FileSource({
1✔
49
 *     url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/croquis.kml',
1✔
50
 *     crs: 'EPSG:4326',
1✔
51
 *     fetcher: itowns.Fetcher.xml,
1✔
52
 *     parser: itowns.KMLParser.parse,
1✔
53
 * });
1✔
54
 *
1✔
55
 * const kmlLayer = new itowns.ColorLayer('Kml', {
1✔
56
 *     name: 'kml',
1✔
57
 *     transparent: true,
1✔
58
 *     crs: view.tileLayer.extent.crs,
1✔
59
 *     source: kmlSource,
1✔
60
 * });
1✔
61
 *
1✔
62
 * view.addLayer(kmlLayer);
1✔
63
 *
1✔
64
 * @example <caption>Advanced: fetch some data, create a source, a layer, and
1✔
65
 * let iTowns do the parsing and converting.</caption>
1✔
66
 * // Parse and Convert by iTowns
1✔
67
 * itowns.Fetcher.xml('https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/ULTRA2009.gpx')
1✔
68
 *     .then(function _(gpx) {
1✔
69
 *         const gpxSource = new itowns.FileSource({
1✔
70
 *             data: gpx,
1✔
71
 *             crs: 'EPSG:4326',
1✔
72
 *             parser: itowns.GpxParser.parse,
1✔
73
 *         });
1✔
74
 *
1✔
75
 *         const gpxLayer = new itowns.ColorLayer('Gpx', {
1✔
76
 *             name: 'Ultra 2009',
1✔
77
 *             transparent: true,
1✔
78
 *             source: gpxSource,
1✔
79
 *         });
1✔
80
 *
1✔
81
 *         return view.addLayer(gpxLayer);
1✔
82
 *     });
1✔
83
 *
1✔
84
 * @example <caption>More advanced: create a layer, fetch some data, parse the
1✔
85
 * data, append a source to the layer and add the layer to iTowns.</caption>
1✔
86
 * // Create a layer
1✔
87
 * const ariege = new itowns.GeometryLayer('ariege', new itowns.THREE.Group());
1✔
88
 *
1✔
89
 * // Specify update method and conversion
1✔
90
 * ariege.update = itowns.FeatureProcessing.update;
1✔
91
 * ariege.convert = itowns.Feature2Mesh.convert({
1✔
92
 *     color: () => new itowns.THREE.Color(0xffcc00),
1✔
93
 *     extrude: () => 5000,
1✔
94
 * });
1✔
95
 *
1✔
96
 * itowns.Fetcher.json('https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/departements/09-ariege/departement-09-ariege.geojson')
1✔
97
 *     .then(function _(geojson) {
1✔
98
 *         return itowns.GeoJsonParser.parse(geojson, {
1✔
99
 *             in: { crs: 'EPSG:4326' },
1✔
100
 *             out: { crs: view.tileLayer.extent.crs,
1✔
101
 *                      style: new itowns.Style({
1✔
102
 *                          fill: {
1✔
103
 *                              color: new itowns.THREE.Color(0xffcc00),
1✔
104
 *                              extrusion_height: () => 5000,
1✔
105
 *                      }),
1✔
106
 *                  },
1✔
107
 *             },
1✔
108
 *         });
1✔
109
 *     }).then(function _(features) {
1✔
110
 *         ariege.source = new itowns.FileSource({
1✔
111
 *             crs: 'EPSG:4326',
1✔
112
 *             features,
1✔
113
 *         });
1✔
114
 *
1✔
115
 *         return view.addLayer(ariegeLayer);
1✔
116
 *     });
1✔
117
 */
1✔
118
class FileSource extends Source {
1✔
119
    /**
1✔
120
     * @param {Object} source - An object that can contain all properties of a
1✔
121
     * FileSource and {@link Source}. Only `crs` is mandatory, but if it
1✔
122
     * presents in `features` under the property `crs`, it is fine.
1✔
123
     */
1✔
124
    constructor(source) {
1✔
125
        if (source.parsedData) {
16!
126
            console.warn('FileSource parsedData parameter is deprecated, use features instead of.');
×
127
            source.features = source.features || source.parsedData;
×
128
        }
×
129
        if (source.projection) {
16!
130
            console.warn('FileSource projection parameter is deprecated, use crs instead.');
×
131
            source.crs = source.crs || source.projection;
×
132
        }
×
133
        if (!source.crs) {
16✔
134
            if (source.features && source.features.crs) {
2✔
135
                source.crs = source.features.crs;
1✔
136
            } else {
1✔
137
                throw new Error('source.crs is required in FileSource');
1✔
138
            }
1✔
139
        }
2✔
140

15✔
141
        if (!source.url && !source.fetchedData && !source.features) {
16✔
142
            throw new Error(`url, fetchedData and features are not set in
1✔
143
                FileSource; at least one needs to be present`);
1✔
144
        }
1✔
145

14✔
146
        // the fake url is for when we use the fetchedData or features mode
14✔
147
        super(source);
14✔
148
        this.url = source.url || 'none';
16✔
149
        this.networkOptions = source.networkOptions ?? {};
16✔
150

16✔
151
        this.isFileSource = true;
16✔
152

16✔
153
        this.fetchedData = source.fetchedData;
16✔
154
        this.parser = source.parser || supportedParsers.get(source.format) || ((d, opt) => { d.extent = opt.extent; return d; });
16✔
155
        this.isVectorSource = (source.parser || supportedParsers.get(source.format)) != undefined;
16✔
156
        this._featuresCaches = this.isVectorSource ? {} : null;
16✔
157
        if (!this.fetchedData && !source.features) {
16✔
158
            const fetcher = source.fetcher || Fetcher.get(source.format);
3✔
159
            this.whenReady = fetcher(this.urlFromExtent(), this.networkOptions).then((f) => {
3✔
160
                this.fetchedData = f;
3✔
161
            });
3✔
162
        } else if (source.features) {
16✔
163
            this._featuresCaches = {};
3✔
164
            this._featuresCaches[source.features.crs] = new LRUCache({ max: 500 });
3✔
165
            this._featuresCaches[source.features.crs].set(0, Promise.resolve(source.features));
3✔
166
        }
3✔
167

14✔
168
        this.whenReady.then(() => this.fetchedData);
14✔
169

14✔
170
        this.zoom = { min: 0, max: Infinity };
14✔
171
    }
14✔
172

1✔
173
    urlFromExtent() {
1✔
174
        return this.url;
6✔
175
    }
6✔
176

1✔
177
    getDataKey(extent) {
1✔
178
        return `z${extent.zoom}r${extent.row}c${extent.col}`;
10✔
179
    }
10✔
180

1✔
181
    onLayerAdded(options) {
1✔
182
        options.in = this;
18✔
183
        if (this._featuresCaches && !this._featuresCaches[options.out.crs]) {
18✔
184
            // Cache feature only if it's vector data, the feature are cached in source.
10✔
185
            // It's not necessary to cache raster in Source,
10✔
186
            // because it's already cached on layer.
10✔
187
            this._featuresCaches[options.out.crs] = new LRUCache({ max: 500 });
10✔
188
        }
10✔
189
        let features = this._featuresCaches[options.out.crs].get(0);
18✔
190
        if (!features) {
18✔
191
            options.out.buildExtent = this.crs != 'EPSG:4978';
10✔
192
            if (options.out.buildExtent) {
10✔
193
                options.out.forcedExtentCrs = options.out.crs != 'EPSG:4978' ? options.out.crs : this.crs;
10✔
194
            }
10✔
195
            features = this.parser(this.fetchedData, options);
10✔
196
            this._featuresCaches[options.out.crs].set(0, features);
10✔
197
        }
10✔
198
        features.then((data) => {
18✔
199
            if (data.extent) {
18✔
200
                this.extent = data.extent.clone();
14✔
201
                // Transform local extent to data.crs projection.
14✔
202
                if (this.extent.crs == data.crs) {
14✔
203
                    this.extent.applyMatrix4(data.matrixWorld);
8✔
204
                }
8✔
205
            }
14✔
206
        });
18✔
207
    }
18✔
208

1✔
209
    onLayerRemoved(options = {}) {
1✔
NEW
210
        if (!this._featuresCaches) {
×
NEW
211
            return;
×
NEW
212
        }
×
NEW
213

×
NEW
214
        // delete unused cache
×
NEW
215
        const unusedCache = this._featuresCaches[options.unusedCrs];
×
NEW
216
        if (unusedCache) {
×
NEW
217
            unusedCache.clear();
×
NEW
218
            delete this._featuresCaches[options.unusedCrs];
×
NEW
219
        }
×
NEW
220
    }
×
221

1✔
222
    /**
1✔
223
     * load  data from cache or Fetch/Parse data.
1✔
224
     * The loaded data is a Feature or Texture.
1✔
225
     *
1✔
226
     * @param      {Extent}  extent   extent requested parsed data.
1✔
227
     * @param      {FeatureBuildingOptions|Layer}  out  The feature returned options
1✔
228
     * @return     {FeatureCollection|Texture}  The parsed data.
1✔
229
     */
1✔
230
    loadData(extent, out) {
1✔
231
        return this._featuresCaches[out.crs].get(0);
11✔
232
    }
11✔
233

1✔
234
    extentInsideLimit(extent) {
1✔
235
        return this.extent.intersectsExtent(extent);
13✔
236
    }
13✔
237
}
1✔
238

1✔
239
export default FileSource;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc