1 | import {fakeServer, FakeServer} from 'nise';
|
2 | import {Source} from './source';
|
3 | import VectorTileSource from './vector_tile_source';
|
4 | import Tile from './tile';
|
5 | import {OverscaledTileID} from './tile_id';
|
6 | import {Evented} from '../util/evented';
|
7 | import {RequestManager} from '../util/request_manager';
|
8 | import fixturesSource from '../../test/unit/assets/source.json';
|
9 | import {getMockDispatcher, getWrapDispatcher} from '../util/test/util';
|
10 | import Map from '../ui/map';
|
11 |
|
12 | function createSource(options, transformCallback?, clearTiles = () => {}) {
|
13 | const source = new VectorTileSource('id', options, getMockDispatcher(), options.eventedParent);
|
14 | source.onAdd({
|
15 | transform: {showCollisionBoxes: false},
|
16 | _getMapId: () => 1,
|
17 | _requestManager: new RequestManager(transformCallback),
|
18 | style: {sourceCaches: {id: {clearTiles}}},
|
19 | getPixelRatio() { return 1; }
|
20 | } as any as Map);
|
21 |
|
22 | source.on('error', (e) => {
|
23 | throw e.error;
|
24 | });
|
25 |
|
26 | return source;
|
27 | }
|
28 |
|
29 | describe('VectorTileSource', () => {
|
30 | let server: FakeServer;
|
31 | beforeEach(() => {
|
32 | global.fetch = null;
|
33 | server = fakeServer.create();
|
34 | });
|
35 |
|
36 | afterEach(() => {
|
37 | server.restore();
|
38 | });
|
39 |
|
40 | test('can be constructed from TileJSON', done => {
|
41 | const source = createSource({
|
42 | minzoom: 1,
|
43 | maxzoom: 10,
|
44 | attribution: 'Maplibre',
|
45 | tiles: ['http://example.com/{z}/{x}/{y}.png']
|
46 | });
|
47 |
|
48 | source.on('data', (e) => {
|
49 | if (e.sourceDataType === 'metadata') {
|
50 | expect(source.tiles).toEqual(['http://example.com/{z}/{x}/{y}.png']);
|
51 | expect(source.minzoom).toBe(1);
|
52 | expect(source.maxzoom).toBe(10);
|
53 | expect((source as Source).attribution).toBe('Maplibre');
|
54 | done();
|
55 | }
|
56 | });
|
57 | });
|
58 |
|
59 | test('can be constructed from a TileJSON URL', done => {
|
60 | server.respondWith('/source.json', JSON.stringify(fixturesSource));
|
61 |
|
62 | const source = createSource({url: '/source.json'});
|
63 |
|
64 | source.on('data', (e) => {
|
65 | if (e.sourceDataType === 'metadata') {
|
66 | expect(source.tiles).toEqual(['http://example.com/{z}/{x}/{y}.png']);
|
67 | expect(source.minzoom).toBe(1);
|
68 | expect(source.maxzoom).toBe(10);
|
69 | expect((source as Source).attribution).toBe('Maplibre');
|
70 | done();
|
71 | }
|
72 | });
|
73 |
|
74 | server.respond();
|
75 | });
|
76 |
|
77 | test('transforms the request for TileJSON URL', () => {
|
78 | server.respondWith('/source.json', JSON.stringify(fixturesSource));
|
79 | const transformSpy = jest.fn().mockImplementation((url) => {
|
80 | return {url};
|
81 | });
|
82 |
|
83 | createSource({url: '/source.json'}, transformSpy);
|
84 | server.respond();
|
85 | expect(transformSpy).toHaveBeenCalledWith('/source.json', 'Source');
|
86 | });
|
87 |
|
88 | test('fires event with metadata property', done => {
|
89 | server.respondWith('/source.json', JSON.stringify(fixturesSource));
|
90 | const source = createSource({url: '/source.json'});
|
91 | source.on('data', (e) => {
|
92 | if (e.sourceDataType === 'content') done();
|
93 | });
|
94 | server.respond();
|
95 | });
|
96 |
|
97 | test('fires "dataloading" event', done => {
|
98 | server.respondWith('/source.json', JSON.stringify(fixturesSource));
|
99 | const evented = new Evented();
|
100 | let dataloadingFired = false;
|
101 | evented.on('dataloading', () => {
|
102 | dataloadingFired = true;
|
103 | });
|
104 | const source = createSource({url: '/source.json', eventedParent: evented});
|
105 | source.on('data', (e) => {
|
106 | if (e.sourceDataType === 'metadata') {
|
107 | if (!dataloadingFired) done('test failed: dataloading not fired');
|
108 | done();
|
109 | }
|
110 | });
|
111 | server.respond();
|
112 | });
|
113 |
|
114 | test('serialize URL', () => {
|
115 | const source = createSource({
|
116 | url: 'http://localhost:2900/source.json'
|
117 | });
|
118 | expect(source.serialize()).toEqual({
|
119 | type: 'vector',
|
120 | url: 'http://localhost:2900/source.json'
|
121 | });
|
122 | });
|
123 |
|
124 | test('serialize TileJSON', () => {
|
125 | const source = createSource({
|
126 | minzoom: 1,
|
127 | maxzoom: 10,
|
128 | attribution: 'Maplibre',
|
129 | tiles: ['http://example.com/{z}/{x}/{y}.png']
|
130 | });
|
131 | expect(source.serialize()).toEqual({
|
132 | type: 'vector',
|
133 | minzoom: 1,
|
134 | maxzoom: 10,
|
135 | attribution: 'Maplibre',
|
136 | tiles: ['http://example.com/{z}/{x}/{y}.png']
|
137 | });
|
138 | });
|
139 |
|
140 | function testScheme(scheme, expectedURL) {
|
141 | test(`scheme "${scheme}"`, done => {
|
142 | const source = createSource({
|
143 | minzoom: 1,
|
144 | maxzoom: 10,
|
145 | attribution: 'Maplibre',
|
146 | tiles: ['http://example.com/{z}/{x}/{y}.png'],
|
147 | scheme
|
148 | });
|
149 |
|
150 | source.dispatcher = getWrapDispatcher()({
|
151 | send(type, params) {
|
152 | expect(type).toBe('loadTile');
|
153 | expect(expectedURL).toBe(params.request.url);
|
154 | done();
|
155 | }
|
156 | });
|
157 |
|
158 | source.on('data', (e) => {
|
159 | if (e.sourceDataType === 'metadata') source.loadTile({
|
160 | tileID: new OverscaledTileID(10, 0, 10, 5, 5)
|
161 | } as any as Tile, () => {});
|
162 | });
|
163 | });
|
164 | }
|
165 |
|
166 | testScheme('xyz', 'http://example.com/10/5/5.png');
|
167 | testScheme('tms', 'http://example.com/10/5/1018.png');
|
168 |
|
169 | test('transforms tile urls before requesting', done => {
|
170 | server.respondWith('/source.json', JSON.stringify(fixturesSource));
|
171 |
|
172 | const source = createSource({url: '/source.json'});
|
173 | const transformSpy = jest.spyOn(source.map._requestManager, 'transformRequest');
|
174 | source.on('data', (e) => {
|
175 | if (e.sourceDataType === 'metadata') {
|
176 | const tile = {
|
177 | tileID: new OverscaledTileID(10, 0, 10, 5, 5),
|
178 | state: 'loading',
|
179 | loadVectorData () {},
|
180 | setExpiryData() {}
|
181 | } as any as Tile;
|
182 | source.loadTile(tile, () => {});
|
183 | expect(transformSpy).toHaveBeenCalledTimes(1);
|
184 | expect(transformSpy).toHaveBeenCalledWith('http://example.com/10/5/5.png', 'Tile');
|
185 | done();
|
186 | }
|
187 | });
|
188 |
|
189 | server.respond();
|
190 | });
|
191 |
|
192 | test('reloads a loading tile properly', done => {
|
193 | const source = createSource({
|
194 | tiles: ['http://example.com/{z}/{x}/{y}.png']
|
195 | });
|
196 | const events = [];
|
197 | source.dispatcher = getWrapDispatcher()({
|
198 | send(type, params, cb) {
|
199 | events.push(type);
|
200 | if (cb) setTimeout(cb, 0);
|
201 | return 1;
|
202 | }
|
203 | });
|
204 |
|
205 | source.on('data', (e) => {
|
206 | if (e.sourceDataType === 'metadata') {
|
207 | const tile = {
|
208 | tileID: new OverscaledTileID(10, 0, 10, 5, 5),
|
209 | state: 'loading',
|
210 | loadVectorData () {
|
211 | this.state = 'loaded';
|
212 | events.push('tileLoaded');
|
213 | },
|
214 | setExpiryData() {}
|
215 | } as any as Tile;
|
216 | source.loadTile(tile, () => {});
|
217 | expect(tile.state).toBe('loading');
|
218 | source.loadTile(tile, () => {
|
219 | expect(events).toEqual(
|
220 | ['loadTile', 'tileLoaded', 'enforceCacheSizeLimit', 'reloadTile', 'tileLoaded']
|
221 | );
|
222 | done();
|
223 | });
|
224 | }
|
225 | });
|
226 | });
|
227 |
|
228 | test('respects TileJSON.bounds', done => {
|
229 | const source = createSource({
|
230 | minzoom: 0,
|
231 | maxzoom: 22,
|
232 | attribution: 'Maplibre',
|
233 | tiles: ['http://example.com/{z}/{x}/{y}.png'],
|
234 | bounds: [-47, -7, -45, -5]
|
235 | });
|
236 | source.on('data', (e) => {
|
237 | if (e.sourceDataType === 'metadata') {
|
238 | expect(source.hasTile(new OverscaledTileID(8, 0, 8, 96, 132))).toBeFalsy();
|
239 | expect(source.hasTile(new OverscaledTileID(8, 0, 8, 95, 132))).toBeTruthy();
|
240 | done();
|
241 | }
|
242 | });
|
243 | });
|
244 |
|
245 | test('does not error on invalid bounds', done => {
|
246 | const source = createSource({
|
247 | minzoom: 0,
|
248 | maxzoom: 22,
|
249 | attribution: 'Maplibre',
|
250 | tiles: ['http://example.com/{z}/{x}/{y}.png'],
|
251 | bounds: [-47, -7, -45, 91]
|
252 | });
|
253 |
|
254 | source.on('data', (e) => {
|
255 | if (e.sourceDataType === 'metadata') {
|
256 | expect(source.tileBounds.bounds).toEqual({_sw:{lng: -47, lat: -7}, _ne:{lng: -45, lat: 90}});
|
257 | done();
|
258 | }
|
259 | });
|
260 | });
|
261 |
|
262 | test('respects TileJSON.bounds when loaded from TileJSON', done => {
|
263 | server.respondWith('/source.json', JSON.stringify({
|
264 | minzoom: 0,
|
265 | maxzoom: 22,
|
266 | attribution: 'Maplibre',
|
267 | tiles: ['http://example.com/{z}/{x}/{y}.png'],
|
268 | bounds: [-47, -7, -45, -5]
|
269 | }));
|
270 | const source = createSource({url: '/source.json'});
|
271 |
|
272 | source.on('data', (e) => {
|
273 | if (e.sourceDataType === 'metadata') {
|
274 | expect(source.hasTile(new OverscaledTileID(8, 0, 8, 96, 132))).toBeFalsy();
|
275 | expect(source.hasTile(new OverscaledTileID(8, 0, 8, 95, 132))).toBeTruthy();
|
276 | done();
|
277 | }
|
278 | });
|
279 | server.respond();
|
280 | });
|
281 |
|
282 | test('respects collectResourceTiming parameter on source', done => {
|
283 | const source = createSource({
|
284 | tiles: ['http://example.com/{z}/{x}/{y}.png'],
|
285 | collectResourceTiming: true
|
286 | });
|
287 | source.dispatcher = getWrapDispatcher()({
|
288 | send(type, params, cb) {
|
289 | expect(params.request.collectResourceTiming).toBeTruthy();
|
290 | setTimeout(cb, 0);
|
291 | done();
|
292 |
|
293 |
|
294 | source.dispatcher = getMockDispatcher();
|
295 |
|
296 | return 1;
|
297 | }
|
298 | });
|
299 |
|
300 | source.on('data', (e) => {
|
301 | if (e.sourceDataType === 'metadata') {
|
302 | const tile = {
|
303 | tileID: new OverscaledTileID(10, 0, 10, 5, 5),
|
304 | state: 'loading',
|
305 | loadVectorData () {},
|
306 | setExpiryData() {}
|
307 | } as any as Tile;
|
308 | source.loadTile(tile, () => {});
|
309 | }
|
310 | });
|
311 | });
|
312 |
|
313 | test('cancels TileJSON request if removed', () => {
|
314 | const source = createSource({url: '/source.json'});
|
315 | source.onRemove();
|
316 | expect((server as any).lastRequest.aborted).toBe(true);
|
317 | });
|
318 |
|
319 | test('supports url property updates', () => {
|
320 | const source = createSource({
|
321 | url: 'http://localhost:2900/source.json'
|
322 | });
|
323 | source.setUrl('http://localhost:2900/source2.json');
|
324 | expect(source.serialize()).toEqual({
|
325 | type: 'vector',
|
326 | url: 'http://localhost:2900/source2.json'
|
327 | });
|
328 | });
|
329 |
|
330 | test('supports tiles property updates', () => {
|
331 | const source = createSource({
|
332 | minzoom: 1,
|
333 | maxzoom: 10,
|
334 | attribution: 'Maplibre',
|
335 | tiles: ['http://example.com/{z}/{x}/{y}.png']
|
336 | });
|
337 | source.setTiles(['http://example2.com/{z}/{x}/{y}.png']);
|
338 | expect(source.serialize()).toEqual({
|
339 | type: 'vector',
|
340 | minzoom: 1,
|
341 | maxzoom: 10,
|
342 | attribution: 'Maplibre',
|
343 | tiles: ['http://example2.com/{z}/{x}/{y}.png']
|
344 | });
|
345 | });
|
346 |
|
347 | test('setTiles only clears the cache once the TileJSON has reloaded', done => {
|
348 | const clearTiles = jest.fn();
|
349 | const source = createSource({tiles: ['http://example.com/{z}/{x}/{y}.pbf']}, undefined, clearTiles);
|
350 | source.setTiles(['http://example2.com/{z}/{x}/{y}.pbf']);
|
351 | expect(clearTiles.mock.calls).toHaveLength(0);
|
352 | source.once('data', () => {
|
353 | expect(clearTiles.mock.calls).toHaveLength(1);
|
354 | done();
|
355 | });
|
356 | });
|
357 | });
|