├── LICENSE
├── .gitignore
├── example
└── index.html
├── README.md
└── src
└── maplibre-preload.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Abel Vázquez Montoro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | MapLibre-preload 1.1.0
7 |
8 |
9 |
10 |
11 |
24 |
25 |
26 |
27 |
44 |
45 |
46 |
47 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # maplibre-preload
2 |
3 | A tiny (1 kB gziped) almost-zero-configuration plugin for preloading tiles and smoothen the experience when using targeted movements in [MapLibreGL JS](https://maplibre.org/).
4 |
5 | ## Demo
6 |
7 | [https://abelvm.github.io/maplibre-preload/example/](https://abelvm.github.io/maplibre-preload/example/)
8 |
9 | You may want to check the `network` tab at develover console to check the cache hits.
10 |
11 | ## Motivation
12 |
13 | It started [here](https://github.com/maplibre/maplibre-gl-js/issues/116), a conversation about the need of precaching tiles when the user start a movement, and as one the reference mentioned there was [MapWorkBox](https://github.com/AbelVM/mapworkbox), a little PoC I built for testing preemptive tiles caching using `Service Workers`.
14 |
15 | So, the idea is to smooth the rendering of the animation frames of the camera and final scenario when using (animated) targeted movements map methods (`panTo`, `zoomTo`, `easeTo` and `flyTo`).
16 |
17 | The MapLibre GL JS requests new tiles as the camera view changes during the animation, and maybe batch-preloading the tiles of the final scenario, the final animation transition might look way better.
18 |
19 | ## Features
20 |
21 | As of today, this plugin offers the next features:
22 |
23 | * It takes advantage of MapLibre internal tiles life-cycle management
24 | * **custom protocols ([addProtocol](https://maplibre.org/maplibre-gl-js/docs/API/functions/addProtocol/)) supported!** (when `useTile = true`)
25 | * It hijacks the old methods and adds the pre-load functionality in a transparent way.
26 | * It automatically detects the tiled sources and apply the preload to all of them
27 | * Full final scenario preload
28 | * Full in-between animation scenarios preload
29 | * Pitch & bearing management
30 | * Limit the amount of server requests, lowering the priority of tiles at the viewport borders if needed (those tiles won't be cached and will be requested by MapLibre as it needs them)
31 | * Cancelling pending requests if the movement has ended or new interactions are detected
32 |
33 | ## How to
34 |
35 | First, just add the dependency `after` MapLibreGL JS.
36 |
37 | Then, just load the plugin after instantiating your map and it will manage everything in a transparent way
38 |
39 | ```javascript
40 | new MaplibrePreload(map, options);
41 |
42 | ```
43 |
44 | Available options on instantiating the plugin:
45 |
46 | | Parameter | Type | Default | Description |
47 | |---|---|---|---|
48 | | progressCallback | function({ loaded, total, failed }) | null | Callback function to be called per tile preload, mainly for debugging purposes. If `useTile` is set to `true`, this parameter is ignored |
49 | | async | boolean | true | Tells the plugin whether to wait for the full preload before triggering the movement|
50 | | burstLimit | integer | 200 | Soft limit for the number of tiles preloaded during the animation |
51 | | useTile | boolean | true | To use internal MapLibre tiles management, or fallback to `fetch` |
52 |
53 | Example:
54 |
55 | ```javascript
56 | const map = new maplibregl.Map(mapOptions);
57 |
58 | new MaplibrePreload(map, {
59 | progressCallback: ({ loaded, total, failed }) => {
60 | console.log(`Preloading tiles: ${loaded}/${total} loaded, ${failed} failed`);
61 | },
62 | async: true,
63 | burstLimit: 250,
64 | useTile: true
65 | });
66 |
67 | ```
68 |
69 | Then, you can call `panTo`, `zoomTo`, `easeTo` or `flyTo` in the old way, and the tiles will be pre preloaded without further coding.
70 |
71 | Some notes on the side effects of the common options of those functions:
72 |
73 | | Parameter | Effect |
74 | |---|---|
75 | | animate | If set to `false`, no preload is performed |
76 | | duration | The preload time limit is set to `5 * duration` for each run |
77 |
78 | ### Changelog
79 |
80 | * **v 1.1.0**
81 | * [Feature] `Tile` class hijacked! We can choose whether use this or classic `fetch` (thanks to hack by [wayofthefuture](https://github.com/wayofthefuture))
82 | * [Fix] Missing path for `zoomTo`
83 | * **v 1.0.0**
84 | * [Feature] Fully rewritten
85 | * [Feature] Full support for in-between animation frames scenarios
86 | * [Feature] Full support of actual `flyTo` camera path during animation
87 | * [Feature] Pitch and bearing are taken into account for each frame
88 | * [Feature] The higher the pitch, the lower priority is given to far tiles
89 | * [Feature] If the amount of tiles in a given frame is bigger than `burstLimit`, the tiles far from the center are dropped till the limit is met
90 | * [Feature] Pending requests are canceled if the movement has ended, the time limit is met or a new movement is started
91 | * **v 0.0.1**
92 | * [Feature] Upgraded to work with MapLibre 5.6.0
93 | * [Feature] Updated dependencies
94 | * [Feature] Working example as gh-page
95 | * **v 0.0.0b**
96 | * [Fix] Return the map object in the `cached__To` methods to keep the original output
97 | * [Fix] Use the [Bresenham algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm) to preload only the tiles in the start -> end path instead of all the tiles in the bounding box defined by those two points
98 | * [Feature] Add a `run` flag to allow preloading with/without running the actual `___To` map methods. Intended to enable the preload of the movement when we can expect the animation later (v.g.: preload when hovering a button, flyTo when clicking)
99 | * [Feature] Hide log messages behind a `debug` flag (boolean, default `false`)
100 | * **v 0.0.0a**
101 | * Initial release
102 |
103 | ### To Do
104 |
105 | * ~~Because of [this issue](https://github.com/maplibre/maplibre-gl-js/issues/6041), there is no way to take advantage of the own MapLibre GL JS tiles and cache management, so I needed to fall back to `fetch` logic and rely on the browser cache.~~
106 | * Check whether TMS schema needs extra work
107 |
--------------------------------------------------------------------------------
/src/maplibre-preload.js:
--------------------------------------------------------------------------------
1 | class MaplibrePreload {
2 |
3 | constructor(map, options = {}) {
4 | this.map = map;
5 | this.progressCallback = options.progressCallback || null;
6 | this.burstLimit = options.burstLimit || 200;
7 | this.async = (options.hasOwnProperty('async') && !options.async) ? false : true;
8 | this.useTile = (options.hasOwnProperty('useTile') && !options.useTile) ? false : true;
9 | this.controller = {};
10 | this._patchMoveMethods();
11 | this.map._captureTileClass= e => {
12 | if (e.tile && e.tile.tileID) {
13 | e.target.Tile = e.tile.constructor;
14 | e.target.OverscaledTileID = e.tile.tileID.constructor;
15 | e.target.off('sourcedata', e.target._captureTileClass);
16 | }
17 | }
18 | if (this.useTile) this.map.on('sourcedata', this.map._captureTileClass);
19 | }
20 |
21 |
22 | _patchMoveMethods() {
23 | const methods = ['flyTo', 'panTo', 'easeTo', 'zoomTo'];
24 | methods.forEach(method => {
25 | const original = this.map[method].bind(this.map);
26 | this.map[method] = async (options) => {
27 | Object.keys(this.controller).forEach(a => {
28 | this.controller[a].abort('cancelling due to new movement');
29 | delete this.controller[a];
30 | });
31 | if (this.async) {
32 | await this._preloadTilesForMove(method, options);
33 | } else {
34 | this._preloadTilesForMove(method, options);
35 | }
36 | return original(options);
37 | };
38 | });
39 | }
40 |
41 | async _preloadTilesForMove(method, options) {
42 | if (options.hasOwnProperty('animate') && !options.animate) return true;
43 | this.duration = options.duration || 1000;
44 | this.padding = options.padding || 0;
45 | this.fps = options.fps || 60;
46 | this.rho = options.curve || 1.42;
47 | const
48 | start = {
49 | 'center': this.map.getCenter(),
50 | 'zoom': this.map.getZoom(),
51 | 'bearing': this.map.getBearing(),
52 | 'pitch': this.map.getPitch()
53 | },
54 | tc = options.center || start.center,
55 | endCenter = (!!tc.lng) ? tc : { 'lng': tc[0], 'lat': tc[1] },
56 | end = {
57 | 'center': endCenter,
58 | 'zoom': options.zoom !== undefined ? options.zoom : start.zoom,
59 | 'bearing': options.bearing !== undefined ? options.bearing : start.bearing,
60 | 'pitch': options.pitch !== undefined ? options.pitch : start.pitch
61 | };
62 |
63 | let samples;
64 | if (method === 'flyTo') {
65 | samples = this._sampleFlyToPath(start, end, options);
66 | } else if (method === 'panTo') {
67 | samples = this._samplePanToPath(start, end, options);
68 | } else if (method === 'easeTo') {
69 | samples = this._sampleEaseToPath(start, end, options);
70 | } else if (method === 'zoomTo') {
71 | samples = this._sampleZoomToPath(start, end, options);
72 | } else {
73 | samples = [end];
74 | }
75 |
76 | const endRequests = {};
77 | const perSource = this._getVisibleTilesPerSource(end, 0);
78 | for (const [sourceId, tiles] of Object.entries(perSource)) {
79 | if (!endRequests[sourceId]) endRequests[sourceId] = new Set();
80 | tiles.forEach(t => endRequests[sourceId].add(t));
81 | }
82 | await this._preloadTilesInternal(endRequests);
83 |
84 | const tileRequests = {};
85 | for (const s of samples) {
86 | let
87 | f = 0,
88 | size = 0,
89 | perSource = this._getVisibleTilesPerSource(s);
90 | for (const [sourceId, tiles] of Object.entries(perSource)) {
91 | size = Math.max(size, tiles.length);
92 | }
93 | while (size > 1.1 * this.burstLimit) {
94 | f++;
95 | size = 0;
96 | perSource = this._getVisibleTilesPerSource(s, f / 20);
97 | for (const [sourceId, tiles] of Object.entries(perSource)) {
98 | size = Math.max(size, tiles.length);
99 | }
100 | }
101 | for (const [sourceId, tiles] of Object.entries(perSource)) {
102 | if (!tileRequests[sourceId]) tileRequests[sourceId] = new Set();
103 | tiles.forEach(t => tileRequests[sourceId].add(t));
104 | }
105 | }
106 | await this._preloadTilesInternal(tileRequests);
107 | }
108 |
109 | _sampleFlyToPath(start, end, options) {
110 | return this._flyToFrames(options);
111 | }
112 |
113 | _samplePanToPath(start, end, options) {
114 | const
115 | totalFrames = Math.ceil((this.duration / 1000) * this.fps),
116 | samples = [end];
117 | for (let i = 1; i < totalFrames; i++) {
118 | const t = i / totalFrames;
119 | samples.push({
120 | 'center': this._interpolateLngLatLinear(start.center, end.center, t),
121 | 'zoom': start.zoom,
122 | 'bearing': start.bearing,
123 | 'pitch': start.pitch
124 | });
125 | }
126 | return samples;
127 | }
128 |
129 | _sampleEaseToPath(start, end, options) {
130 | const
131 | totalFrames = Math.ceil((this.duration / 1000) * this.fps),
132 | samples = [end];
133 | for (let i = 1; i < totalFrames; i++) {
134 | const t = i / totalFrames;
135 | samples.push({
136 | 'center': this._interpolateLngLatLinear(start.center, end.center, t),
137 | 'zoom': this._interpolateLinear(start.zoom, end.zoom, t),
138 | 'bearing': this._interpolateLinear(start.bearing, end.bearing, t),
139 | 'pitch': this._interpolateLinear(start.pitch, end.pitch, t)
140 | });
141 | }
142 | return samples;
143 | }
144 |
145 | _sampleZoomToPath(start, end, options) {
146 | const
147 | totalFrames = Math.ceil((this.duration / 1000) * this.fps),
148 | samples = [end];
149 | for (let i = 1; i < totalFrames; i++) {
150 | const t = i / totalFrames;
151 | samples.push({
152 | 'center': start.center,
153 | 'zoom': this._interpolateLinear(start.zoom, end.zoom, t),
154 | 'bearing': start.bearing,
155 | 'pitch': start.pitch
156 | });
157 | }
158 | return samples;
159 | }
160 |
161 | _getVisibleTilesPerSource({ center, zoom, bearing, pitch }, factor = 0) {
162 | const perSource = {};
163 | for (const sourceId in this.map.style.sourceCaches) {
164 | const sourceCache = this.map.style.sourceCaches[sourceId];
165 | if (!sourceCache.used) continue;
166 | perSource[sourceId] = this._getVisibleTileRange(this.map.getSource(sourceId), { center, zoom, bearing, pitch }, factor).map(t => `${t[0]}|${t[1]}|${t[2]}`);
167 | }
168 | return perSource;
169 | }
170 |
171 | _getVisibleTileRange(source, { center, zoom, bearing, pitch }, factor) {
172 |
173 | function lngLatToTile(lng, lat, zoom) {
174 | const z2 = Math.pow(2, zoom);
175 | const x = z2 * ((lng + 180) / 360);
176 | const y = z2 * (1 - (Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)) / Math.PI)) / 2;
177 | return [x, y];
178 | }
179 |
180 | const
181 | tr = this.map.transform,
182 | width = tr.width,
183 | height = tr.height,
184 | pitchLimit = pitch / 150,
185 | corner_points = [
186 | [width * factor, height * (factor + pitchLimit)],
187 | [width * (1 - factor), height * (factor + pitchLimit)],
188 | [width * (1 - factor), height * (1 - factor)],
189 | [width * factor, height * (1 - factor)]
190 | ],
191 | corner_lnglat = corner_points.map(p => this.map.transform.screenPointToLocation({ x: p[0], y: p[1] })),
192 | tileCoords = corner_lnglat.map(c => lngLatToTile(c.lng, c.lat, Math.floor(zoom))),
193 | xs = tileCoords.map(([x, _]) => x),
194 | ys = tileCoords.map(([_, y]) => y),
195 | minX = Math.floor(Math.min(...xs)),
196 | maxX = Math.ceil(Math.max(...xs)),
197 | minY = Math.floor(Math.min(...ys)),
198 | maxY = Math.ceil(Math.max(...ys)),
199 | tiles = [];
200 |
201 | for (let x = minX; x < maxX; x++) {
202 | for (let y = minY; y < maxY; y++) {
203 | if (source.scheme != 'xyz') y = Math.pow(2, zoom) - y - 1;
204 | tiles.push([Math.floor(zoom), x, y]);
205 | }
206 | }
207 |
208 | return tiles;
209 | }
210 |
211 | async _preloadTilesInternal(tileRequests) {
212 |
213 | if(this.useTile){
214 | const tileArray = [];
215 | for (const [sourceId, tileSet] of Object.entries(tileRequests)) {
216 | const
217 | source = this.map.getSource(sourceId),
218 | tileSize = source.tileSize;
219 | for (const t of [...tileSet]) {
220 | const
221 | [z, x, y] = t.split('|'),
222 | tileID = new this.map.OverscaledTileID(z, 0, z, x, y),
223 | tile = new this.map.Tile(tileID, tileSize);
224 | tileArray.push(source.loadTile(tile));
225 | }
226 | }
227 | return Promise.allSettled(tileArray);
228 | }else{
229 | return new Promise(async (resolve, reject) => {
230 | const
231 | uuid = this._uuid(),
232 | timeoutId = setTimeout(() => {
233 | this.controller[uuid].abort('timeout');
234 | cleanup();
235 | resolve();
236 | }, this.duration * 5),
237 | cleanup = () => {
238 | delete this.controller[uuid];
239 | clearTimeout(timeoutId);
240 | },
241 | fetchArray = [];
242 | let
243 | loaded = 0,
244 | failed = 0;
245 | this.controller[uuid] = new AbortController();
246 |
247 | for (const [sourceId, tileSet] of Object.entries(tileRequests)) {
248 | const source = this.map.getSource(sourceId);
249 | for (const tile of [...tileSet]) {
250 | const
251 | [z, x, y] = tile.split('|'),
252 | url = source.tiles[0].replace('{z}', z).replace('{x}', x).replace('{y}', y);
253 | try {
254 | fetchArray.push(fetch(url, { 'signal': this.controller[uuid].signal }));
255 | } catch (e) {
256 | console.log(e);
257 | }
258 |
259 | }
260 | }
261 | try {
262 | const response = await Promise.allSettled(fetchArray);
263 | response.forEach(r => {
264 | if (!r.ok) {
265 | failed++;
266 | } else {
267 | loaded++;
268 | }
269 | if (this.progressCallback) {
270 | this.progressCallback({ loaded, total: fetchArray.length, failed });
271 | }
272 | });
273 | cleanup();
274 | resolve();
275 | } catch (e) {
276 | console.log(e);
277 | cleanup();
278 | resolve();
279 | }
280 | });
281 | }
282 | }
283 |
284 | _flyToFrames(options) {
285 | // ported from https://github.com/maplibre/maplibre-gl-js/blob/b7cf56df3605c4ce6f68df216ea1c6d69790c385/src/ui/camera.ts_L1379
286 | const
287 | totalFrames = Math.ceil((this.duration / 1000) * this.fps),
288 | tr = this.map._getTransformForUpdate(),
289 | startCenter = tr.center,
290 | startZoom = tr.zoom,
291 | startBearing = tr.bearing,
292 | startPitch = tr.pitch,
293 | startRoll = tr.roll,
294 | startPadding = tr.padding,
295 | center = (!!options.center.lng) ? options.center : { lng: options.center[0], lat: options.center[1] },
296 | bearing = 'bearing' in options ? this.map._normalizeBearing(options.bearing, startBearing) : startBearing,
297 | pitch = 'pitch' in options ? + options.pitch : startPitch,
298 | roll = 'roll' in options ? this.map._normalizeBearing(options.roll, startRoll) : startRoll,
299 | padding = 'padding' in options ? options.padding : startPadding,
300 | flyToHandler = this.map.cameraHelper.handleFlyTo(tr, {
301 | bearing,
302 | pitch,
303 | roll,
304 | padding,
305 | locationAtOffset: tr.center,
306 | offsetAsPoint: { 'x': 0, 'y': 0 },
307 | center: options.center,
308 | minZoom: options.minZoom || 0,
309 | zoom: options.zoom,
310 | }),
311 | w0 = Math.max(tr.width, tr.height),
312 | w1 = w0 / flyToHandler.scaleOfZoom,
313 | u1 = flyToHandler.pixelPathLength;
314 | let rho = options.curve || 1.42;
315 | if (typeof flyToHandler.scaleOfMinZoom === 'number') {
316 | const wMax = w0 / flyToHandler.scaleOfMinZoom;
317 | rho = Math.sqrt(wMax / u1 * 2);
318 | }
319 | const rho2 = rho * rho;
320 | function zoomOutFactor(descent) {
321 | const b = (w1 * w1 - w0 * w0 + (descent ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (descent ? w1 : w0) * rho2 * u1);
322 | return Math.log(Math.sqrt(b * b + 1) - b);
323 | }
324 | function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; }
325 | function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; }
326 | function tanh(n) { return sinh(n) / cosh(n); }
327 | const r0 = zoomOutFactor(false);
328 | function w(s) { return (cosh(r0) / cosh(r0 + rho * s)); };
329 | function u(s) { return w0 * ((cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2) / u1; };
330 | let S = (zoomOutFactor(true) - r0) / rho;
331 | if (Math.abs(u1) < 0.000002 || !isFinite(S)) {
332 | const k = w1 < w0 ? -1 : 1;
333 | S = Math.abs(Math.log(w1 / w0)) / rho;
334 | u = () => 0;
335 | w = (s) => Math.exp(k * rho * s);
336 | }
337 | const frames = [];
338 | for (let i = 0; i <= totalFrames; i++) {
339 | const k = i / totalFrames;
340 | const s = k * S;
341 | const scale = 1 / w(s);
342 | frames.push({
343 | 'center': this._interpolateLngLatLinear(startCenter, center, k),
344 | 'zoom': startZoom + Math.log2(scale),
345 | 'bearing': bearing + (bearing - startBearing) * k,
346 | 'pitch': pitch + (pitch - startPitch) * k
347 | })
348 | }
349 | frames.push({
350 | 'center': center,
351 | 'zoom': options.zoom,
352 | 'bearing': bearing,
353 | 'pitch': pitch
354 | });
355 | return frames;
356 |
357 | }
358 |
359 | _interpolateLinear(a, b, t) { return a + (b - a) * t; }
360 |
361 | _interpolateLngLatLinear(a, b, t) {
362 | return { lng: this._interpolateLinear(a.lng, b.lng, t), lat: this._interpolateLinear(a.lat, b.lat, t) };
363 | }
364 |
365 | _uuid() {
366 | const
367 | lut = [],
368 | d0 = Math.random() * 0xffffffff | 0,
369 | d1 = Math.random() * 0xffffffff | 0,
370 | d2 = Math.random() * 0xffffffff | 0,
371 | d3 = Math.random() * 0xffffffff | 0;
372 | for (var i = 0; i < 256; i++) {
373 | lut[i] = (i < 16 ? '0' : '') + (i).toString(16);
374 | }
375 | return lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff] + '-' +
376 | lut[d1 & 0xff] + lut[d1 >> 8 & 0xff] + '-' + lut[d1 >> 16 & 0x0f | 0x40] + lut[d1 >> 24 & 0xff] + '-' +
377 | lut[d2 & 0x3f | 0x80] + lut[d2 >> 8 & 0xff] + '-' + lut[d2 >> 16 & 0xff] + lut[d2 >> 24 & 0xff] +
378 | lut[d3 & 0xff] + lut[d3 >> 8 & 0xff] + lut[d3 >> 16 & 0xff] + lut[d3 >> 24 & 0xff];
379 | }
380 |
381 | }
382 |
--------------------------------------------------------------------------------