openlayers-prefetching 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/PrefetchManager.esm.js +711 -0
- package/dist/PrefetchManager.esm.js.map +1 -0
- package/dist/PrefetchManager.js +716 -0
- package/dist/PrefetchManager.js.map +1 -0
- package/dist/types/PrefetchConstants.d.ts +15 -0
- package/dist/types/PrefetchManager.d.ts +64 -0
- package/dist/types/PrefetchPlanner.d.ts +23 -0
- package/dist/types/PrefetchScheduler.d.ts +29 -0
- package/dist/types/PrefetchStats.d.ts +28 -0
- package/dist/types/PrefetchTypes.d.ts +65 -0
- package/dist/types/TileLoader.d.ts +23 -0
- package/package.json +51 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import { listen, unlistenByKey } from 'ol/events.js';
|
|
2
|
+
import MapEventType from 'ol/MapEventType.js';
|
|
3
|
+
import { getUid } from 'ol/util.js';
|
|
4
|
+
import TileState from 'ol/TileState.js';
|
|
5
|
+
import { getForViewAndSize, buffer } from 'ol/extent.js';
|
|
6
|
+
|
|
7
|
+
const PrefetchCategory = {
|
|
8
|
+
SPATIAL_ACTIVE: 'spatial',
|
|
9
|
+
BACKGROUND_LAYERS_VIEWPORT: 'bgViewport',
|
|
10
|
+
BACKGROUND_LAYERS_BUFFER: 'bgBuffer',
|
|
11
|
+
NEXT_NAV_ACTIVE: 'nextNavActive',
|
|
12
|
+
NEXT_NAV_BACKGROUND: 'nextNavBackground',
|
|
13
|
+
};
|
|
14
|
+
const DEFAULT_CATEGORY_PRIORITIES = {
|
|
15
|
+
[PrefetchCategory.SPATIAL_ACTIVE]: 1,
|
|
16
|
+
[PrefetchCategory.BACKGROUND_LAYERS_VIEWPORT]: 2,
|
|
17
|
+
[PrefetchCategory.BACKGROUND_LAYERS_BUFFER]: 3,
|
|
18
|
+
[PrefetchCategory.NEXT_NAV_ACTIVE]: 4,
|
|
19
|
+
[PrefetchCategory.NEXT_NAV_BACKGROUND]: 5,
|
|
20
|
+
};
|
|
21
|
+
function getCategoryName(category) {
|
|
22
|
+
switch (category) {
|
|
23
|
+
case PrefetchCategory.SPATIAL_ACTIVE:
|
|
24
|
+
return 'Spatial (active)';
|
|
25
|
+
case PrefetchCategory.BACKGROUND_LAYERS_VIEWPORT:
|
|
26
|
+
return 'BG viewport';
|
|
27
|
+
case PrefetchCategory.BACKGROUND_LAYERS_BUFFER:
|
|
28
|
+
return 'BG buffer';
|
|
29
|
+
case PrefetchCategory.NEXT_NAV_ACTIVE:
|
|
30
|
+
return 'Next nav (active)';
|
|
31
|
+
case PrefetchCategory.NEXT_NAV_BACKGROUND:
|
|
32
|
+
return 'Next nav (BG)';
|
|
33
|
+
default:
|
|
34
|
+
return `Category ${category}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function createInitialCategoryCounts() {
|
|
38
|
+
return {
|
|
39
|
+
[PrefetchCategory.SPATIAL_ACTIVE]: { queued: 0, loading: 0, loaded: 0, errors: 0 },
|
|
40
|
+
[PrefetchCategory.BACKGROUND_LAYERS_VIEWPORT]: { queued: 0, loading: 0, loaded: 0, errors: 0 },
|
|
41
|
+
[PrefetchCategory.BACKGROUND_LAYERS_BUFFER]: { queued: 0, loading: 0, loaded: 0, errors: 0 },
|
|
42
|
+
[PrefetchCategory.NEXT_NAV_ACTIVE]: { queued: 0, loading: 0, loaded: 0, errors: 0 },
|
|
43
|
+
[PrefetchCategory.NEXT_NAV_BACKGROUND]: { queued: 0, loading: 0, loaded: 0, errors: 0 },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @module ol/prefetch/PrefetchStats
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Tracks prefetch statistics, per-category counts, error logs,
|
|
52
|
+
* and notifies listeners on changes.
|
|
53
|
+
*/
|
|
54
|
+
class PrefetchStats {
|
|
55
|
+
constructor() {
|
|
56
|
+
this.loadedCount_ = 0;
|
|
57
|
+
this.errorCount_ = 0;
|
|
58
|
+
this.categoryCounts_ = createInitialCategoryCounts();
|
|
59
|
+
this.errorLog_ = [];
|
|
60
|
+
this.listeners_ = [];
|
|
61
|
+
}
|
|
62
|
+
get categoryCounts() {
|
|
63
|
+
return this.categoryCounts_;
|
|
64
|
+
}
|
|
65
|
+
resetQueuedCounts() {
|
|
66
|
+
for (const key in this.categoryCounts_) {
|
|
67
|
+
this.categoryCounts_[key].queued = 0;
|
|
68
|
+
this.categoryCounts_[key].loading = 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
recordQueued(category) {
|
|
72
|
+
if (this.categoryCounts_[category]) {
|
|
73
|
+
this.categoryCounts_[category].queued++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
setQueuedCount(category, queued) {
|
|
77
|
+
if (this.categoryCounts_[category]) {
|
|
78
|
+
this.categoryCounts_[category].queued = queued;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
recordLoadingStart(category) {
|
|
82
|
+
if (this.categoryCounts_[category]) {
|
|
83
|
+
this.categoryCounts_[category].loading++;
|
|
84
|
+
this.categoryCounts_[category].queued = Math.max(0, this.categoryCounts_[category].queued - 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
recordLoadingEnd(category) {
|
|
88
|
+
if (this.categoryCounts_[category]) {
|
|
89
|
+
this.categoryCounts_[category].loading = Math.max(0, this.categoryCounts_[category].loading - 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
recordAlreadyLoaded(category) {
|
|
93
|
+
this.loadedCount_++;
|
|
94
|
+
if (this.categoryCounts_[category]) {
|
|
95
|
+
this.categoryCounts_[category].loaded++;
|
|
96
|
+
this.categoryCounts_[category].queued = Math.max(0, this.categoryCounts_[category].queued - 1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
recordLoaded(category) {
|
|
100
|
+
this.loadedCount_++;
|
|
101
|
+
if (this.categoryCounts_[category]) {
|
|
102
|
+
this.categoryCounts_[category].loaded++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
recordError(category, errorEntry) {
|
|
106
|
+
this.errorCount_++;
|
|
107
|
+
if (this.categoryCounts_[category]) {
|
|
108
|
+
this.categoryCounts_[category].errors++;
|
|
109
|
+
}
|
|
110
|
+
this.errorLog_.unshift(errorEntry);
|
|
111
|
+
if (this.errorLog_.length > 50) {
|
|
112
|
+
this.errorLog_.length = 50;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
recordEmpty(category) {
|
|
116
|
+
if (this.categoryCounts_[category]) {
|
|
117
|
+
this.categoryCounts_[category].errors++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
getSnapshot(queueLength, loadingSize, paused, nextTarget, categoryPriorities) {
|
|
121
|
+
return {
|
|
122
|
+
queued: queueLength,
|
|
123
|
+
loading: loadingSize,
|
|
124
|
+
loaded: this.loadedCount_,
|
|
125
|
+
errors: this.errorCount_,
|
|
126
|
+
paused,
|
|
127
|
+
spatialActive: { ...this.categoryCounts_[PrefetchCategory.SPATIAL_ACTIVE] },
|
|
128
|
+
bgViewport: { ...this.categoryCounts_[PrefetchCategory.BACKGROUND_LAYERS_VIEWPORT] },
|
|
129
|
+
bgBuffer: { ...this.categoryCounts_[PrefetchCategory.BACKGROUND_LAYERS_BUFFER] },
|
|
130
|
+
nextNavActive: { ...this.categoryCounts_[PrefetchCategory.NEXT_NAV_ACTIVE] },
|
|
131
|
+
nextNavBackground: { ...this.categoryCounts_[PrefetchCategory.NEXT_NAV_BACKGROUND] },
|
|
132
|
+
nextTarget: nextTarget ? { center: nextTarget.center, zoom: nextTarget.zoom } : null,
|
|
133
|
+
recentErrors: this.errorLog_.slice(),
|
|
134
|
+
categoryPriorities: { ...categoryPriorities },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
onStats(callback) {
|
|
138
|
+
this.listeners_.push(callback);
|
|
139
|
+
}
|
|
140
|
+
notify(stats) {
|
|
141
|
+
for (const cb of this.listeners_) {
|
|
142
|
+
cb(stats);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
dispose() {
|
|
146
|
+
this.listeners_ = [];
|
|
147
|
+
this.errorLog_ = [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @module ol/prefetch/PrefetchPlanner
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* Responsible for deciding WHAT tiles to load.
|
|
156
|
+
*
|
|
157
|
+
* Builds the prioritised prefetch queue by inspecting the current view state,
|
|
158
|
+
* active layer, background layers, and next navigation target.
|
|
159
|
+
*/
|
|
160
|
+
class PrefetchPlanner {
|
|
161
|
+
/**
|
|
162
|
+
* @param spatialBufferFactor Factor to expand viewport for spatial prefetch.
|
|
163
|
+
*/
|
|
164
|
+
constructor(spatialBufferFactor) {
|
|
165
|
+
this.lastNextTargetKey_ = null;
|
|
166
|
+
this.spatialBufferFactor_ = spatialBufferFactor;
|
|
167
|
+
}
|
|
168
|
+
buildQueue(map, activeLayer, backgroundLayers, nextTarget, categoryPriorities, stats) {
|
|
169
|
+
var _a, _b, _c;
|
|
170
|
+
const queue = [];
|
|
171
|
+
const seenTiles = new Set();
|
|
172
|
+
const nextTargetKey = nextTarget
|
|
173
|
+
? `${nextTarget.center[0]}|${nextTarget.center[1]}|${nextTarget.zoom}`
|
|
174
|
+
: null;
|
|
175
|
+
const preserveNextCounts = nextTargetKey && nextTargetKey === this.lastNextTargetKey_;
|
|
176
|
+
const prevNextNavActive = preserveNextCounts
|
|
177
|
+
? stats.categoryCounts[PrefetchCategory.NEXT_NAV_ACTIVE].queued
|
|
178
|
+
: 0;
|
|
179
|
+
const prevNextNavBackground = preserveNextCounts
|
|
180
|
+
? stats.categoryCounts[PrefetchCategory.NEXT_NAV_BACKGROUND].queued
|
|
181
|
+
: 0;
|
|
182
|
+
stats.resetQueuedCounts();
|
|
183
|
+
const view = map.getView();
|
|
184
|
+
if (!view || !view.isDef()) {
|
|
185
|
+
return queue;
|
|
186
|
+
}
|
|
187
|
+
const mapSize = map.getSize();
|
|
188
|
+
if (!mapSize) {
|
|
189
|
+
return queue;
|
|
190
|
+
}
|
|
191
|
+
const viewState = view.getState();
|
|
192
|
+
const viewExtent = getForViewAndSize(viewState.center, viewState.resolution, viewState.rotation, mapSize);
|
|
193
|
+
const zoom = view.getZoom();
|
|
194
|
+
if (zoom === undefined) {
|
|
195
|
+
return queue;
|
|
196
|
+
}
|
|
197
|
+
const z = Math.round(zoom);
|
|
198
|
+
const projection = view.getProjection();
|
|
199
|
+
const pixelRatio = (_c = (_b = (_a = map).getPixelRatio) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : 1;
|
|
200
|
+
const ctx = { queue, seenTiles, pixelRatio, stats };
|
|
201
|
+
if (activeLayer) {
|
|
202
|
+
this.enqueueSpatialBuffer_(ctx, activeLayer, viewExtent, z, projection, categoryPriorities[PrefetchCategory.SPATIAL_ACTIVE], PrefetchCategory.SPATIAL_ACTIVE);
|
|
203
|
+
}
|
|
204
|
+
for (const entry of backgroundLayers) {
|
|
205
|
+
if (entry.layer === activeLayer) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const subPriority = entry.priority * 0.001; // small factor to ensure background layers are ordered by their priority setting
|
|
209
|
+
this.enqueueViewportTiles_(ctx, entry.layer, viewExtent, z, projection, categoryPriorities[PrefetchCategory.BACKGROUND_LAYERS_VIEWPORT] + subPriority, PrefetchCategory.BACKGROUND_LAYERS_VIEWPORT);
|
|
210
|
+
}
|
|
211
|
+
if (nextTarget) {
|
|
212
|
+
this.lastNextTargetKey_ = nextTargetKey;
|
|
213
|
+
const nextZ = Math.round(nextTarget.zoom);
|
|
214
|
+
const nextResolution = view.getResolutionForZoom(nextTarget.zoom);
|
|
215
|
+
const nextExtent = getForViewAndSize(nextTarget.center, nextResolution, 0, mapSize);
|
|
216
|
+
if (activeLayer) {
|
|
217
|
+
this.enqueueViewportTiles_(ctx, activeLayer, nextExtent, nextZ, projection, categoryPriorities[PrefetchCategory.NEXT_NAV_ACTIVE], PrefetchCategory.NEXT_NAV_ACTIVE);
|
|
218
|
+
this.enqueueSpatialBuffer_(ctx, activeLayer, nextExtent, nextZ, projection, categoryPriorities[PrefetchCategory.NEXT_NAV_ACTIVE], PrefetchCategory.NEXT_NAV_ACTIVE);
|
|
219
|
+
}
|
|
220
|
+
for (const entry of backgroundLayers) {
|
|
221
|
+
if (entry.layer === activeLayer) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const subPriority = entry.priority * 0.001;
|
|
225
|
+
this.enqueueViewportTiles_(ctx, entry.layer, nextExtent, nextZ, projection, categoryPriorities[PrefetchCategory.NEXT_NAV_BACKGROUND] + subPriority, PrefetchCategory.NEXT_NAV_BACKGROUND);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (!nextTarget) {
|
|
229
|
+
this.lastNextTargetKey_ = null;
|
|
230
|
+
}
|
|
231
|
+
else if (preserveNextCounts) {
|
|
232
|
+
if (stats.categoryCounts[PrefetchCategory.NEXT_NAV_ACTIVE].queued === 0) {
|
|
233
|
+
stats.setQueuedCount(PrefetchCategory.NEXT_NAV_ACTIVE, prevNextNavActive);
|
|
234
|
+
}
|
|
235
|
+
if (stats.categoryCounts[PrefetchCategory.NEXT_NAV_BACKGROUND].queued === 0) {
|
|
236
|
+
stats.setQueuedCount(PrefetchCategory.NEXT_NAV_BACKGROUND, prevNextNavBackground);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
queue.sort((a, b) => a.priority - b.priority);
|
|
240
|
+
return queue;
|
|
241
|
+
}
|
|
242
|
+
enqueueViewportTiles_(ctx, layer, extent, z, projection, priority, category) {
|
|
243
|
+
const source = layer.getSource();
|
|
244
|
+
if (!source) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const tileGrid = source.getTileGridForProjection(projection);
|
|
248
|
+
const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
|
|
249
|
+
for (let x = tileRange.minX; x <= tileRange.maxX; x++) {
|
|
250
|
+
for (let y = tileRange.minY; y <= tileRange.maxY; y++) {
|
|
251
|
+
this.enqueueTile_(ctx, layer, source, [z, x, y], projection, priority, category);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
enqueueSpatialBuffer_(ctx, layer, viewExtent, z, projection, priority, category) {
|
|
256
|
+
const source = layer.getSource();
|
|
257
|
+
if (!source) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const tileGrid = source.getTileGridForProjection(projection);
|
|
261
|
+
const extentWidth = viewExtent[2] - viewExtent[0];
|
|
262
|
+
const extentHeight = viewExtent[3] - viewExtent[1];
|
|
263
|
+
const bufferX = (extentWidth * (this.spatialBufferFactor_ - 1)) / 2;
|
|
264
|
+
const bufferY = (extentHeight * (this.spatialBufferFactor_ - 1)) / 2;
|
|
265
|
+
const bufferValue = Math.max(bufferX, bufferY);
|
|
266
|
+
const bufferedExtent = buffer(viewExtent, bufferValue);
|
|
267
|
+
const bufferedTileRange = tileGrid.getTileRangeForExtentAndZ(bufferedExtent, z);
|
|
268
|
+
const viewportTileRange = tileGrid.getTileRangeForExtentAndZ(viewExtent, z);
|
|
269
|
+
for (let x = bufferedTileRange.minX; x <= bufferedTileRange.maxX; x++) {
|
|
270
|
+
for (let y = bufferedTileRange.minY; y <= bufferedTileRange.maxY; y++) {
|
|
271
|
+
if (viewportTileRange.containsXY(x, y)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
this.enqueueTile_(ctx, layer, source, [z, x, y], projection, priority, category);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
enqueueTile_(ctx, layer, source, tileCoord, projection, priority, category) {
|
|
279
|
+
const layerKey = getUid(layer);
|
|
280
|
+
const tileKey = `${layerKey}/${tileCoord[0]}/${tileCoord[1]}/${tileCoord[2]}`;
|
|
281
|
+
if (ctx.seenTiles.has(tileKey)) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
ctx.seenTiles.add(tileKey);
|
|
285
|
+
let tile;
|
|
286
|
+
try {
|
|
287
|
+
tile = source.getTile(tileCoord[0], tileCoord[1], tileCoord[2], ctx.pixelRatio, projection);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!tile) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const state = tile.getState();
|
|
296
|
+
if (state === TileState.LOADED || state === TileState.LOADING) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
ctx.queue.push({
|
|
300
|
+
id: tileKey,
|
|
301
|
+
priority,
|
|
302
|
+
category,
|
|
303
|
+
layer,
|
|
304
|
+
tileCoord,
|
|
305
|
+
timestamp: Date.now(),
|
|
306
|
+
});
|
|
307
|
+
ctx.stats.recordQueued(category);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Responsible for WHEN to load.
|
|
313
|
+
*
|
|
314
|
+
* Manages the tick timer, checks preconditions (user interaction, map tile
|
|
315
|
+
* queue draining), and signals the manager to fill download slots.
|
|
316
|
+
*/
|
|
317
|
+
class PrefetchScheduler {
|
|
318
|
+
constructor(tickInterval, callbacks) {
|
|
319
|
+
this.tickTimer_ = null;
|
|
320
|
+
this.enabled_ = true;
|
|
321
|
+
this.tickInterval_ = tickInterval;
|
|
322
|
+
this.callbacks_ = callbacks;
|
|
323
|
+
}
|
|
324
|
+
set enabled(enabled) {
|
|
325
|
+
this.enabled_ = enabled;
|
|
326
|
+
}
|
|
327
|
+
get enabled() {
|
|
328
|
+
return this.enabled_;
|
|
329
|
+
}
|
|
330
|
+
scheduleTick() {
|
|
331
|
+
if (this.tickTimer_ || !this.enabled_) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.tickTimer_ = setTimeout(() => {
|
|
335
|
+
this.tickTimer_ = null;
|
|
336
|
+
this.callbacks_.onRebuildNeeded();
|
|
337
|
+
this.callbacks_.onFillSlots();
|
|
338
|
+
}, this.tickInterval_);
|
|
339
|
+
}
|
|
340
|
+
runTick(userInteracting, mapTileQueue, stats) {
|
|
341
|
+
if (!this.enabled_ || userInteracting) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (mapTileQueue && mapTileQueue.getTilesLoading() > 0) {
|
|
345
|
+
this.callbacks_.onStatsChanged();
|
|
346
|
+
this.scheduleTick();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
this.callbacks_.onRebuildNeeded();
|
|
350
|
+
this.callbacks_.onFillSlots();
|
|
351
|
+
}
|
|
352
|
+
dispose() {
|
|
353
|
+
if (this.tickTimer_) {
|
|
354
|
+
clearTimeout(this.tickTimer_);
|
|
355
|
+
this.tickTimer_ = null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @module ol/prefetch/TileLoader
|
|
362
|
+
*/
|
|
363
|
+
/**
|
|
364
|
+
* Manages in-flight prefetch downloads. Supports abandoning all in-flight
|
|
365
|
+
* loads on user interaction so they don't block slots or compete with the
|
|
366
|
+
* map's own tile queue for HTTP connections.
|
|
367
|
+
*/
|
|
368
|
+
class TileLoader {
|
|
369
|
+
constructor(callbacks) {
|
|
370
|
+
this.loading_ = new Map();
|
|
371
|
+
this.callbacks_ = callbacks;
|
|
372
|
+
}
|
|
373
|
+
get activeCount() {
|
|
374
|
+
return this.loading_.size;
|
|
375
|
+
}
|
|
376
|
+
startTask(task, map, stats) {
|
|
377
|
+
var _a, _b, _c;
|
|
378
|
+
const source = task.layer.getSource();
|
|
379
|
+
if (!source) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const projection = map.getView().getProjection();
|
|
383
|
+
const pixelRatio = (_c = (_b = (_a = map).getPixelRatio) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : 1;
|
|
384
|
+
const category = task.category;
|
|
385
|
+
let tile;
|
|
386
|
+
try {
|
|
387
|
+
tile = source.getTile(task.tileCoord[0], task.tileCoord[1], task.tileCoord[2], pixelRatio, projection);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (!tile) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const state = tile.getState();
|
|
396
|
+
if (state === TileState.LOADED) {
|
|
397
|
+
stats.recordAlreadyLoaded(category);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (state === TileState.LOADING) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this.loading_.set(task.id, { task, unlisten: () => { } });
|
|
404
|
+
stats.recordLoadingStart(category);
|
|
405
|
+
const taskId = task.id;
|
|
406
|
+
const onTileChange = () => {
|
|
407
|
+
const newState = tile.getState();
|
|
408
|
+
if (newState !== TileState.LOADED &&
|
|
409
|
+
newState !== TileState.ERROR &&
|
|
410
|
+
newState !== TileState.EMPTY) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
tile.removeEventListener('change', onTileChange);
|
|
414
|
+
if (!this.loading_.has(taskId)) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.loading_.delete(taskId);
|
|
418
|
+
stats.recordLoadingEnd(category);
|
|
419
|
+
if (newState === TileState.LOADED) {
|
|
420
|
+
stats.recordLoaded(category);
|
|
421
|
+
}
|
|
422
|
+
else if (newState === TileState.ERROR) {
|
|
423
|
+
stats.recordError(category, this.buildErrorEntry_(task, tile));
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
stats.recordEmpty(category);
|
|
427
|
+
}
|
|
428
|
+
this.callbacks_.onStatsChanged();
|
|
429
|
+
this.callbacks_.onSlotFreed();
|
|
430
|
+
};
|
|
431
|
+
this.loading_.set(taskId, {
|
|
432
|
+
task,
|
|
433
|
+
unlisten: () => tile.removeEventListener('change', onTileChange),
|
|
434
|
+
});
|
|
435
|
+
tile.addEventListener('change', onTileChange);
|
|
436
|
+
tile.load();
|
|
437
|
+
}
|
|
438
|
+
abandonAll(stats) {
|
|
439
|
+
for (const [, entry] of this.loading_) {
|
|
440
|
+
entry.unlisten();
|
|
441
|
+
stats.recordLoadingEnd(entry.task.category);
|
|
442
|
+
}
|
|
443
|
+
this.loading_.clear();
|
|
444
|
+
}
|
|
445
|
+
buildErrorEntry_(task, tile) {
|
|
446
|
+
const layerName = task.layer.get('name') || task.layer.get('label') || 'unknown';
|
|
447
|
+
const anyTile = tile;
|
|
448
|
+
let reason = anyTile._prefetchError;
|
|
449
|
+
if (!reason) {
|
|
450
|
+
const src = task.layer.getSource();
|
|
451
|
+
if (src && typeof src.getUrls === 'function') {
|
|
452
|
+
const urls = src.getUrls();
|
|
453
|
+
if (urls && urls.length > 0) {
|
|
454
|
+
try {
|
|
455
|
+
reason = `Tile load failed (${new URL(urls[0]).hostname})`;
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
reason = 'Tile load failed';
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!reason) {
|
|
463
|
+
reason = 'Tile load failed';
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
tileCoord: task.tileCoord,
|
|
468
|
+
category: getCategoryName(task.category),
|
|
469
|
+
layerName,
|
|
470
|
+
reason,
|
|
471
|
+
timestamp: Date.now(),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
dispose() {
|
|
475
|
+
for (const [, entry] of this.loading_) {
|
|
476
|
+
entry.unlisten();
|
|
477
|
+
}
|
|
478
|
+
this.loading_.clear();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* @module ol/prefetch/PrefetchManager
|
|
484
|
+
*/
|
|
485
|
+
/**
|
|
486
|
+
* Manages controlled prefetching of tiles across multiple layers and locations.
|
|
487
|
+
*
|
|
488
|
+
* Priority order:
|
|
489
|
+
* 1. User interactions (pan/zoom) always get absolute priority (managed by OL's TileQueue).
|
|
490
|
+
* 2. Spatial prefetching: tiles around the visible viewport for the active layer.
|
|
491
|
+
* 3. Background layer prefetching: load tiles for hidden layers at current viewport.
|
|
492
|
+
* 4. Anticipated navigation prefetching: preload tiles at the next expected location.
|
|
493
|
+
*
|
|
494
|
+
* When the user interacts with the map (pan/zoom), all prefetching is paused
|
|
495
|
+
* and only resumes after the user stops interacting.
|
|
496
|
+
*/
|
|
497
|
+
class PrefetchManager {
|
|
498
|
+
constructor(options) {
|
|
499
|
+
var _a, _b, _c, _d, _e;
|
|
500
|
+
this.userInteracting_ = false;
|
|
501
|
+
this.idleTimeout_ = null;
|
|
502
|
+
this.backgroundLayers_ = [];
|
|
503
|
+
this.activeLayer_ = null;
|
|
504
|
+
this.nextTarget_ = null;
|
|
505
|
+
this.categoryPriorities_ = {
|
|
506
|
+
...DEFAULT_CATEGORY_PRIORITIES,
|
|
507
|
+
};
|
|
508
|
+
this.queue_ = [];
|
|
509
|
+
this.stats_ = new PrefetchStats();
|
|
510
|
+
this.listenerKeys_ = [];
|
|
511
|
+
this.map_ = options.map;
|
|
512
|
+
this.maxConcurrentPrefetches_ = (_a = options.maxConcurrentPrefetches) !== null && _a !== void 0 ? _a : 12;
|
|
513
|
+
this.idleDelay_ = (_b = options.idleDelay) !== null && _b !== void 0 ? _b : 300;
|
|
514
|
+
this.enabled_ = (_c = options.enabled) !== null && _c !== void 0 ? _c : true;
|
|
515
|
+
this.planner_ = new PrefetchPlanner((_d = options.spatialBufferFactor) !== null && _d !== void 0 ? _d : 1.5);
|
|
516
|
+
this.loader_ = new TileLoader({
|
|
517
|
+
onSlotFreed: () => {
|
|
518
|
+
if (!this.userInteracting_) {
|
|
519
|
+
this.fillSlots_();
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
onStatsChanged: () => this.notifyStats_(),
|
|
523
|
+
});
|
|
524
|
+
this.scheduler_ = new PrefetchScheduler((_e = options.tickInterval) !== null && _e !== void 0 ? _e : 200, {
|
|
525
|
+
onRebuildNeeded: () => this.rebuildQueue_(),
|
|
526
|
+
onFillSlots: () => this.fillSlots_(),
|
|
527
|
+
onStatsChanged: () => this.notifyStats_(),
|
|
528
|
+
});
|
|
529
|
+
this.setupListeners_();
|
|
530
|
+
}
|
|
531
|
+
setupListeners_() {
|
|
532
|
+
const map = this.map_;
|
|
533
|
+
this.listenerKeys_.push(listen(map, MapEventType.MOVESTART, this.onMoveStart_, this), listen(map, MapEventType.MOVEEND, this.onMoveEnd_, this), listen(map, MapEventType.POSTRENDER, this.onPostRender_, this));
|
|
534
|
+
}
|
|
535
|
+
onMoveStart_() {
|
|
536
|
+
this.userInteracting_ = true;
|
|
537
|
+
if (this.idleTimeout_) {
|
|
538
|
+
clearTimeout(this.idleTimeout_);
|
|
539
|
+
this.idleTimeout_ = null;
|
|
540
|
+
}
|
|
541
|
+
this.loader_.abandonAll(this.stats_);
|
|
542
|
+
this.queue_ = this.queue_.filter((task) => task.category === PrefetchCategory.NEXT_NAV_ACTIVE ||
|
|
543
|
+
task.category === PrefetchCategory.NEXT_NAV_BACKGROUND);
|
|
544
|
+
this.stats_.resetQueuedCounts();
|
|
545
|
+
for (const task of this.queue_) {
|
|
546
|
+
this.stats_.recordQueued(task.category);
|
|
547
|
+
}
|
|
548
|
+
this.scheduler_.dispose();
|
|
549
|
+
this.notifyStats_();
|
|
550
|
+
}
|
|
551
|
+
onMoveEnd_() {
|
|
552
|
+
if (this.idleTimeout_) {
|
|
553
|
+
clearTimeout(this.idleTimeout_);
|
|
554
|
+
}
|
|
555
|
+
this.idleTimeout_ = setTimeout(() => {
|
|
556
|
+
this.userInteracting_ = false;
|
|
557
|
+
this.rebuildQueue_();
|
|
558
|
+
this.scheduler_.scheduleTick();
|
|
559
|
+
this.notifyStats_();
|
|
560
|
+
}, this.idleDelay_);
|
|
561
|
+
}
|
|
562
|
+
onPostRender_() {
|
|
563
|
+
if (!this.userInteracting_ && this.enabled_) {
|
|
564
|
+
this.scheduler_.scheduleTick();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
rebuildQueue_() {
|
|
568
|
+
if (this.userInteracting_) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.queue_ = this.planner_.buildQueue(this.map_, this.activeLayer_, this.backgroundLayers_, this.nextTarget_, this.categoryPriorities_, this.stats_);
|
|
572
|
+
this.notifyStats_();
|
|
573
|
+
}
|
|
574
|
+
fillSlots_() {
|
|
575
|
+
if (!this.enabled_ || this.userInteracting_) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (this.queue_.length === 0 && this.loader_.activeCount === 0) {
|
|
579
|
+
this.notifyStats_();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const mapTileQueue = this.getMapTileQueue_();
|
|
583
|
+
while (this.loader_.activeCount < this.maxConcurrentPrefetches_ &&
|
|
584
|
+
this.queue_.length > 0) {
|
|
585
|
+
if (this.userInteracting_) {
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
if (mapTileQueue && mapTileQueue.getTilesLoading() > 0) {
|
|
589
|
+
this.scheduler_.scheduleTick();
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
const task = this.queue_.shift();
|
|
593
|
+
if (!task) {
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
this.loader_.startTask(task, this.map_, this.stats_);
|
|
597
|
+
}
|
|
598
|
+
if (this.queue_.length > 0 && this.loader_.activeCount === 0) {
|
|
599
|
+
this.scheduler_.scheduleTick();
|
|
600
|
+
}
|
|
601
|
+
this.notifyStats_();
|
|
602
|
+
}
|
|
603
|
+
getMapTileQueue_() {
|
|
604
|
+
var _a;
|
|
605
|
+
const mapAny = this.map_;
|
|
606
|
+
return (_a = mapAny.tileQueue_) !== null && _a !== void 0 ? _a : null;
|
|
607
|
+
}
|
|
608
|
+
notifyStats_() {
|
|
609
|
+
const snapshot = this.stats_.getSnapshot(this.queue_.length, this.loader_.activeCount, this.userInteracting_, this.nextTarget_, this.categoryPriorities_);
|
|
610
|
+
this.stats_.notify(snapshot);
|
|
611
|
+
}
|
|
612
|
+
addBackgroundLayer(layer, priority = 0) {
|
|
613
|
+
const exists = this.backgroundLayers_.some((e) => e.layer === layer);
|
|
614
|
+
if (!exists) {
|
|
615
|
+
this.backgroundLayers_.push({ layer, priority });
|
|
616
|
+
this.backgroundLayers_.sort((a, b) => a.priority - b.priority);
|
|
617
|
+
this.rebuildQueue_();
|
|
618
|
+
this.scheduler_.scheduleTick();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
removeBackgroundLayer(layer) {
|
|
622
|
+
const idx = this.backgroundLayers_.findIndex((e) => e.layer === layer);
|
|
623
|
+
if (idx >= 0) {
|
|
624
|
+
this.backgroundLayers_.splice(idx, 1);
|
|
625
|
+
this.rebuildQueue_();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
setBackgroundLayerPriority(layer, priority) {
|
|
629
|
+
const entry = this.backgroundLayers_.find((e) => e.layer === layer);
|
|
630
|
+
if (entry) {
|
|
631
|
+
entry.priority = priority;
|
|
632
|
+
this.backgroundLayers_.sort((a, b) => a.priority - b.priority);
|
|
633
|
+
this.rebuildQueue_();
|
|
634
|
+
this.scheduler_.scheduleTick();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
getBackgroundLayers() {
|
|
638
|
+
return this.backgroundLayers_.map((e) => ({ layer: e.layer, priority: e.priority }));
|
|
639
|
+
}
|
|
640
|
+
setActiveLayer(layer) {
|
|
641
|
+
this.activeLayer_ = layer;
|
|
642
|
+
this.rebuildQueue_();
|
|
643
|
+
this.scheduler_.scheduleTick();
|
|
644
|
+
}
|
|
645
|
+
setNextTarget(center, zoom) {
|
|
646
|
+
this.nextTarget_ = { center, zoom };
|
|
647
|
+
this.rebuildQueue_();
|
|
648
|
+
this.scheduler_.scheduleTick();
|
|
649
|
+
}
|
|
650
|
+
clearNextTarget() {
|
|
651
|
+
this.nextTarget_ = null;
|
|
652
|
+
}
|
|
653
|
+
setEnabled(enabled) {
|
|
654
|
+
this.enabled_ = enabled;
|
|
655
|
+
this.scheduler_.enabled = enabled;
|
|
656
|
+
if (enabled) {
|
|
657
|
+
this.rebuildQueue_();
|
|
658
|
+
this.scheduler_.scheduleTick();
|
|
659
|
+
}
|
|
660
|
+
this.notifyStats_();
|
|
661
|
+
}
|
|
662
|
+
getEnabled() {
|
|
663
|
+
return this.enabled_;
|
|
664
|
+
}
|
|
665
|
+
setMaxConcurrent(max) {
|
|
666
|
+
this.maxConcurrentPrefetches_ = Math.max(1, max);
|
|
667
|
+
this.fillSlots_();
|
|
668
|
+
}
|
|
669
|
+
getMaxConcurrent() {
|
|
670
|
+
return this.maxConcurrentPrefetches_;
|
|
671
|
+
}
|
|
672
|
+
setCategoryPriorities(priorities) {
|
|
673
|
+
for (const key in priorities) {
|
|
674
|
+
const typedKey = key;
|
|
675
|
+
if (typedKey in this.categoryPriorities_ && priorities[typedKey] !== undefined) {
|
|
676
|
+
this.categoryPriorities_[typedKey] = priorities[typedKey];
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
this.rebuildQueue_();
|
|
680
|
+
this.scheduler_.scheduleTick();
|
|
681
|
+
this.notifyStats_();
|
|
682
|
+
}
|
|
683
|
+
getCategoryPriorities() {
|
|
684
|
+
return { ...this.categoryPriorities_ };
|
|
685
|
+
}
|
|
686
|
+
onStats(callback) {
|
|
687
|
+
this.stats_.onStats(callback);
|
|
688
|
+
}
|
|
689
|
+
getStats() {
|
|
690
|
+
return this.stats_.getSnapshot(this.queue_.length, this.loader_.activeCount, this.userInteracting_, this.nextTarget_, this.categoryPriorities_);
|
|
691
|
+
}
|
|
692
|
+
dispose() {
|
|
693
|
+
for (const key of this.listenerKeys_) {
|
|
694
|
+
unlistenByKey(key);
|
|
695
|
+
}
|
|
696
|
+
this.listenerKeys_.length = 0;
|
|
697
|
+
if (this.idleTimeout_) {
|
|
698
|
+
clearTimeout(this.idleTimeout_);
|
|
699
|
+
}
|
|
700
|
+
this.scheduler_.dispose();
|
|
701
|
+
this.loader_.dispose();
|
|
702
|
+
this.stats_.dispose();
|
|
703
|
+
this.queue_ = [];
|
|
704
|
+
this.backgroundLayers_ = [];
|
|
705
|
+
this.activeLayer_ = null;
|
|
706
|
+
this.nextTarget_ = null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export { PrefetchCategory, PrefetchManager as default };
|
|
711
|
+
//# sourceMappingURL=PrefetchManager.esm.js.map
|