All files / app/services/preloader preloader.service.ts

100% Statements 100/100
100% Branches 29/29
100% Functions 12/12
100% Lines 92/92

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334                                1x         663x         663x                       663x               663x                       663x               663x   663x 1326x         1326x         1326x 1326x     663x 663x                                         453x 453x   453x   453x 453x   453x 453x   453x 453x 453x                     168x 168x                     132x 132x                                     808x 808x 808x 808x 458x   808x 458x     808x 808x                                       337x   337x 6x         337x   337x 94x 94x 94x 94x     243x 243x 243x   243x 176x 176x   176x 352x       243x 243x 243x                   1332x                       276x 249x 434x   179x   27x 30x   9x                         2176x 2176x 1308x 2616x   1308x 1012x 1012x 2024x     868x 886x   868x 598x 598x 610x     1610x                         1628x 1628x 1018x 2036x     610x 625x     1628x                         2128x 2128x   1580x   1580x 1580x      
import { Injectable } from '@angular/core';
import { LogService } from '../log/log.service';
import { BehaviorSubject } from 'rxjs';
import { LoadingInfo } from './loadingInfo/loadingInfo';
import { Preloaders } from './preloaders/preloaders';
 
/**
 * Preloader service, used to manage asset loading, so that the app may display
 * animations while the data loads. Multiple preloaders can be responsible for
 * the loading of different data, and any data can be associated with multiple
 * preloaders. Inspired by
 * https://blog.slinto.sk/angular-http-preloaders-3ee7bd937ee0
 */
@Injectable({
  providedIn: 'root',
})
export class PreloaderService {
  /**
   * Tracks how many elements are loading for each preloader and if each
   * preloader is loading at all
   */
  info: Map<Preloaders, LoadingInfo> = new Map<Preloaders, LoadingInfo>();
  /**
   * Maps every preloader to an observable, that will emit a boolean
   * corresponding to whether or not the preloader is loading or not.
   */
  statusLoading: Map<Preloaders, BehaviorSubject<boolean | null>> = new Map<
    Preloaders,
    BehaviorSubject<boolean | null>
  >();
  /**
   * Observable that will emit a boolean corresponding to whether or not any
   * preloader is loading.
   */
  statusAnyLoading: BehaviorSubject<boolean | null>;
  /** Logger. See {@link LogService} */
  logger: LogService;
  /** Max quantity to load for each preloader. */
  maxQty: Map<Preloaders, number> = new Map<Preloaders, number>();
  /**
   * Previous max quantity stored. Usefull when a preloader has finished loading
   * (hence the max quantity is set to 0) but other preloars haven't. If we want
   * to know the progression along all preloaders, we use this map to get the
   * old max total. Other wise, the total pourcentage would jump back a bit each
   * time a preloader finishes loading.
   */
  oldMaxQty: Map<Preloaders, number> = new Map<Preloaders, number>();
 
  /**
   * Loading message. Emits when something has to load as well as when something
   * as loaded.
   */
  loadingMessage: BehaviorSubject<string>;
 
  /**
   * Whether or not the entire app is loading. Usefull when first loading the
   * app, but also on language change for instance.
   */
  isMainLoad = true;
 
  /**
   * Preloader service constructor.
   *
   * @param logService The {@link LogService}
   */
  constructor(logService: LogService) {
    this.logger = logService.withClassName('PreloaderService');
 
    Object.keys(Preloaders).forEach((element) => {
      this.info.set(element as Preloaders, {
        qtyToLoad: 0,
        isLoading: false,
      });
 
      this.statusLoading.set(
        element as Preloaders,
        new BehaviorSubject<boolean | null>(null)
      );
 
      this.maxQty.set(element as Preloaders, 0);
      this.oldMaxQty.set(element as Preloaders, 0);
    });
 
    this.statusAnyLoading = new BehaviorSubject<boolean | null>(null);
    this.loadingMessage = new BehaviorSubject<string>('');
  }
 
  /**
   * Set up a given {@link Preloaders} with an amount of data to load.
   *
   * @param loader The {@link Preloaders}
   * @param qty The quantity
   * @param message The preloader message
   * @param withPreloaderTot If the pourcentage for the preloader should be
   *   displayed in the message
   * @param withTot If the total pourcentage should be displayed in the message
   * @returns The message
   */
  toLoad(
    loader: Preloaders,
    qty: number,
    message = '',
    withPreloaderTot = true,
    withTot = true
  ): string {
    const oldQty = this.info.get(loader)?.qtyToLoad as number;
    const newQty = oldQty + qty;
 
    this.info.set(loader, { isLoading: true, qtyToLoad: newQty });
 
    this.statusLoading.get(loader)?.next(true);
    this.statusAnyLoading.next(true);
 
    this.maxQty.set(loader, (this.maxQty.get(loader) as number) + qty);
    this.oldMaxQty.set(loader, this.maxQty.get(loader) as number);
 
    if (!message) message = this.defaultToLoadMessage(loader, qty);
    message = this.formatMessage(message, loader, withPreloaderTot, withTot);
    return message;
  }
 
  /**
   * Default to load message, if none is specified
   *
   * @param loader The {@link Preloaders}
   * @param qty The quantity
   * @returns The message
   */
  private defaultToLoadMessage(loader: Preloaders, qty: number): string {
    const message = 'LOADING... ' + loader + ' - ' + qty;
    return message;
  }
 
  /**
   * Default loaded message, if none is specified
   *
   * @param loader The {@link Preloaders}
   * @param qty The quantity
   * @returns The message
   */
  private defaultLoadedMessage(loader: Preloaders, qty: number): string {
    const message = 'LOADED... ' + loader + ' - ' + qty;
    return message;
  }
 
  /**
   * Format the message to be emitted.
   *
   * @param message The preloader message
   * @param loader The {@link Preloaders}
   * @param withPreloaderTot If the pourcentage for the preloader should be
   *   displayed in the message
   * @param withTot If the total pourcentage should be displayed in the message
   * @returns The message
   */
  formatMessage(
    message: string,
    loader: Preloaders,
    withPreloaderTot = true,
    withTot = true
  ): string {
    const loaderProgress = +this.getProgressionPercent(loader).toFixed(2);
    const totProgress = +this.getProgressionPercent().toFixed(2);
    this.logger.debug(message, loaderProgress + '%', totProgress + '%');
    if (withPreloaderTot) {
      message = message + ' - ' + loaderProgress + '%';
    }
    if (withTot) {
      message =
        message + ' (' + +this.getProgressionPercent().toFixed(2) + '%)';
    }
    this.loadingMessage.next(message);
    return message;
  }
 
  /**
   * Call when an amount of data has been loaded for a given {@link Preloaders}
   *
   * @param loader The {@link Preloaders}
   * @param qty The quantity
   * @param message The preloader message
   * @param withPreloaderTot If the pourcentage for the preloader should be
   *   displayed in the message
   * @param withTot If the total pourcentage should be displayed in the message
   */
  loaded(
    loader: Preloaders,
    qty: number,
    message = '',
    withPreloaderTot = true,
    withTot = true
  ) {
    const oldQty = this.info.get(loader)?.qtyToLoad as number;
 
    if (oldQty - qty < 0) {
      this.logger.warn(
        'More data was loaded than expected for the preloader ',
        loader
      );
    }
    const newQty = oldQty - qty < 0 ? 0 : oldQty - qty;
 
    if (newQty !== 0) {
      this.info.set(loader, { isLoading: true, qtyToLoad: newQty });
      if (!message) message = this.defaultLoadedMessage(loader, qty);
      message = this.formatMessage(message, loader, withPreloaderTot, withTot);
      return message;
    }
 
    this.maxQty.set(loader, 0);
    this.info.set(loader, { isLoading: false, qtyToLoad: 0 });
    this.statusLoading.get(loader)?.next(false);
 
    if (!this.isAnyLoading()) {
      this.isMainLoad = false;
      this.statusAnyLoading.next(false);
 
      for (const entry of this.oldMaxQty.entries()) {
        this.oldMaxQty.set(entry[0], 0);
      }
    }
 
    if (!message) message = this.defaultLoadedMessage(loader, qty);
    message = this.formatMessage(message, loader, withPreloaderTot, withTot);
    return message;
  }
 
  /**
   * Returns whether or not a given {@link Preloaders} is loading.
   *
   * @param loader The {@link Preloaders}
   * @returns If he {@link Preloaders} is loading
   */
  isLoading(loader: Preloaders) {
    return this.info.get(loader)?.isLoading as boolean;
  }
 
  /**
   * Checks if any {@link Preloaders} is loading. If nothing is given as input,
   * it checks for every existing {@link Preloaders}, otherwise it checks for the
   * {@link Preloaders} given as input.
   *
   * @param loaders The {@link Preloaders} to check
   * @returns If any {@link Preloaders} is loading
   */
  isAnyLoading(...loaders: Preloaders[]) {
    if (loaders.length == 0) {
      for (const iterator of this.info.entries()) {
        if (iterator[1].isLoading) return true;
      }
      return false;
    } else {
      for (const loader of loaders) {
        if (this.info.get(loader)?.isLoading) return true;
      }
      return false;
    }
  }
 
  /**
   * Get the total max elements to load for every {@link Preloaders}.If nothing
   * is given as input, it checks for every existing {@link Preloaders},
   * otherwise it checks for the {@link Preloaders} given as input.
   *
   * @param loaders The {@link Preloaders} to check
   * @returns The max number of elements to load
   */
  getTotalMaxElToLoad(...loaders: Preloaders[]) {
    let res = 0;
    if (loaders.length == 0) {
      for (const iterator of this.maxQty.entries()) {
        res += iterator[1];
      }
      if (res == 0) return res;
      res = 0;
      for (const iterator of this.oldMaxQty.entries()) {
        res += iterator[1];
      }
    } else {
      for (const loader of loaders) {
        res += this.maxQty.get(loader) as number;
      }
      if (res == 0) return res;
      res = 0;
      for (const loader of loaders) {
        res += this.oldMaxQty.get(loader) as number;
      }
    }
    return res;
  }
 
  /**
   * Get the total number of elements still having to load for every
   * {@link Preloaders}.If nothing is given as input, it checks for every
   * existing {@link Preloaders}, otherwise it checks for the {@link Preloaders}
   * given as input.
   *
   * @param loaders The {@link Preloaders} to check
   * @returns The total number of elements still having to load
   */
  getTotalElToLoad(...loaders: Preloaders[]) {
    let res = 0;
    if (loaders.length == 0) {
      for (const iterator of this.info.entries()) {
        res += iterator[1].qtyToLoad;
      }
    } else {
      for (const loader of loaders) {
        res += this.info.get(loader)?.qtyToLoad as number;
      }
    }
    return res;
  }
 
  /**
   * Get the progression percentage for {@link Preloaders}.If nothing is given as
   * input, it computes the value taking into account every existing
   * {@link Preloaders}, otherwise it does only for the {@link Preloaders} given
   * as input.
   *
   * @param loaders The {@link Preloaders}
   * @returns The progression percentage
   */
  getProgressionPercent(...loaders: Preloaders[]) {
    const maxElToLoad = this.getTotalMaxElToLoad(...loaders);
    if (maxElToLoad === 0) return 100;
 
    const totalElToLoad = this.getTotalElToLoad(...loaders);
 
    const res = (totalElToLoad / maxElToLoad) * 100;
    return 100 - res;
  }
}