Skip to content

Commit f8c8e58

Browse files
committed
refactor: Move layer loading progress functionality to separate service
1 parent 2179dc2 commit f8c8e58

File tree

2 files changed

+263
-236
lines changed

2 files changed

+263
-236
lines changed
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import {Cluster, Source} from 'ol/source';
2+
import {Injectable, NgZone} from '@angular/core';
3+
import {Subject, buffer, debounceTime, pairwise} from 'rxjs';
4+
5+
import {HsConfig} from 'hslayers-ng/config';
6+
import {HsEventBusService} from 'hslayers-ng/shared/event-bus';
7+
import {HsLanguageService} from 'hslayers-ng/shared/language';
8+
import {HsLayerDescriptor, HsLayerLoadProgress} from 'hslayers-ng/types';
9+
import {HsLogService} from 'hslayers-ng/shared/log';
10+
import {HsToastService} from 'hslayers-ng/common/toast';
11+
import {HsUtilsService} from 'hslayers-ng/shared/utils';
12+
import {
13+
Image as ImageLayer,
14+
Layer,
15+
Tile,
16+
Vector as VectorLayer,
17+
} from 'ol/layer';
18+
import {getBase, getTitle} from 'hslayers-ng/common/extensions';
19+
20+
@Injectable({
21+
providedIn: 'root',
22+
})
23+
export class HsLayerManagerLoadingProgressService {
24+
lastProgressUpdate: number;
25+
26+
constructor(
27+
private hsConfig: HsConfig,
28+
private hsLog: HsLogService,
29+
private zone: NgZone,
30+
private hsUtilsService: HsUtilsService,
31+
private hsToastService: HsToastService,
32+
private hsLanguageService: HsLanguageService,
33+
private hsEventBusService: HsEventBusService,
34+
) {}
35+
36+
private determineLayerType(olLayer) {
37+
if (this.hsUtilsService.instOf(olLayer, VectorLayer)) {
38+
return 'features';
39+
} else if (this.hsUtilsService.instOf(olLayer, ImageLayer)) {
40+
return 'image';
41+
} else if (this.hsUtilsService.instOf(olLayer, Tile)) {
42+
return 'tile';
43+
}
44+
return undefined;
45+
}
46+
47+
private loadError(
48+
loadProgress: HsLayerLoadProgress,
49+
olLayer: Layer<Source>,
50+
typeCallback?: (
51+
loadProgress: HsLayerLoadProgress,
52+
olLayer: Layer<Source>,
53+
) => any,
54+
) {
55+
loadProgress.loadError += 1;
56+
this.changeLoadCounter(olLayer, loadProgress, -1);
57+
if (typeCallback) {
58+
typeCallback.bind(this)(loadProgress, olLayer);
59+
}
60+
}
61+
62+
private featuresLoadFailed(
63+
loadProgress: HsLayerLoadProgress,
64+
olLayer: Layer<Source>,
65+
) {
66+
if (loadProgress) {
67+
loadProgress.error = true;
68+
}
69+
this.hsToastService.createToastPopupMessage(
70+
'LAYERS.featuresLoadError',
71+
`${getTitle(
72+
olLayer,
73+
)}: ${this.hsLanguageService.getTranslationIgnoreNonExisting(
74+
'ADDLAYERS.ERROR',
75+
'someErrorHappened',
76+
null,
77+
)}`,
78+
{},
79+
);
80+
}
81+
82+
private imageLoadFailed(
83+
loadProgress: HsLayerLoadProgress,
84+
olLayer: Layer<Source>,
85+
) {
86+
loadProgress.loaded = true;
87+
loadProgress.error = true;
88+
this.hsEventBusService.layerLoads.next(olLayer);
89+
}
90+
91+
private tileLoadFailed(
92+
loadProgress: HsLayerLoadProgress,
93+
olLayer: Layer<Source>,
94+
) {
95+
if (loadProgress.loadError == loadProgress.total) {
96+
loadProgress.error = true;
97+
}
98+
}
99+
100+
/**
101+
* Create events for checking whether the layer is being loaded or is loaded
102+
* @param layer - Layer which is being added
103+
*/
104+
loadingEvents(layer: HsLayerDescriptor): void {
105+
const olLayer = layer.layer;
106+
if (getBase(olLayer) && this.hsConfig.componentsEnabled.basemapGallery) {
107+
return;
108+
}
109+
const source: any = olLayer.get('cluster')
110+
? (olLayer.getSource() as Cluster).getSource()
111+
: olLayer.getSource();
112+
if (!source) {
113+
this.hsLog.error(`Layer ${getTitle(olLayer)} has no source`);
114+
return;
115+
}
116+
const loadProgress: HsLayerLoadProgress = {
117+
pending: 0,
118+
total: 0,
119+
loadError: 0,
120+
loaded: true,
121+
error: undefined,
122+
percents: 0,
123+
};
124+
layer.loadProgress = loadProgress;
125+
126+
const layerType = this.determineLayerType(olLayer);
127+
128+
if (!layerType) {
129+
return;
130+
}
131+
132+
this.createLoadingProgressTimer(loadProgress, olLayer);
133+
const loadStart = this.subscribeToEventSubject(1, loadProgress, olLayer);
134+
const loadEnd = this.subscribeToEventSubject(-1, loadProgress, olLayer);
135+
136+
source.on(`${layerType}loadstart`, (event) => {
137+
loadStart.next(true);
138+
});
139+
source.on(`${layerType}loadend`, (event) => {
140+
loadEnd.next(true);
141+
loadProgress.error = false;
142+
});
143+
source.on(`${layerType}loaderror`, (event) => {
144+
this.loadError(loadProgress, olLayer, this[`${layerType}LoadFailed`]);
145+
});
146+
147+
if (layerType == 'features') {
148+
source.on('propertychange', (event) => {
149+
if (event.key == 'loaded') {
150+
if (event.oldValue == false) {
151+
this.hsEventBusService.layerLoads.next(olLayer);
152+
} else {
153+
this.hsEventBusService.layerLoadings.next({
154+
layer: olLayer,
155+
progress: loadProgress,
156+
});
157+
}
158+
}
159+
});
160+
}
161+
}
162+
163+
/**
164+
* Creates loading progress timer which controls the executions of load events callbacks
165+
* and tries to reset progress once the loading has finished (no execution in 2000ms)
166+
*/
167+
private createLoadingProgressTimer(
168+
loadProgress: HsLayerLoadProgress,
169+
olLayer: Layer<Source>,
170+
) {
171+
loadProgress.timer = new Subject();
172+
/**
173+
* NOTE:
174+
* pairwise is a hacky solution for the cases when pending numbers get out of sync
175+
* eg. everything has been loaded but pending value is not 0.
176+
* Could not find the root cause of the problem
177+
*/
178+
loadProgress.timer
179+
.pipe(debounceTime(2000), pairwise())
180+
.subscribe(([previous, current]) => {
181+
if (
182+
loadProgress.pending == 0 ||
183+
(previous === current && current != 0)
184+
) {
185+
this.zone.run(() => {
186+
loadProgress.total = 0;
187+
if (current != 0) {
188+
loadProgress.pending = 0;
189+
}
190+
loadProgress.percents = 0;
191+
this.hsEventBusService.layerLoads.next(olLayer);
192+
});
193+
}
194+
});
195+
}
196+
197+
/**
198+
* Create an event subject which is used to cast value in an event callback.
199+
* and
200+
* Subscribe to an subject to allow debouncing of event callback method.
201+
* Subscription increments or decrements pending parameter of loadProgress which is used to indicate progress in UI
202+
*/
203+
private subscribeToEventSubject(
204+
signMultiplier: 1 | -1,
205+
loadProgress: HsLayerLoadProgress,
206+
olLayer: Layer<Source>,
207+
): Subject<boolean> {
208+
const subject: Subject<boolean> = new Subject();
209+
subject
210+
.pipe(
211+
//Buffer emitions to an array until closing notifier emits.
212+
buffer(
213+
// In case 100ms has passed without another emit => close buffer and emit value
214+
subject.pipe(debounceTime(100)),
215+
),
216+
)
217+
.subscribe((loads) => {
218+
loadProgress.total += signMultiplier == 1 ? loads.length : 0;
219+
this.changeLoadCounter(
220+
olLayer,
221+
loadProgress,
222+
loads.length * signMultiplier,
223+
);
224+
});
225+
return subject;
226+
}
227+
228+
/**
229+
* Adjust layer progress counter object and calculate loading state (percentages)
230+
* change is positive number in case of loadStart and negative number in case of loadEnd/Error events
231+
*/
232+
private changeLoadCounter(
233+
layer: Layer<Source>,
234+
progress: HsLayerLoadProgress,
235+
change: number,
236+
): void {
237+
progress.pending += change;
238+
progress.loaded = progress.pending === 0;
239+
let percents = 0;
240+
if (progress.total > 0) {
241+
percents = Math.round(
242+
((progress.total - progress.pending) / progress.total) * 100,
243+
);
244+
}
245+
/**
246+
* Total is reset only after 2 seconds of idle state.
247+
* Panning sooner will make a progress bar UI animation to jump or 'backpaddle' unnecessarily.
248+
* Using 0 instead of 100 (when loading ended) prevents that
249+
*/
250+
this.zone.run(() => {
251+
progress.percents = percents === 100 ? 0 : percents;
252+
});
253+
progress.timer.next(progress.pending);
254+
this.hsEventBusService.layerLoadings.next({layer, progress});
255+
}
256+
}

0 commit comments

Comments
 (0)