res => {
      if (res[0] !== undefined) {
        this.cromosColeccion = res;
        this.OrdenarCromos();
        this.GET_ImagenCromo();
        console.log(res);
      } else {
        console.log('No hay cromos en esta coleccion');
        this.cromosColeccion = undefined;
      }
    }


response => {
        const blob = new Blob([response.blob()], { type: 'image/jpg'});

        const reader = new FileReader();
        reader.addEventListener('load', () => {
          this.imagenCromoArray[i] = reader.result.toString();
        }, false);

        if (blob) {
          reader.readAsDataURL(blob);
        }
    }


() => {
          this.imagenCromoArray[i] = reader.result.toString();
        }


(a, b) => a.Nombre.localeCompare(b.Nombre)


response => {
        const blob = new Blob([response.blob()], { type: 'image/jpg'});

        const reader = new FileReader();
        reader.addEventListener('load', () => {
          this.imagenColeccion = reader.result.toString();
        }, false);

        if (blob) {
          reader.readAsDataURL(blob);
        }
    }


@IonicPage()
@Component({
  selector: 'page-mis-cromos',
  templateUrl: 'mis-cromos.html',
})
export class MisCromosPage {

  // PARAMETROS QUE RECOGEMOS DE LA PAGINA PREVIA
  coleccion:Coleccion;

  //PARAMETROS DE UNA COLECCION DE CROMOS
  imagenColeccion: string;
  cromosColeccion: Cromo[];

  //PARAMETROS DE UN CROMO
  cromo: Cromo;
  imagenCromoArray: string[] = [];

  // URLs que utilizaremos
  private APIUrl = 'http://localhost:3000/api/Colecciones';

  constructor(public navCtrl: NavController, public navParams: NavParams,
              private http: Http, private https: HttpClient) {
    this.coleccion=navParams.get('coleccion');
  }

  //Se realizarán las siguiente tareas al inicializar la pantalla.
  ionViewDidLoad() {
    console.log('Bienvenido a la página de cromos de la coleccion');
    this.CromosEImagenDeLaColeccion(this.coleccion);
    this.GET_Imagen();
  }

  // Le pasamos la coleccion y buscamos la imagen que tiene y sus cromos
  CromosEImagenDeLaColeccion(coleccion: Coleccion) {

    console.log('entro a buscar cromos y foto');
    console.log(coleccion.ImagenColeccion);
    // Si la coleccion tiene una foto (recordemos que la foto no es obligatoria)
    if (coleccion.ImagenColeccion !== undefined) {

      // Busca en la base de datos la imágen con el nombre registrado en equipo.FotoEquipo y la recupera
      this.http.get('http://localhost:3000/api/imagenes/ImagenColeccion/download/' + coleccion.ImagenColeccion,
      { responseType: ResponseContentType.Blob })
      .subscribe(response => {
        const blob = new Blob([response.blob()], { type: 'image/jpg'});

        const reader = new FileReader();
        reader.addEventListener('load', () => {
          this.imagenColeccion = reader.result.toString();
        }, false);

        if (blob) {
          reader.readAsDataURL(blob);
        }
      });

      // Sino la imagenColeccion será undefined para que no nos pinte la foto de otro equipo préviamente seleccionado
    } else {
      this.imagenColeccion = undefined;
    }

    // Una vez tenemos el logo del equipo seleccionado, buscamos sus alumnos
    console.log('voy a mostrar los cromos de la coleccion ' + coleccion.id);

    // Busca los cromos dela coleccion en la base de datos
    this.https.get<Cromo[]>(this.APIUrl + '/' + coleccion.id + '/cromos')
    .subscribe(res => {
      if (res[0] !== undefined) {
        this.cromosColeccion = res;
        this.OrdenarCromos();
        this.GET_ImagenCromo();
        console.log(res);
      } else {
        console.log('No hay cromos en esta coleccion');
        this.cromosColeccion = undefined;
      }
    });
  }

   // Busca la imagen que tiene el nombre del cromo.Imagen y lo carga en imagenCromo
   GET_ImagenCromo() {

    // tslint:disable-next-line:prefer-for-of
  for (let i = 0; i < this.cromosColeccion.length; i++) {

    this.cromo = this.cromosColeccion[i];

    if (this.cromo.Imagen !== undefined ) {
      // Busca en la base de datos la imágen con el nombre registrado en equipo.FotoEquipo y la recupera
      this.http.get('http://localhost:3000/api/imagenes/ImagenCromo/download/' + this.cromo.Imagen,
      { responseType: ResponseContentType.Blob })
      .subscribe(response => {
        const blob = new Blob([response.blob()], { type: 'image/jpg'});

        const reader = new FileReader();
        reader.addEventListener('load', () => {
          this.imagenCromoArray[i] = reader.result.toString();
        }, false);

        if (blob) {
          reader.readAsDataURL(blob);
        }
    });
    }
  }
  }

// Ordena los cromos por nombre. Asi si tengo algun cromo repetido, salen juntos
  OrdenarCromos() {
  this.cromosColeccion.sort((a, b) => a.Nombre.localeCompare(b.Nombre));
  }

  // Busca la imagen que tiene el nombre del coleccion.ImagenColeccion y lo carga en imagenColeccion
  GET_Imagen() {

    if (this.coleccion.ImagenColeccion !== undefined ) {
      // Busca en la base de datos la imágen con el nombre registrado en equipo.FotoEquipo y la recupera
      this.http.get('http://localhost:3000/api/imagenes/ImagenColeccion/download/' + this.coleccion.ImagenColeccion,
      { responseType: ResponseContentType.Blob })
      .subscribe(response => {
        const blob = new Blob([response.blob()], { type: 'image/jpg'});

        const reader = new FileReader();
        reader.addEventListener('load', () => {
          this.imagenColeccion = reader.result.toString();
        }, false);

        if (blob) {
          reader.readAsDataURL(blob);
        }
    });

    }
  }
}


//Se realizarán las siguiente tareas al inicializar la pantalla.
ionViewDidLoad() {
    console.log('Bienvenido a la página de cromos de la coleccion');
    this.CromosEImagenDeLaColeccion(this.coleccion);
    this.GET_Imagen();
  }


// Le pasamos la coleccion y buscamos la imagen que tiene y sus cromos
CromosEImagenDeLaColeccion(coleccion: Coleccion) {

    console.log('entro a buscar cromos y foto');
    console.log(coleccion.ImagenColeccion);
    // Si la coleccion tiene una foto (recordemos que la foto no es obligatoria)
    if (coleccion.ImagenColeccion !== undefined) {

      // Busca en la base de datos la imágen con el nombre registrado en equipo.FotoEquipo y la recupera
      this.http.get('http://localhost:3000/api/imagenes/ImagenColeccion/download/' + coleccion.ImagenColeccion,
      { responseType: ResponseContentType.Blob })
      .subscribe(response => {
        const blob = new Blob([response.blob()], { type: 'image/jpg'});

        const reader = new FileReader();
        reader.addEventListener('load', () => {
          this.imagenColeccion = reader.result.toString();
        }, false);

        if (blob) {
          reader.readAsDataURL(blob);
        }
      });

      // Sino la imagenColeccion será undefined para que no nos pinte la foto de otro equipo préviamente seleccionado
    } else {
      this.imagenColeccion = undefined;
    }

    // Una vez tenemos el logo del equipo seleccionado, buscamos sus alumnos
    console.log('voy a mostrar los cromos de la coleccion ' + coleccion.id);

    // Busca los cromos dela coleccion en la base de datos
    this.https.get<Cromo[]>(this.APIUrl + '/' + coleccion.id + '/cromos')
    .subscribe(res => {
      if (res[0] !== undefined) {
        this.cromosColeccion = res;
        this.OrdenarCromos();
        this.GET_ImagenCromo();
        console.log(res);
      } else {
        console.log('No hay cromos en esta coleccion');
        this.cromosColeccion = undefined;
      }
    });
  }


// Busca la imagen que tiene el nombre del cromo.Imagen y lo carga en imagenCromo
GET_ImagenCromo() {

    // tslint:disable-next-line:prefer-for-of
  for (let i = 0; i < this.cromosColeccion.length; i++) {

    this.cromo = this.cromosColeccion[i];

    if (this.cromo.Imagen !== undefined ) {
      // Busca en la base de datos la imágen con el nombre registrado en equipo.FotoEquipo y la recupera
      this.http.get('http://localhost:3000/api/imagenes/ImagenCromo/download/' + this.cromo.Imagen,
      { responseType: ResponseContentType.Blob })
      .subscribe(response => {
        const blob = new Blob([response.blob()], { type: 'image/jpg'});

        const reader = new FileReader();
        reader.addEventListener('load', () => {
          this.imagenCromoArray[i] = reader.result.toString();
        }, false);

        if (blob) {
          reader.readAsDataURL(blob);
        }
    });
    }
  }
  }


// Ordena los cromos por nombre. Asi si tengo algun cromo repetido, salen juntos
OrdenarCromos() {
  this.cromosColeccion.sort((a, b) => a.Nombre.localeCompare(b.Nombre));
  }


// Busca la imagen que tiene el nombre del coleccion.ImagenColeccion y lo carga en imagenColeccion
GET_Imagen() {

    if (this.coleccion.ImagenColeccion !== undefined ) {
      // Busca en la base de datos la imágen con el nombre registrado en equipo.FotoEquipo y la recupera
      this.http.get('http://localhost:3000/api/imagenes/ImagenColeccion/download/' + this.coleccion.ImagenColeccion,
      { responseType: ResponseContentType.Blob })
      .subscribe(response => {
        const blob = new Blob([response.blob()], { type: 'image/jpg'});

        const reader = new FileReader();
        reader.addEventListener('load', () => {
          this.imagenColeccion = reader.result.toString();
        }, false);

        if (blob) {
          reader.readAsDataURL(blob);
        }
    });

    }
  }


l => !l.isDisabled


() => {
            abp.utils.setCookieValue(
                'Abp.Localization.CultureName',
                languageName,
                new Date(new Date().getTime() + 5 * 365 * 86400000), // 5 year
                abp.appPath,
            );

            window.location.reload();
        }


@Component({
    selector: 'language-switch',
    template: `

        {{currentLanguage.displayName}}

    <nz-dropdown-menu #languageMenuTpl="nzDropdownMenu">

                {{language.displayName}}

    </nz-dropdown-menu>
    `
})
export class LanguageSwitchComponent extends AppComponentBase
    implements OnInit {
    languages: abp.localization.ILanguageInfo[];
    currentLanguage: abp.localization.ILanguageInfo;

    constructor(
        injector: Injector,
        private _profileService: ProfileServiceProxy,
    ) {
        super(injector);
    }

    ngOnInit() {
        this.languages = _.filter(this.localization.languages, l => !l.isDisabled);
        this.currentLanguage = this.localization.currentLanguage;
    }

    changeLanguage(languageName: string): void {
        const input = new ChangeUserLanguageDto();
        input.languageName = languageName;

        this._profileService.changeLanguage(input).subscribe(() => {
            abp.utils.setCookieValue(
                'Abp.Localization.CultureName',
                languageName,
                new Date(new Date().getTime() + 5 * 365 * 86400000), // 5 year
                abp.appPath,
            );

            window.location.reload();
        });
    }
}


ngOnInit() {
        this.languages = _.filter(this.localization.languages, l => !l.isDisabled);
        this.currentLanguage = this.localization.currentLanguage;
    }


changeLanguage(languageName: string): void {
        const input = new ChangeUserLanguageDto();
        input.languageName = languageName;

        this._profileService.changeLanguage(input).subscribe(() => {
            abp.utils.setCookieValue(
                'Abp.Localization.CultureName',
                languageName,
                new Date(new Date().getTime() + 5 * 365 * 86400000), // 5 year
                abp.appPath,
            );

            window.location.reload();
        });
    }


(rack: LockerComponent) => {
          rack.resetComponent();
        }


rack => rack.selected


(rack: SimpleLockerComponent, ix: number) => {
          return {rack: rack, ix: ix};
        }


(rack: { rack: SimpleLockerComponent, ix: number }) => rack.rack.selected


(rack: { rack: SimpleLockerComponent, ix: number }) => rack.ix


(total, rack) => total + rack.dimension.y


btl => {
        btl.positions = btl.positions.map(
          pos => {
            let newPos = pos;
            if (pos.inLocker(this.fridge.id)) {
              newPos = new Position(pos.lockerId, pos.x, pos.y, pos.rack + 1);
            }
            return newPos;
          }
        );
      }


pos => {
            let newPos = pos;
            if (pos.inLocker(this.fridge.id)) {
              newPos = new Position(pos.lockerId, pos.x, pos.y, pos.rack + 1);
            }
            return newPos;
          }


btl => btl.positions.filter(
        pos => pos.inRack(this.fridge.id, 0)
      ).length > 0


pos => pos.inRack(this.fridge.id, 0)


btl => {
          btl.positions.forEach(
            pos => {
              let newPos = pos;
              if (pos.inLocker(this.fridge.id)) {
                newPos = new Position(pos.lockerId, pos.x, pos.y, pos.rack - 1);
              }
              return newPos;
            }
          );
        }


pos => {
              let newPos = pos;
              if (pos.inLocker(this.fridge.id)) {
                newPos = new Position(pos.lockerId, pos.x, pos.y, pos.rack - 1);
              }
              return newPos;
            }


btl => btl.positions.filter(
        pos => pos.inRack(this.fridge.id, rackIndex)
      ).length > 0


pos => pos.inRack(this.fridge.id, rackIndex)


(rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateY(rack.locker, 1);
          this.shiftBottles(ix, 0, 1);
        }
      }


(rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateY(rack.locker, -1);
          this.shiftBottles(ix, 0, -1);
        }
      }


(rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateX(rack.locker, 1);
        }
      }


(rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateX(rack.locker, -1);
        }
      }


(rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateY(rack.locker, 1);
        }
      }


(rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateY(rack.locker, -1);
        }
      }


(rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateX(rack.locker, 1);
          this.shiftBottles(ix, 1, 0);
        }
      }


(rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateX(rack.locker, -1);
          this.shiftBottles(ix, -1, 0);
        }
      }


rack => result = result && rack.canRemoveFirstRow()


rack => result = result && rack.canRemoveLastRow()


rack => result = result && rack.canRemoveFirstColumn()


rack => result = result && rack.canRemoveLastColumn()


(min, rack) => min = Math.min(min, rack.locker.dimension.x)


(max, oneRack) => Math.max(max, oneRack.dimension.x)


bottle => {
        bottle.positions = bottle.positions.map(
          pos => {
            if (pos.inRack(this.fridge.id, rack)) {
              return new Position(pos.lockerId, pos.x + shiftX, pos.y + shiftY, pos.rack);
            } else {
              return pos;
            }
          }
        );
      }


pos => {
            if (pos.inRack(this.fridge.id, rack)) {
              return new Position(pos.lockerId, pos.x + shiftX, pos.y + shiftY, pos.rack);
            } else {
              return pos;
            }
          }


ngOnDestroy(): void {
    if (this.gesture) {
      this.gesture.destroy();
    }
  }


public resetComponent() {
    if (this.rackComponents) {
      this.rackComponents.forEach(
        (rack: LockerComponent) => {
          rack.resetComponent();
        }
      );
    }
  }


anyRackSelected() {
    return this.rackComponents.filter(rack => rack.selected).length > 0;
  }


cellSelected(cell: Cell) {
    if (!this.editing && cell) {
      this.onCellSelected.emit(cell);
    }
  }


rackSelected(event: { rack: Locker, selected: boolean }) {
    this.onRacksSelected.emit(
      this.rackComponents
        .map((rack: SimpleLockerComponent, ix: number) => {
          return {rack: rack, ix: ix};
        })
        .filter((rack: { rack: SimpleLockerComponent, ix: number }) => rack.rack.selected)
        .map((rack: { rack: SimpleLockerComponent, ix: number }) => rack.ix)
    );
  }


ngAfterViewInit(): void {
    if (this.zoomable && this.zoomable.zoomable) {
      this.gesture = this.setupPinchZoom(this.zoomable.zoomableComponent);
    }
  }


addTopRack(): boolean {
    this.hapticConfirm();
    let currentHeight = this.fridge.racks.reduce((total, rack) => total + rack.dimension.y, 0);
    if (currentHeight + 1 > FridgeLockerComponent.MAX_NB_ROWS) {
      this.notificationService.information('locker-editor.maxi-row-reached');
      return false;
    }
    let rack = this.getNewRack();
    rack.id = this.fridge.id;
    this.fridge.racks = [ rack, ...this.fridge.racks ];
    this.fridge.dimensions = [ rack.dimension, ...this.fridge.dimensions ];
    this.content.forEach(
      btl => {
        btl.positions = btl.positions.map(
          pos => {
            let newPos = pos;
            if (pos.inLocker(this.fridge.id)) {
              newPos = new Position(pos.lockerId, pos.x, pos.y, pos.rack + 1);
            }
            return newPos;
          }
        );
      }
    );
  }


addBottomRack(): boolean {
    this.hapticConfirm();
    let currentHeight = this.fridge.racks.reduce((total, rack) => total + rack.dimension.y, 0);
    if (currentHeight + 1 > FridgeLockerComponent.MAX_NB_ROWS) {
      this.notificationService.information('locker-editor.maxi-row-reached');
      return false;
    }
    let rack = this.getNewRack();
    rack.id = this.fridge.id;
    this.fridge.racks = [ ...this.fridge.racks, rack ];
    this.fridge.dimensions = [ ...this.fridge.dimensions, rack.dimension ];
    //this.resetComponent();
  }


removeTopRack(): boolean {
    this.hapticConfirm();
    if (this.fridge.racks.length < 2) {
      this.notificationService.warning('locker-editor.mini-row-reached');
      return false;
    }
    ;
    // on vérifie que le rack à supprimer est vide
    let bottlesInRack = this.content.filter(
      btl => btl.positions.filter(
        pos => pos.inRack(this.fridge.id, 0)
      ).length > 0
    );
    if (bottlesInRack.length > 0) {
      this.notificationService.warning('locker-editor.rack-not-empty');
    } else {
      // le rack est vide on le supprime
      this.fridge.racks = this.fridge.racks.slice(1);
      this.fridge.dimensions = this.fridge.dimensions.slice(1);
      this.content.forEach(
        btl => {
          btl.positions.forEach(
            pos => {
              let newPos = pos;
              if (pos.inLocker(this.fridge.id)) {
                newPos = new Position(pos.lockerId, pos.x, pos.y, pos.rack - 1);
              }
              return newPos;
            }
          );
        }
      );
      //this.resetComponent();
    }
  }


removeBottomRack(): boolean {
    this.hapticConfirm();
    if (this.fridge.racks.length < 2) {
      this.notificationService.warning('locker-editor.mini-row-reached');
      return false;
    }
    let rackIndex = this.fridge.racks.length - 1;
    // on vérifie que le rack est vide avant de le supprimer
    let bottlesInRack = this.content.filter(
      btl => btl.positions.filter(
        pos => pos.inRack(this.fridge.id, rackIndex)
      ).length > 0
    );
    if (bottlesInRack.length > 0) {
      this.notificationService.warning('locker-editor.rack-not-empty');
    } else {
      // il est vide, on le supprime
      this.fridge.racks = this.fridge.racks.slice(0, this.fridge.racks.length - 1);
      this.fridge.dimensions = this.fridge.dimensions.slice(0, this.fridge.dimensions.length - 1);
    }
  }


addTopRow(): boolean {
    this.hapticConfirm();
    if (!this.canIncreaseHeight()) {
      return false;
    }
    this.rackComponents.forEach(
      (rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateY(rack.locker, 1);
          this.shiftBottles(ix, 0, 1);
        }
      }
    );

    this.resetComponent();
    return true;
  }


removeTopRow(): boolean {
    this.hapticConfirm();
    if (!this.canRemoveFirstRow()) {
      return false;
    }

    this.rackComponents.forEach(
      (rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateY(rack.locker, -1);
          this.shiftBottles(ix, 0, -1);
        }
      }
    );

    this.resetComponent();
    return true;
  }


addRightColumn(): boolean {
    this.hapticConfirm();
    if (!this.canIncreaseWidth()) {
      return false;
    }

    this.rackComponents.forEach(
      (rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateX(rack.locker, 1);
        }
      }
    );

    this.resetComponent();
    return true;
  }


removeRightColumn(): boolean {
    this.hapticConfirm();
    if (!this.canRemoveLastColumn()) {
      return false;
    }

    this.rackComponents.forEach(
      (rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateX(rack.locker, -1);
        }
      }
    );

    this.resetComponent();
    return true;
  }


addBottomRow(): boolean {
    this.hapticConfirm();
    if (!this.canIncreaseHeight()) {
      return false;
    }

    this.rackComponents.forEach(
      (rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateY(rack.locker, 1);
        }
      }
    );

    this.resetComponent();
    return true;
  }


removeBottomRow(): boolean {
    this.hapticConfirm();
    if (!this.canRemoveLastRow()) {
      return false;
    }

    this.rackComponents.forEach(
      (rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateY(rack.locker, -1);
        }
      }
    );

    this.resetComponent();
    return true;
  }


addLeftColumn(): boolean {
    this.hapticConfirm();
    if (!this.canIncreaseWidth()) {
      return false;
    }

    this.rackComponents.forEach(
      (rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateX(rack.locker, 1);
          this.shiftBottles(ix, 1, 0);
        }
      }
    );

    this.resetComponent();
    return true;
  }


removeLeftColumn(): boolean {
    this.hapticConfirm();
    if (!this.canRemoveFirstColumn()) {
      return false;
    }

    this.rackComponents.forEach(
      (rack: SimpleLockerComponent, ix: number) => {
        if (rack.selected) {
          this.updateX(rack.locker, -1);
          this.shiftBottles(ix, -1, 0);
        }
      }
    );

    this.resetComponent();
    return true;
  }


//avant d'enlever une rangée on s'assure qu'elle est vide + un locker a au moins une rangée
public canRemoveFirstRow(): boolean {
    if (!this.canDecreaseHeight()) {
      return false;
    }
    let result = true;
    this.rackComponents.filter(
      rack => rack.selected
    ).forEach(
      rack => result = result && rack.canRemoveFirstRow()
    );
    return result;
  }


//avant d'enlever une rangée on s'assure qu'elle est vide + un locker a au moins une rangée
public canRemoveLastRow(): boolean {
    if (!this.canDecreaseHeight()) {
      return false;
    }
    let result = true;
    this.rackComponents.filter(
      rack => rack.selected
    ).forEach(
      rack => result = result && rack.canRemoveLastRow()
    );
    return result;
  }


//avant d'enlever une colonne on s'assure qu'elle est vide + un locker a au moins une colonne
public canRemoveFirstColumn(): boolean {
    if (!this.canDecreaseWidth()) {
      return false;
    }
    let result = true;
    this.rackComponents.filter(
      rack => rack.selected
    ).forEach(
      rack => result = result && rack.canRemoveFirstColumn()
    );
    return result;
  }


//avant d'enlever une colonne on s'assure qu'elle est vide + un locker a au moins une colonne
public canRemoveLastColumn(): boolean {
    if (!this.canDecreaseWidth()) {
      return false;
    }
    let result = true;
    this.rackComponents.filter(
      rack => rack.selected
    ).forEach(
      rack => result = result && rack.canRemoveLastColumn()
    );
    return result;
  }


/**
   * Comme on enlève une rangée à chaque rack sélectionné on vérifie que le frigo ne dépasse pas la hauteur
   * max d'un frigo
   * @returns {boolean}
   */
protected canDecreaseHeight() {
    let nbSelectedRacks = this.rackComponents.filter(
      rack => rack.selected
    ).length;
    let currentHeight = this.fridge.racks.reduce((total, rack) => total + rack.dimension.y, 0);
    if (currentHeight - nbSelectedRacks > FridgeLockerComponent.MIN_NB_ROWS) {
      return true;
    } else {
      this.notificationService.warning('locker-editor.mini-row-reached');
      return false;
    }
  }


/**
   * Comme on enlève une colonne à chaque rack sélectionné on vérifie qu'aucun des racks concernés ne passe en
   * dessous du nombre minimal de colonnes autorisé dans un rack
   * @returns {boolean}
   */
protected canDecreaseWidth() {
    let minWidth = this.rackComponents.filter(
      rack => rack.selected
    ).reduce((min, rack) => min = Math.min(min, rack.locker.dimension.x), FridgeLockerComponent.MAX_NB_COLUMNS);
    if (minWidth - 1 > FridgeLockerComponent.MIN_NB_COLUMNS) {
      return true;
    } else {
      this.notificationService.warning('locker-editor.mini-col-reached');
      return false;
    }
  }


/**
   * Comme on ajoute une rangée de haut à chaque rack sélectionné on vérifie que le frigo ne dépasse pas la hauteur
   * max d'un frigo
   * @returns {boolean}
   */
protected canIncreaseHeight() {
    let nbSelectedRacks = this.rackComponents.filter(
      rack => rack.selected
    ).length;
    let currentHeight = this.fridge.racks.reduce((total, rack) => total + rack.dimension.y, 0);
    if (currentHeight + nbSelectedRacks >= FridgeLockerComponent.MAX_NB_ROWS) {
      this.notificationService.warning('locker-editor.maxi-row-reached');
      return false;
    }
    return true;
  }


/**
   * On ajoute une rangée de haut à chaque rack sélectionné. On vérifie que chaque rack ne dépasse pas sa largeur
   * max autorisée.
   * @returns {boolean}
   */
protected canIncreaseWidth() {
    if (this.dimension.x + 1 >= FridgeLockerComponent.MAX_NB_COLUMNS) {
      this.notificationService.warning('locker-editor.maxi-col-reached');
      return false;
    }
    return true;
  }


private getNewRack(): Locker {
    let width = this.fridge.racks.reduce((max, oneRack) => Math.max(max, oneRack.dimension.x), 0);
    let formats = this.fridge.racks[ 0 ].supportedFormats;
    return new SimpleLocker(undefined, '', LockerType.shifted, {
      x: width,
      y: 1
    }, true, '', formats);

  }


private shiftBottles(rack: number, shiftX: number, shiftY: number) {
    this.content.forEach(
      bottle => {
        bottle.positions = bottle.positions.map(
          pos => {
            if (pos.inRack(this.fridge.id, rack)) {
              return new Position(pos.lockerId, pos.x + shiftX, pos.y + shiftY, pos.rack);
            } else {
              return pos;
            }
          }
        );
      }
    );
  }


async () => {
		await beforeAllDb('shares');
	}


async () => {
		await afterAllTests();
	}


async () => {
		await beforeEachDb();
	}


it => it.folder_id === '000000000000000000000000000000F1'


it => it.note_id === '00000000000000000000000000000002'


su => su.user.email === 'user2@localhost'


su => su.user.email === 'user3@localhost'


() => {
                this.display = "block";
                setTimeout(() => this.activateToast()); // Is needed to make "display: none" & "opacity" transitions working
            }


() => this.activateToast()


() => {
                this.remove();
            }


() => {
                this.timeout = setTimeout(() => {
                    this.ngZone.run(() => {
                        this.remove();
                    });
                }, this.options.timeOut);
            }


() => {
                    this.ngZone.run(() => {
                        this.remove();
                    });
                }


() => {
                        this.remove();
                    }


() => this.toastService.remove(this.toastPackage.toastId)


() => this.remove()


() => {
            intervalId = setInterval(() => {
                this.ngZone.run(() => {
                    this.updateProgress();
                });
            }, (this.options.timeOut || 0) / 100);
        }


() => {
                this.ngZone.run(() => {
                    this.updateProgress();
                });
            }


() => {
                    this.updateProgress();
                }


subscription => subscription.unsubscribe()


// <example-url>./../examples/index.html#/toast</example-url>
/**
 * /**
 * __Name : __
 * Toast Component
 *
 * __Usage :__
 * Used to show toast, handle animation and events. Handles progress bar animation.
 * This component is created dynamically in ToastContainerService
*/
@Component({
    selector: "nui-toast-component",
    templateUrl: "./toast.component.html",
    styleUrls: ["./toast.component.less"],
    encapsulation: ViewEncapsulation.None,
    host: { "[attr.role]": "role" },
})
export class ToastComponent implements OnDestroy {
    public body?: string | SafeHtml | null;
    public title?: string;
    public options: IToastConfig;
    /** width of progress bar */
    public width = -1;
    public toastIcon: string;
    public closeButton?: boolean;

    get role(): string { return this.toastPackage.toastType === "success" || this.toastPackage.toastType === "info" ? "status" : "alert"; }

    /** a combination of toast type and options.toastClass */
    @HostBinding("class") toastClasses = "";
    @HostBinding("class.nui-toast--fade-out") fadeOut = true;
    @HostBinding("style.display") display = "none";

    /** controls animation */
    private state: ToastState = ToastState.Inactive;
    private timeout: any;
    private intervalId: NodeJS.Timeout;
    private hideTime: number;
    private subscriptions: Array<Subscription> = [];
    private animationFadeOutLength = 300;
    private toastTypeToSeverityIcon: { [key: string]: string } = {
        error: "severity_critical",
        warning: "severity_warning",
        info: "severity_info",
        success: "severity_ok",
    };

    constructor(private toastService: ToastServiceBase,
        private toastPackage: ToastPackage,
        private ngZone: NgZone) {
        this.title = toastPackage.title;
        this.body = toastPackage.body;
        this.options = toastPackage.config;
        this.toastClasses = `nui-toast--${toastPackage.toastType} ${toastPackage.config.toastClass} nui-toast--animation`;
        this.toastIcon = this.toastTypeToSeverityIcon[toastPackage.toastType];
        this.closeButton = this.toastPackage.config.closeButton;

        const activateSubscription = this.toastPackage.toastRef.afterActivate()
            .subscribe(() => {
                this.display = "block";
                setTimeout(() => this.activateToast()); // Is needed to make "display: none" & "opacity" transitions working
            });

        const closeSubscription = this.toastPackage.toastRef.manualClosed()
            .subscribe(() => {
                this.remove();
            });

        this.subscriptions.push(activateSubscription, closeSubscription);
    }

    /**
     * triggers fade in animation and sets timeout till the toast will be dismissed
     */
    public activateToast() {
        this.state = ToastState.Active;
        this.fadeOut = false;

        if (this.options.timeOut) {
            this.ngZone.runOutsideAngular(() => {
                this.timeout = setTimeout(() => {
                    this.ngZone.run(() => {
                        this.remove();
                    });
                }, this.options.timeOut);
            });
            this.hideTime = new Date().getTime() + this.options.timeOut;

            if (this.options.progressBar) {
                this.intervalId = this.repeatProgressBarChange();
            }
        }
    }

    /**
     * tells toastService to remove this toast after animation time
     */
    public remove() {
        if (this.state === ToastState.Removed) {
            return;
        }

        clearTimeout(this.timeout);
        this.state = ToastState.Removed;
        this.fadeOut = true;
        this.timeout = setTimeout(() => this.toastService.remove(this.toastPackage.toastId), this.animationFadeOutLength);
    }

    /**
     * handles click on toast itself
     */
    @HostListener("click")
    public clickToast() {
        if (this.state === ToastState.Removed) {
            return;
        }

        this.toastPackage.triggerClick();

        if (this.options.clickToDismiss) {
            this.remove();
        }
    }

    /**
     * disables progressBar and prevents toast from closing
     */
    @HostListener("mouseenter")
    public stickAround() {
        if (this.state === ToastState.Removed) {
            return;
        }

        clearTimeout(this.timeout);
        this.options.timeOut = 0;
        this.hideTime = 0;
        // disable progressBar
        clearInterval(this.intervalId);
        this.width = 0;
    }

    /**
     *  using extendedTimeOut value to delay toast's closure
     */
    @HostListener("mouseleave")
    public delayedHideToast() {
        clearInterval(this.intervalId);

        if (this.options.extendedTimeOut === 0 || this.state === ToastState.Removed) {
            return;
        }

        this.timeout = setTimeout(() => this.remove(), this.options.extendedTimeOut);
        this.options.timeOut = this.options.extendedTimeOut;
        this.hideTime = new Date().getTime() + (this.options.timeOut || 0);
        this.width = 100;

        if (this.options.progressBar) {
            this.intervalId = this.repeatProgressBarChange();
        }
    }

    private repeatProgressBarChange(): NodeJS.Timeout {
        let intervalId;

        this.ngZone.runOutsideAngular(() => {
            intervalId = setInterval(() => {
                this.ngZone.run(() => {
                    this.updateProgress();
                });
            }, (this.options.timeOut || 0) / 100);
        });

        // using type assertion to avoid compile time error
        // variable intervalId is assigned by clojure / async
        return intervalId as unknown as NodeJS.Timeout;
    }

    /**
     * updates progress bar width
     */
    private updateProgress() {
        if (this.width === 0 || !this.options.timeOut) {
            return;
        }

        const now = new Date().getTime();
        const remaining = this.hideTime - now;

        this.width = (remaining / this.options.timeOut) * 100;

        if (this.options.progressAnimation === "increasing") {
            this.width = 100 - this.width;
        }

        if (this.width <= 0) {
            this.width = 0;
        }

        if (this.width >= 100) {
            this.width = 100;
        }
    }

    ngOnDestroy() {
        this.subscriptions.forEach(subscription => subscription.unsubscribe());
        clearInterval(this.intervalId);
        clearTimeout(this.timeout);
    }
}


enum ToastState {
    Inactive = "inactive",
    Active = "active",
    Removed = "removed",
}


/**
     * triggers fade in animation and sets timeout till the toast will be dismissed
     */
public activateToast() {
        this.state = ToastState.Active;
        this.fadeOut = false;

        if (this.options.timeOut) {
            this.ngZone.runOutsideAngular(() => {
                this.timeout = setTimeout(() => {
                    this.ngZone.run(() => {
                        this.remove();
                    });
                }, this.options.timeOut);
            });
            this.hideTime = new Date().getTime() + this.options.timeOut;

            if (this.options.progressBar) {
                this.intervalId = this.repeatProgressBarChange();
            }
        }
    }


/**
     * tells toastService to remove this toast after animation time
     */
public remove() {
        if (this.state === ToastState.Removed) {
            return;
        }

        clearTimeout(this.timeout);
        this.state = ToastState.Removed;
        this.fadeOut = true;
        this.timeout = setTimeout(() => this.toastService.remove(this.toastPackage.toastId), this.animationFadeOutLength);
    }


/**
     * handles click on toast itself
     */
@HostListener("click")
    public clickToast() {
        if (this.state === ToastState.Removed) {
            return;
        }

        this.toastPackage.triggerClick();

        if (this.options.clickToDismiss) {
            this.remove();
        }
    }


/**
     * disables progressBar and prevents toast from closing
     */
@HostListener("mouseenter")
    public stickAround() {
        if (this.state === ToastState.Removed) {
            return;
        }

        clearTimeout(this.timeout);
        this.options.timeOut = 0;
        this.hideTime = 0;
        // disable progressBar
        clearInterval(this.intervalId);
        this.width = 0;
    }


/**
     *  using extendedTimeOut value to delay toast's closure
     */
@HostListener("mouseleave")
    public delayedHideToast() {
        clearInterval(this.intervalId);

        if (this.options.extendedTimeOut === 0 || this.state === ToastState.Removed) {
            return;
        }

        this.timeout = setTimeout(() => this.remove(), this.options.extendedTimeOut);
        this.options.timeOut = this.options.extendedTimeOut;
        this.hideTime = new Date().getTime() + (this.options.timeOut || 0);
        this.width = 100;

        if (this.options.progressBar) {
            this.intervalId = this.repeatProgressBarChange();
        }
    }


private repeatProgressBarChange(): NodeJS.Timeout {
        let intervalId;

        this.ngZone.runOutsideAngular(() => {
            intervalId = setInterval(() => {
                this.ngZone.run(() => {
                    this.updateProgress();
                });
            }, (this.options.timeOut || 0) / 100);
        });

        // using type assertion to avoid compile time error
        // variable intervalId is assigned by clojure / async
        return intervalId as unknown as NodeJS.Timeout;
    }


/**
     * updates progress bar width
     */
private updateProgress() {
        if (this.width === 0 || !this.options.timeOut) {
            return;
        }

        const now = new Date().getTime();
        const remaining = this.hideTime - now;

        this.width = (remaining / this.options.timeOut) * 100;

        if (this.options.progressAnimation === "increasing") {
            this.width = 100 - this.width;
        }

        if (this.width <= 0) {
            this.width = 0;
        }

        if (this.width >= 100) {
            this.width = 100;
        }
    }


ngOnDestroy() {
        this.subscriptions.forEach(subscription => subscription.unsubscribe());
        clearInterval(this.intervalId);
        clearTimeout(this.timeout);
    }


export interface AudioSearch {
    // eslint-disable-next-line @typescript-eslint/naming-convention
    audio_list?: Array<AudioInfo>;
    kw?: string;
}


async (req: NextApiRequest, res: NextApiResponse) => {
  await conectarDB();
  const {
    query: { id },
  } = req;

  try {
    const deletedDonador = await Donador.deleteOne({ _id: id });
    if (!deletedDonador) {
      return res.status(400).json({ success: false });
    }
    res.status(201).json({ succsess: true, data: {} });
  } catch (error) {
    console.log(error);
    res.status(400).json({ success: false });
  }
}


export function newStringArray(): Array<string> {
  return new Array<string>();
}


export function pushString(arr: string[], str: string): Array<string> {
  arr.push(str);
  return arr;
}


() => {
  VM.restoreState();
}


() => {
  beforeEach(() => TestBed.configureTestingModule({}));

  it('should be created', () => {
    const service: ContributorService = TestBed.get(ContributorService);
    expect(service).toBeTruthy();
  });
}


() => {
    const service: ContributorService = TestBed.get(ContributorService);
    expect(service).toBeTruthy();
  }


() => {
  let collectionRightsObjectFactory: CollectionRightsObjectFactory = null;

  beforeEach(() => {
    collectionRightsObjectFactory = new CollectionRightsObjectFactory();
  });

  it('should not be able to modify owner names', () => {
    var initialCollectionRightsBackendObject = {
      collection_id: 0,
      can_edit: true,
      can_unpublish: false,
      is_private: true,
      owner_names: ['A']
    };

    var sampleCollectionRights = collectionRightsObjectFactory.create(
      initialCollectionRightsBackendObject);
    var ownerNames = sampleCollectionRights.getOwnerNames();
    ownerNames.push('B');

    expect(sampleCollectionRights.getOwnerNames()).toEqual(['A']);
  });

  it('should accept accept changes to the bindable list of collection nodes',
    () => {
      var initialCollectionRightsBackendObject = {
        collection_id: 0,
        can_edit: true,
        can_unpublish: false,
        is_private: true,
        owner_names: ['A']
      };

      var sampleCollectionRights = collectionRightsObjectFactory.create(
        initialCollectionRightsBackendObject);
      var ownerNames = sampleCollectionRights.getBindableOwnerNames();
      ownerNames.push('B');

      expect(sampleCollectionRights.getOwnerNames()).toEqual(['A', 'B']);
    }
  );

  it('should be able to set public when canEdit is true', () => {
    var initialCollectionRightsBackendObject = {
      collection_id: 0,
      can_edit: true,
      can_unpublish: false,
      is_private: true,
      owner_names: ['A']
    };

    var sampleCollectionRights = collectionRightsObjectFactory.create(
      initialCollectionRightsBackendObject);
    expect(sampleCollectionRights.isPrivate()).toBe(true);
    expect(sampleCollectionRights.isPublic()).toBe(false);

    sampleCollectionRights.setPublic();
    expect(sampleCollectionRights.isPrivate()).toBe(false);
    expect(sampleCollectionRights.isPublic()).toBe(true);
  });

  it('should throw error and not be able to set public when canEdit is false',
    () => {
      var initialCollectionRightsBackendObject = {
        collection_id: 0,
        can_edit: false,
        can_unpublish: false,
        is_private: true,
        owner_names: ['A']
      };

      var sampleCollectionRights = collectionRightsObjectFactory.create(
        initialCollectionRightsBackendObject);
      expect(sampleCollectionRights.isPrivate()).toBe(true);
      expect(sampleCollectionRights.isPublic()).toBe(false);

      expect(() => {
        sampleCollectionRights.setPublic();
      }).toThrowError('User is not allowed to edit this collection.');
      expect(sampleCollectionRights.isPrivate()).toBe(true);
      expect(sampleCollectionRights.isPublic()).toBe(false);
    }
  );

  it('should be able to set private when canUnpublish is true', () => {
    var initialCollectionRightsBackendObject = {
      collection_id: 0,
      can_edit: true,
      can_unpublish: true,
      is_private: false,
      owner_names: ['A']
    };

    var sampleCollectionRights = collectionRightsObjectFactory.create(
      initialCollectionRightsBackendObject);
    expect(sampleCollectionRights.isPrivate()).toBe(false);
    expect(sampleCollectionRights.isPublic()).toBe(true);

    sampleCollectionRights.setPrivate();
    expect(sampleCollectionRights.isPrivate()).toBe(true);
    expect(sampleCollectionRights.isPublic()).toBe(false);
  });

  it('should throw error when when canUnpublish is false during unpublishing',
    () => {
      var noUnpublishCollectionRightsBackendObject = {
        collection_id: 0,
        can_edit: true,
        can_unpublish: false,
        is_private: false,
        owner_names: ['A']
      };

      var sampleCollectionRights = collectionRightsObjectFactory.create(
        noUnpublishCollectionRightsBackendObject);
      expect(sampleCollectionRights.isPrivate()).toBe(false);
      expect(sampleCollectionRights.isPublic()).toBe(true);

      expect(() => {
        sampleCollectionRights.setPrivate();
      }).toThrowError('User is not allowed to unpublish this collection.');
      // Verify that the status remains unchanged.
      expect(sampleCollectionRights.isPrivate()).toBe(false);
      expect(sampleCollectionRights.isPublic()).toBe(true);
    }
  );

  it('should create an empty collection rights object', () => {
    var emptyCollectionRightsBackendObject = (
      collectionRightsObjectFactory.createEmptyCollectionRights());

    expect(
      emptyCollectionRightsBackendObject.getCollectionId()).toBeNull();
    expect(emptyCollectionRightsBackendObject.canEdit()).toBeNull();
    expect(emptyCollectionRightsBackendObject.canUnpublish()).toBeNull();
    expect(emptyCollectionRightsBackendObject.isPrivate()).toBeNull();
    expect(emptyCollectionRightsBackendObject.getOwnerNames()).toEqual([]);
  });

  it('should make a copy from another collection rights', () => {
    var noUnpublishCollectionRightsBackendObject = {
      collection_id: 0,
      can_edit: true,
      can_unpublish: false,
      is_private: false,
      owner_names: ['A']
    };

    var sampleCollectionRights = collectionRightsObjectFactory.create(
      noUnpublishCollectionRightsBackendObject);

    var emptyCollectionRightsBackendObject = (
      collectionRightsObjectFactory.createEmptyCollectionRights());

    emptyCollectionRightsBackendObject.copyFromCollectionRights(
      sampleCollectionRights);
    expect(emptyCollectionRightsBackendObject.getCollectionId()).toEqual(0);
    expect(emptyCollectionRightsBackendObject.canEdit()).toBe(true);
    expect(emptyCollectionRightsBackendObject.canUnpublish()).toBe(false);
    expect(emptyCollectionRightsBackendObject.isPrivate()).toBe(false);
    expect(emptyCollectionRightsBackendObject.getOwnerNames()).toEqual(['A']);
  });
}


() => {
    collectionRightsObjectFactory = new CollectionRightsObjectFactory();
  }


() => {
    var initialCollectionRightsBackendObject = {
      collection_id: 0,
      can_edit: true,
      can_unpublish: false,
      is_private: true,
      owner_names: ['A']
    };

    var sampleCollectionRights = collectionRightsObjectFactory.create(
      initialCollectionRightsBackendObject);
    var ownerNames = sampleCollectionRights.getOwnerNames();
    ownerNames.push('B');

    expect(sampleCollectionRights.getOwnerNames()).toEqual(['A']);
  }


() => {
      var initialCollectionRightsBackendObject = {
        collection_id: 0,
        can_edit: true,
        can_unpublish: false,
        is_private: true,
        owner_names: ['A']
      };

      var sampleCollectionRights = collectionRightsObjectFactory.create(
        initialCollectionRightsBackendObject);
      var ownerNames = sampleCollectionRights.getBindableOwnerNames();
      ownerNames.push('B');

      expect(sampleCollectionRights.getOwnerNames()).toEqual(['A', 'B']);
    }


() => {
    var initialCollectionRightsBackendObject = {
      collection_id: 0,
      can_edit: true,
      can_unpublish: false,
      is_private: true,
      owner_names: ['A']
    };

    var sampleCollectionRights = collectionRightsObjectFactory.create(
      initialCollectionRightsBackendObject);
    expect(sampleCollectionRights.isPrivate()).toBe(true);
    expect(sampleCollectionRights.isPublic()).toBe(false);

    sampleCollectionRights.setPublic();
    expect(sampleCollectionRights.isPrivate()).toBe(false);
    expect(sampleCollectionRights.isPublic()).toBe(true);
  }


() => {
      var initialCollectionRightsBackendObject = {
        collection_id: 0,
        can_edit: false,
        can_unpublish: false,
        is_private: true,
        owner_names: ['A']
      };

      var sampleCollectionRights = collectionRightsObjectFactory.create(
        initialCollectionRightsBackendObject);
      expect(sampleCollectionRights.isPrivate()).toBe(true);
      expect(sampleCollectionRights.isPublic()).toBe(false);

      expect(() => {
        sampleCollectionRights.setPublic();
      }).toThrowError('User is not allowed to edit this collection.');
      expect(sampleCollectionRights.isPrivate()).toBe(true);
      expect(sampleCollectionRights.isPublic()).toBe(false);
    }


() => {
        sampleCollectionRights.setPublic();
      }


() => {
    var initialCollectionRightsBackendObject = {
      collection_id: 0,
      can_edit: true,
      can_unpublish: true,
      is_private: false,
      owner_names: ['A']
    };

    var sampleCollectionRights = collectionRightsObjectFactory.create(
      initialCollectionRightsBackendObject);
    expect(sampleCollectionRights.isPrivate()).toBe(false);
    expect(sampleCollectionRights.isPublic()).toBe(true);

    sampleCollectionRights.setPrivate();
    expect(sampleCollectionRights.isPrivate()).toBe(true);
    expect(sampleCollectionRights.isPublic()).toBe(false);
  }


() => {
      var noUnpublishCollectionRightsBackendObject = {
        collection_id: 0,
        can_edit: true,
        can_unpublish: false,
        is_private: false,
        owner_names: ['A']
      };

      var sampleCollectionRights = collectionRightsObjectFactory.create(
        noUnpublishCollectionRightsBackendObject);
      expect(sampleCollectionRights.isPrivate()).toBe(false);
      expect(sampleCollectionRights.isPublic()).toBe(true);

      expect(() => {
        sampleCollectionRights.setPrivate();
      }).toThrowError('User is not allowed to unpublish this collection.');
      // Verify that the status remains unchanged.
      expect(sampleCollectionRights.isPrivate()).toBe(false);
      expect(sampleCollectionRights.isPublic()).toBe(true);
    }


() => {
        sampleCollectionRights.setPrivate();
      }


() => {
    var emptyCollectionRightsBackendObject = (
      collectionRightsObjectFactory.createEmptyCollectionRights());

    expect(
      emptyCollectionRightsBackendObject.getCollectionId()).toBeNull();
    expect(emptyCollectionRightsBackendObject.canEdit()).toBeNull();
    expect(emptyCollectionRightsBackendObject.canUnpublish()).toBeNull();
    expect(emptyCollectionRightsBackendObject.isPrivate()).toBeNull();
    expect(emptyCollectionRightsBackendObject.getOwnerNames()).toEqual([]);
  }


() => {
    var noUnpublishCollectionRightsBackendObject = {
      collection_id: 0,
      can_edit: true,
      can_unpublish: false,
      is_private: false,
      owner_names: ['A']
    };

    var sampleCollectionRights = collectionRightsObjectFactory.create(
      noUnpublishCollectionRightsBackendObject);

    var emptyCollectionRightsBackendObject = (
      collectionRightsObjectFactory.createEmptyCollectionRights());

    emptyCollectionRightsBackendObject.copyFromCollectionRights(
      sampleCollectionRights);
    expect(emptyCollectionRightsBackendObject.getCollectionId()).toEqual(0);
    expect(emptyCollectionRightsBackendObject.canEdit()).toBe(true);
    expect(emptyCollectionRightsBackendObject.canUnpublish()).toBe(false);
    expect(emptyCollectionRightsBackendObject.isPrivate()).toBe(false);
    expect(emptyCollectionRightsBackendObject.getOwnerNames()).toEqual(['A']);
  }


async params => {
      if (params.commandId && params.executionStartTime) {
        this.selectedExecution = await this.commandExecutorService.watchExecution(
          params.commandId,
          parseInt(params.executionStartTime, 10)
        );
      } else {
        this.selectedExecution = null;
      }
    }


es => this.executions = es


@Component({
  selector: 'app-command-executor',
  templateUrl: './command-executor.component.html'
})
export class CommandExecutorComponent implements OnInit, OnDestroy {
  commands: Array<Command> = [];
  executions: Array<Execution> = [];
  isEditable = false;
  adding = false;
  editedCommand: Command = null;
  paramsSubscription: Subscription;
  executionsSubscription: Subscription;
  selectedExecution: ExecutionWithOutput = null;

  constructor(private route: ActivatedRoute, private commandExecutorService: CommandExecutorService, private configService: ConfigService,
              private confirmationService: ConfirmationService) {
  }

  ngOnInit(): void {
    this.paramsSubscription = this.route.params.subscribe(async params => {
      if (params.commandId && params.executionStartTime) {
        this.selectedExecution = await this.commandExecutorService.watchExecution(
          params.commandId,
          parseInt(params.executionStartTime, 10)
        );
      } else {
        this.selectedExecution = null;
      }
    });
    this.executionsSubscription = this.commandExecutorService.watchAllExecutions().subscribe(es => this.executions = es);
    this.loadCommands();
    this.loadExecutions();
    this.loadConfiguration();
  }

  async loadCommands(): Promise<void> {
    this.commands = await this.commandExecutorService.getAllCommands();
  }

  async loadExecutions(): Promise<void> {
    this.executions = await this.commandExecutorService.getAllExecutions();
  }

  async loadConfiguration(): Promise<void> {
    const config = await this.configService.getConfiguration();
    this.isEditable = config.isInAdminMode;
  }

  startAddingCommand(): void {
    this.editedCommand = null;
    this.adding = true;
  }

  startEditingCommand(command: Command): void {
    this.adding = false;
    this.editedCommand = command;
  }

  async addCommand(command: Command): Promise<void> {
    this.adding = false;
    await this.commandExecutorService.addCommand(command);
    await this.loadCommands();
  }

  async editCommand(command: Command): Promise<void> {
    const existingCommand = this.editedCommand;
    this.editedCommand = null;
    await this.commandExecutorService.deleteCommand(existingCommand);
    await this.commandExecutorService.addCommand(command);
    await Promise.all([this.loadCommands(), this.loadExecutions()]);
  }

  async deleteCommand(command: Command): Promise<void> {
    if (await this.confirmationService.requestConfirmation('Removal', `You are about to remove the command '${command.name}'.`)) {
      await this.commandExecutorService.deleteCommand(command);
      await this.loadCommands();
      await this.loadExecutions();
    }
  }

  async executeCommand(command: Command): Promise<void> {
    await this.commandExecutorService.execute(command);
    await this.loadExecutions();
  }

  async terminateExecution(execution: Execution): Promise<void> {
    await this.commandExecutorService.terminateExecution(execution);
  }

  async haltExecution(execution: Execution): Promise<void> {
    await this.commandExecutorService.haltExecution(execution);
  }

  async deleteExecution(execution: Execution): Promise<void> {
    await this.commandExecutorService.deleteExecution(execution);
    await this.loadExecutions();
  }

  async deleteAllExecutions(): Promise<void> {
    const message = `You are about to remove ALL executions! Complete executions will be removed from the history and all active executions
     will get aborted.`;
    if (await this.confirmationService.requestConfirmation('Global removal', message)) {
      await this.commandExecutorService.deleteAllExecutions();
      await this.loadExecutions();
    }
  }

  ngOnDestroy(): void {
    this.paramsSubscription.unsubscribe();
    this.executionsSubscription.unsubscribe();
  }
}


ngOnInit(): void {
    this.paramsSubscription = this.route.params.subscribe(async params => {
      if (params.commandId && params.executionStartTime) {
        this.selectedExecution = await this.commandExecutorService.watchExecution(
          params.commandId,
          parseInt(params.executionStartTime, 10)
        );
      } else {
        this.selectedExecution = null;
      }
    });
    this.executionsSubscription = this.commandExecutorService.watchAllExecutions().subscribe(es => this.executions = es);
    this.loadCommands();
    this.loadExecutions();
    this.loadConfiguration();
  }


async loadCommands(): Promise<void> {
    this.commands = await this.commandExecutorService.getAllCommands();
  }


async loadExecutions(): Promise<void> {
    this.executions = await this.commandExecutorService.getAllExecutions();
  }


async loadConfiguration(): Promise<void> {
    const config = await this.configService.getConfiguration();
    this.isEditable = config.isInAdminMode;
  }


startAddingCommand(): void {
    this.editedCommand = null;
    this.adding = true;
  }


startEditingCommand(command: Command): void {
    this.adding = false;
    this.editedCommand = command;
  }


async addCommand(command: Command): Promise<void> {
    this.adding = false;
    await this.commandExecutorService.addCommand(command);
    await this.loadCommands();
  }


async editCommand(command: Command): Promise<void> {
    const existingCommand = this.editedCommand;
    this.editedCommand = null;
    await this.commandExecutorService.deleteCommand(existingCommand);
    await this.commandExecutorService.addCommand(command);
    await Promise.all([this.loadCommands(), this.loadExecutions()]);
  }


async deleteCommand(command: Command): Promise<void> {
    if (await this.confirmationService.requestConfirmation('Removal', `You are about to remove the command '${command.name}'.`)) {
      await this.commandExecutorService.deleteCommand(command);
      await this.loadCommands();
      await this.loadExecutions();
    }
  }


async executeCommand(command: Command): Promise<void> {
    await this.commandExecutorService.execute(command);
    await this.loadExecutions();
  }


async terminateExecution(execution: Execution): Promise<void> {
    await this.commandExecutorService.terminateExecution(execution);
  }


async haltExecution(execution: Execution): Promise<void> {
    await this.commandExecutorService.haltExecution(execution);
  }


async deleteExecution(execution: Execution): Promise<void> {
    await this.commandExecutorService.deleteExecution(execution);
    await this.loadExecutions();
  }


async deleteAllExecutions(): Promise<void> {
    const message = `You are about to remove ALL executions! Complete executions will be removed from the history and all active executions
     will get aborted.`;
    if (await this.confirmationService.requestConfirmation('Global removal', message)) {
      await this.commandExecutorService.deleteAllExecutions();
      await this.loadExecutions();
    }
  }


ngOnDestroy(): void {
    this.paramsSubscription.unsubscribe();
    this.executionsSubscription.unsubscribe();
  }


function BaseForm<T = Record<string, any>>(props: BaseFormProps<T>) {
  const {
    children,
    contentRender,
    submitter,
    fieldProps,
    formItemProps,
    groupProps,
    dateFormatter = 'string',
    form: userForm,
    formRef: propsFormRef,
    onInit,
    syncToUrl,
    onReset,
    omitNil = true,
    ...rest
  } = props;

  const [form] = Form.useForm();
  /** 同步 url 上的参数 */
  const [urlSearch, setUrlSearch] = useUrlSearchParams({});
  const formRef = useRef<FormInstance>(userForm || form);

  // 初始化给一个默认的 form
  useImperativeHandle(propsFormRef, () => formRef.current, []);

  const fieldsValueType = useRef<Record<string, ProFieldValueType>>({});
  /** 保存 transformKeyRef，用于对表单key transform */
  const transformKeyRef = useRef<Record<string, SearchTransformKeyFn | undefined>>({});

  const [loading, setLoading] = useMountMergeState<boolean>(false);

  const items = React.Children.toArray(children);
  const submitterProps: SubmitterProps =
    typeof submitter === 'boolean' || !submitter ? {} : submitter;

  const transformKey = (values: any, omit: boolean) =>
    transformKeySubmitValue(
      conversionSubmitValue(values, dateFormatter, fieldsValueType.current, omit),
      transformKeyRef.current,
    );

  /** 渲染提交按钮与重置按钮 */
  const submitterNode =
    submitter === false ? undefined : (
      <Submitter
        key="submitter"
        {...submitterProps}
        onReset={() => {
          const finalValues = transformKey(formRef.current.getFieldsValue(), omitNil);
          submitterProps?.onReset?.(finalValues);
          onReset?.(finalValues);
          // 如果 syncToUrl，清空一下数据
          if (syncToUrl) {
            setUrlSearch(transformKey(formRef.current.getFieldsValue(), false));
          }
        }}


(
  syncUrl: BaseFormProps<any>['syncToUrl'],
  params: Record<string, any>,
  type: 'get' | 'set',
) => {
  if (syncUrl === true) {
    return params;
  }
  return runFunction(syncUrl, params, type);
}


() => formRef.current


(values: any, omit: boolean) =>
    transformKeySubmitValue(
      conversionSubmitValue(values, dateFormatter, fieldsValueType.current, omit),
      transformKeyRef.current,
    )


() => {
    const finalValues = transformKey(formRef.current.getFieldsValue(), omitNil);
    onInit?.(finalValues);
  }


() => {
    if (!syncToUrl) {
      return {};
    }
    return genParams(syncToUrl, urlSearch, 'get');
  }


(name, { valueType = 'text', transform }) => {
            if (Array.isArray(name)) {
              transformKeyRef.current = namePathSet(transformKeyRef.current, name, transform);
              fieldsValueType.current = namePathSet(fieldsValueType.current, name, valueType);
            }
          }


(event) => {
              if (event.key === 'Enter') {
                formRef.current?.submit();
              }
            }


(pre, next) => {
                    return {
                      ...pre,
                      [next]: finalValues[next] || undefined,
                    };
                  }


export type CommonFormProps<T extends Record<string, any> = Record<string, any>> = {
  submitter?:
    | SubmitterProps<{
        form?: FormInstance<any>;
      }>
    | false;

  /**
   * 支持异步操作，更加方便
   *
   * @name 表单结束后调用
   */
  onFinish?: (formData: T) => Promise<boolean | void>;

  /** @name 获取真正的可以获得值的 from */
  formRef?: React.MutableRefObject<FormInstance | undefined>;
  /** @name 同步结果到 url 中 */
  syncToUrl?: true | ((values: T, type: 'get' | 'set') => T);
  /**
   * 如果为 false,会原样保存。
   *
   * @default true
   * @param 要不要值中的 Null 和 undefined
   */

  omitNil?: boolean;
  /**
   * 格式化 Date 的方式，默认转化为 string
   *
   * @see date -> YYYY-MM-DD
   * @see dateTime -> YYYY-MM-DD  HH:mm:SS
   * @see time -> HH:mm:SS
   */
  dateFormatter?: 'number' | 'string' | false;
};


export type BaseFormProps<T = Record<string, any>> = {
  contentRender?: (
    items: React.ReactNode[],
    submitter: React.ReactElement<SubmitterProps> | undefined,
    form: FormInstance<any>,
  ) => React.ReactNode;
  fieldProps?: FieldProps;
  onInit?: (values: T) => void;
  formItemProps?: FormItemProps;
  groupProps?: GroupProps;
} & Omit<FormProps, 'onFinish'> &
  CommonFormProps<T>;


export function setStatusBarColors() {
  // Make the iOS status bar transparent with white text.
  // See https://github.com/burkeholland/nativescript-statusbar/issues/2
  // for details on the technique used.
  if (application.ios) {
    var AppDelegate = UIResponder.extend({
      applicationDidFinishLaunchingWithOptions: function() {
        utils.ios.getter(UIApplication, UIApplication.sharedApplication).statusBarStyle = UIStatusBarStyle.LightContent;
        return true;
      }
    }, {
        name: "AppDelegate",
        protocols: [UIApplicationDelegate]
      });
    application.ios.delegate = AppDelegate;
  }

  // Make the Android status bar transparent.
  // See http://bradmartin.net/2016/03/10/fullscreen-and-navigation-bar-color-in-a-nativescript-android-app/
  // for details on the technique used.
  if (application.android) {
    application.android.onActivityStarted = function() {
      if (application.android && platform.device.sdkVersion >= "21") {
        var View = android.view.View;
        var window = application.android.startActivity.getWindow();
        window.setStatusBarColor(0x000000);

        var decorView = window.getDecorView();
        decorView.setSystemUiVisibility(
          View.SYSTEM_UI_FLAG_LAYOUT_STABLE
          | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
          | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
          | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
      }
    }
  }
}


/*
 * This program has been developed by students from the bachelor Computer Science at Utrecht University
 * within the Software Project course. © Copyright Utrecht University (Department of Information and
 * Computing Sciences)
 */
/**
 * @packageDocumentation
 * @module Models
 */
/**
 * This class represents the database model for an Item from the shop used in the front-end.
 */
export class Item {
	_id: string;
	title: string;
	category: string;
	previewImage: string;
	fullImage: string;
	fullImage2: string;
	price: number;
	initial: boolean;
}


/*
 * Copyright 2022 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
export interface Timezone {
    offset: string;
    label: string;
    ianaCode: string;
}


export class OA_YBHYSQYBHYSQ_W_MobWFMDViewBase extends IBizListViewController {

    mdctrl: mdctrlService;

  constructor(opt) {
    super(Object.assign(opt, { 'url': '/IonicApp/ywsp/OA_YBHYSQYBHYSQ_W_MobWFMDView.do' }));
  }

  ngOnInit(): void {
    this.mdctrl = new mdctrlService({ 'name': 'mdctrl', 'component': this, 'url': this.getBackendUrl() });
    this.regControl('mdctrl', this.mdctrl);
    super.ngOnInit();
  }

  regUIActions(config){
        let uiaction_0 = {"type":"DEUIACTION","tag":"New"
        };
        super.regUIAction(uiaction_0);
    }

    public getNewDataView(params: any = {}): any {
       let newmode = params.srfnewmode;
       if(!newmode) {
            newmode='';
       }
    }

    public getEditDataView(params: any = {}): any {
       let list = [params.srfeditmode2,params.srfeditmode];
       for(let i=0;i<2;i++){
          let editmode=list[i];
          if(!editmode)
             continue;
          if(editmode=='10'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W10_MobWFEditView3', viewParam:params };
          }
          if(editmode=='30'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W30_MobWFEditView3', viewParam:params };
          }
          if(editmode=='40@1'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W40_MobWFEditView3', viewParam:params };
          }
          if(editmode=='30@1'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W30_MobWFEditView3', viewParam:params };
          }
          if(editmode=='10@1'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W10_MobWFEditView3', viewParam:params };
          }
          if(editmode=='20'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W20_MobWFEditView3', viewParam:params };
          }
          if(editmode=='50@1'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W50_MobWFEditView3', viewParam:params };
          }
          if(editmode=='20@1'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W20_MobWFEditView3', viewParam:params };
          }
          if(editmode=='50'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W50_MobWFEditView3', viewParam:params };
          }
          if(editmode=='40'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W40_MobWFEditView3', viewParam:params };
          }
      }
    }

  getEditMode(data){
    let editMode=super.getEditMode(data);
    if(editMode&&editMode!='')
       editMode= editMode+':'+ data.hysqbz;
    else
       editMode= data.hysqbz;

     return editMode;
      }

  regAppFuncs(): void {
    super.regAppFuncs();
  }

  regUICounters() : void {
  }
}


ngOnInit(): void {
    this.mdctrl = new mdctrlService({ 'name': 'mdctrl', 'component': this, 'url': this.getBackendUrl() });
    this.regControl('mdctrl', this.mdctrl);
    super.ngOnInit();
  }


regUIActions(config){
        let uiaction_0 = {"type":"DEUIACTION","tag":"New"
        };
        super.regUIAction(uiaction_0);
    }


public getNewDataView(params: any = {}): any {
       let newmode = params.srfnewmode;
       if(!newmode) {
            newmode='';
       }
    }


public getEditDataView(params: any = {}): any {
       let list = [params.srfeditmode2,params.srfeditmode];
       for(let i=0;i<2;i++){
          let editmode=list[i];
          if(!editmode)
             continue;
          if(editmode=='10'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W10_MobWFEditView3', viewParam:params };
          }
          if(editmode=='30'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W30_MobWFEditView3', viewParam:params };
          }
          if(editmode=='40@1'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W40_MobWFEditView3', viewParam:params };
          }
          if(editmode=='30@1'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W30_MobWFEditView3', viewParam:params };
          }
          if(editmode=='10@1'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W10_MobWFEditView3', viewParam:params };
          }
          if(editmode=='20'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W20_MobWFEditView3', viewParam:params };
          }
          if(editmode=='50@1'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W50_MobWFEditView3', viewParam:params };
          }
          if(editmode=='20@1'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W20_MobWFEditView3', viewParam:params };
          }
          if(editmode=='50'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W50_MobWFEditView3', viewParam:params };
          }
          if(editmode=='40'){
              return { openMode:'', className:'OA_YBHYSQYBHYSQ_W40_MobWFEditView3', viewParam:params };
          }
      }
    }


getEditMode(data){
    let editMode=super.getEditMode(data);
    if(editMode&&editMode!='')
       editMode= editMode+':'+ data.hysqbz;
    else
       editMode= data.hysqbz;

     return editMode;
      }


regAppFuncs(): void {
    super.regAppFuncs();
  }


regUICounters() : void {
  }


export default interface IFinancialItemIXC {
  id: string;
  status: string;
  charge: number;
  contract_r: string;
  installment_r: number;
  contract_a: string;
  sell_id: string;
  emission_date: Date;
  due_date: Date;
  value: number;
  received_value: number;
  open_value: number;
  paid_value: number;
  payment_date: Date;
  credit_date: Date;
  received_date: Date;
  type: string;
  cancellation_date: Date;
  cancellation_reason: string;
}


export enum SpecialKeys {
  ExtensionEnable = '<ExtensionEnable>',
  ExtensionDisable = '<ExtensionDisable>',
  TimeoutFinished = '<TimeoutFinished>',
}


(detail, components, spec, externals) => {
  return {
    'uid': detail.uid,
    'dom': detail.dom,
    'components': components,
    'debug.sketcher': 'Tabbar',

    domModification: {
      attributes: {
        role: 'tablist'
      }
    },

    'behaviours': SketchBehaviours.augment(
      detail.tabbarBehaviours,
      [
        Highlighting.config({
          highlightClass: detail.markers.selectedClass,
          itemClass: detail.markers.tabClass,

          // https://www.w3.org/TR/2010/WD-wai-aria-practices-20100916/#tabpanel
          // Consider a more seam-less way of combining highlighting and toggling
          onHighlight (tabbar, tab) {
            // TODO: Integrate highlighting and toggling in a nice way
            Attr.set(tab.element(), 'aria-selected', 'true');
          },
          onDehighlight (tabbar, tab) {
            Attr.set(tab.element(), 'aria-selected', 'false');
          }
        }),

        Keying.config({
          mode: 'flow',
          getInitial (tabbar) {
            // Restore focus to the previously highlighted tab.
            return Highlighting.getHighlighted(tabbar).map((tab) => {
              return tab.element();
            });
          },
          selector: '.' + detail.markers.tabClass,
          executeOnMove: true
        })
      ]
    )
  };
}


(tab) => {
              return tab.element();
            }


// https://www.w3.org/TR/2010/WD-wai-aria-practices-20100916/#tabpanel
// Consider a more seam-less way of combining highlighting and toggling
onHighlight (tabbar, tab) {
            // TODO: Integrate highlighting and toggling in a nice way
            Attr.set(tab.element(), 'aria-selected', 'true');
          }


onDehighlight (tabbar, tab) {
            Attr.set(tab.element(), 'aria-selected', 'false');
          }


getInitial (tabbar) {
            // Restore focus to the previously highlighted tab.
            return Highlighting.getHighlighted(tabbar).map((tab) => {
              return tab.element();
            });
          }


(rawData : string) =>
			{
				//Remove any double-quotes that would make the data ill-formed
				let transformed = rawData.replace(/"/g, '');

				//Reformat combined column names for timestamps
				transformed = transformed.replace('Year Month Day Hour Minutes in YYYY,MM,DD,HH24,MI format in Local standard time', 'Year,Month,Day,Hour,Minute');

				//If there are extraneous lines of text (e.g. notes) before or after the actual data, discard them
				transformed = CsvDataUtil.stripExtraneousLines(transformed, true);

				return transformed;
			}


(row : any) => {
				return row.map((field : string) => { return field.trim(); });
			}


(field : string) => { return field.trim(); }


(resolve : Function, reject : Function) =>
		{
			readFile(csvFile).then((data : Buffer) =>
			{
				//Apply the transformation function (if supplied)
				let transformed = data.toString('utf-8');
				if (transform !== undefined) {
					transformed = transform(transformed);
				}

				//Attempt to parse the transformed data
				let parser = csv_parse(transformed, undefined, (err : Error, result : string[][]) =>
				{
					if (err) {
						reject(err);
					}
					else {
						resolve(result);
					}
				});
			})
			.catch((err : Error) => {
				reject(err);
			});
		}


(data : Buffer) =>
			{
				//Apply the transformation function (if supplied)
				let transformed = data.toString('utf-8');
				if (transform !== undefined) {
					transformed = transform(transformed);
				}

				//Attempt to parse the transformed data
				let parser = csv_parse(transformed, undefined, (err : Error, result : string[][]) =>
				{
					if (err) {
						reject(err);
					}
					else {
						resolve(result);
					}
				});
			}


(err : Error, result : string[][]) =>
				{
					if (err) {
						reject(err);
					}
					else {
						resolve(result);
					}
				}


(err : Error) => {
				reject(err);
			}


(resolve : Function, reject : Function) =>
		{
			csv_stringify(data, (err : Error, output : any) =>
			{
				if (err)
				{
					Error.captureStackTrace(err);
					reject(err);
				}
				else
				{
					writeFile(csvFile, output).then((result : any) => {
						resolve(true);
					})
					.catch((err : Error) => {
						reject(err);
					});
				}
			});
		}


(err : Error, output : any) =>
			{
				if (err)
				{
					Error.captureStackTrace(err);
					reject(err);
				}
				else
				{
					writeFile(csvFile, output).then((result : any) => {
						resolve(true);
					})
					.catch((err : Error) => {
						reject(err);
					});
				}
			}


(result : any) => {
						resolve(true);
					}


(err : Error) => {
						reject(err);
					}


(row : any[]) => {
				return row.map((field : any) => {
					return (($.type(field) == 'number' && Number.isInteger(field) === false) ? (<number>field).toFixed(2) : field.toString());
				});
			}


(field : any) => {
					return (($.type(field) == 'number' && Number.isInteger(field) === false) ? (<number>field).toFixed(2) : field.toString());
				}


(field : string) => { return field.length; }


(row : string[]) =>
			{
				return row.map((field : string, col : number) => {
					return pad(field, columnWidths[col]);
				});
			}


(field : string, col : number) => {
					return pad(field, columnWidths[col]);
				}


(line : string) => { return line.length; }


(line : string, index : number) =>
		{
			//Determine if this is a data row
			if (line.length == rowLength) {
				return true;
			}

			//If we are keeping the header, determine if this is the header row
			if (keepHeader === true && index < lines.length-1 && lines[index+1].length == rowLength)
			{
				//If the line immediately preceding a data row does not have the correct comma count, it's not a header
				//(Substring counting technique adapted from here: <https://stackoverflow.com/a/4009768>)
				let dataCommas = (lines[index+1].match(/,/g) || []).length;
				let lineCommas = (line.match(/,/g) || []).length;
				if (dataCommas == lineCommas) {
					return true;
				}
			}

			return false;
		}


//Parses a headerless, single-column CSV file and returns the values as a list
public static async csvToList(csvFile : string)
	{
		try
		{
			//Attempt to parse the CSV file
			let result = await CsvDataUtil.parseCsv(csvFile);

			//Flatten the result array
			let flattened : string[] = [];
			for (let row of result)
			{
				if (row.length != 1) {
					throw new Error('CSV file contains more than one column');
				}

				flattened.push(row[0]);
			}

			return flattened;
		}
		catch (err)
		{
			//Propagate any errors
			throw err;
		}
	}


//Parses data in the hybrid fixed-width CSV format that BOM supplies data in
public static async parseBOMCsv(csvFile : string) : Promise<string[][]>
	{
		try
		{
			//We need to transform the raw data before we can safely parse it
			let transform = (rawData : string) =>
			{
				//Remove any double-quotes that would make the data ill-formed
				let transformed = rawData.replace(/"/g, '');

				//Reformat combined column names for timestamps
				transformed = transformed.replace('Year Month Day Hour Minutes in YYYY,MM,DD,HH24,MI format in Local standard time', 'Year,Month,Day,Hour,Minute');

				//If there are extraneous lines of text (e.g. notes) before or after the actual data, discard them
				transformed = CsvDataUtil.stripExtraneousLines(transformed, true);

				return transformed;
			};

			//Attempt to parse the CSV file, performing our transforms prior to parsing
			let result = await CsvDataUtil.parseCsv(csvFile, transform);

			//Strip away the excess whitespace introduced by the fixed-width formatting
			result = result.map((row : any) => {
				return row.map((field : string) => { return field.trim(); });
			});

			return result;
		}
		catch (err)
		{
			//Propagate any errors
			throw err;
		}
	}


//Parses a CSV file, applying the data transformation callback (if supplied) prior to parsing
public static parseCsv(csvFile : string, transform? : (rawData : string) => string) : Promise<string[][]>
	{
		return new Promise((resolve : Function, reject : Function) =>
		{
			readFile(csvFile).then((data : Buffer) =>
			{
				//Apply the transformation function (if supplied)
				let transformed = data.toString('utf-8');
				if (transform !== undefined) {
					transformed = transform(transformed);
				}

				//Attempt to parse the transformed data
				let parser = csv_parse(transformed, undefined, (err : Error, result : string[][]) =>
				{
					if (err) {
						reject(err);
					}
					else {
						resolve(result);
					}
				});
			})
			.catch((err : Error) => {
				reject(err);
			});
		});
	}


//Writes data to a CSV file
public static writeCsv(csvFile : string, data : any[][]) : Promise<boolean>
	{
		return new Promise((resolve : Function, reject : Function) =>
		{
			csv_stringify(data, (err : Error, output : any) =>
			{
				if (err)
				{
					Error.captureStackTrace(err);
					reject(err);
				}
				else
				{
					writeFile(csvFile, output).then((result : any) => {
						resolve(true);
					})
					.catch((err : Error) => {
						reject(err);
					});
				}
			});
		});
	}


//Writes data to a CSV file in the hybrid fixed-width CSV format that BOM supplies data in
public static async writeBOMCsv(csvFile : string, data : any[][])
	{
		try
		{
			//Stringify the data, formatting floating-point values with two decimal places
			let headerRow = data.slice(0, 1);
			let stringified : string[][] = data.slice(1).map((row : any[]) => {
				return row.map((field : any) => {
					return (($.type(field) == 'number' && Number.isInteger(field) === false) ? (<number>field).toFixed(2) : field.toString());
				});
			});

			//Determine the maximum width for each column
			let columnWidths : number[] = [];
			for (let col = 0; col < stringified[0].length; ++col)
			{
				let columnValues = DataUtils.extractColumn(stringified, col);
				columnWidths[col] = mathjs.max(columnValues.map((field : string) => { return field.length; }));
			}

			//Pad all of the columns to their maximum width
			let padded = stringified.map((row : string[]) =>
			{
				return row.map((field : string, col : number) => {
					return pad(field, columnWidths[col]);
				});
			});

			//Write the padded data to file
			await CsvDataUtil.writeCsv(csvFile, headerRow.concat(padded));
		}
		catch (err)
		{
			//Propagate any errors
			throw err;
		}
	}


//Concatenates multiple station list CSV files, using the specified notes file to construct the common header
public static async concatenateStationLists(listFiles : string[], notesFile : string)
	{
		try
		{
			//Read the notes data and discard all text prior to the section describing the site details file format
			let notesData = (await readFile(notesFile, {encoding: 'utf-8'})).replace(/\r\n/g, '\n');
			notesData = notesData.substr(notesData.indexOf('\nSITE DETAILS FILE'));

			//Extract the list of header fields
			let headerFields : string[] = [];
			let sitesTableRegex = new RegExp('[0-9\\- ]+,[0-9 ]+,([^\n]+)\n', 'g');
			let match : RegExpExecArray|null = null;
			while ((match = sitesTableRegex.exec(notesData)) !== null) {
				headerFields.push(match[1].trim().replace(/\.$/, '').replace(/,/g, ''));
			}

			//Read all of the station lists into memory, discarding their individual headers
			let rows : string[] = [];
			for (let listFile of listFiles) {
				rows = rows.concat(CsvDataUtil.stripExtraneousLines(await readFile(listFile, {encoding: 'utf-8'}), false));
			}

			//Construct our common header and concatenate it with the extracted data rows
			return headerFields.join(',') + '\n' + rows.join('\n');
		}
		catch (err)
		{
			//Propagate any errors
			throw err;
		}
	}


//Determines the length of a row of data in a BOM-format CSV file
private static dataRowLength(lines : string[]) : number {
		return mathjs.mode(lines.map((line : string) => { return line.length; }));
	}


//Strips extraneous lines of text (e.g. notes) before or after the actual data in BOM-format CSV file
private static stripExtraneousLines(csvData : string, keepHeader : boolean)
	{
		let lines = csvData.split('\n');
		let rowLength = CsvDataUtil.dataRowLength(lines);
		return lines.filter((line : string, index : number) =>
		{
			//Determine if this is a data row
			if (line.length == rowLength) {
				return true;
			}

			//If we are keeping the header, determine if this is the header row
			if (keepHeader === true && index < lines.length-1 && lines[index+1].length == rowLength)
			{
				//If the line immediately preceding a data row does not have the correct comma count, it's not a header
				//(Substring counting technique adapted from here: <https://stackoverflow.com/a/4009768>)
				let dataCommas = (lines[index+1].match(/,/g) || []).length;
				let lineCommas = (line.match(/,/g) || []).length;
				if (dataCommas == lineCommas) {
					return true;
				}
			}

			return false;
		})
		.join('\n');
	}


(v,k)=> { keys.push(k) }


w => {
      if(this.tracks.has(w)) {
        let target= this.tracks.get(w);
        // ** target **
        let f= this.source.getFeatureById('track-' + w);
        if(f) { // exists so update it
          f.setGeometry( new MultiLineString( this.parseCoordinates(target) ) );
          f.setStyle(this.buildStyle(w));
        }
        else {  // does not exist so create it
          f= new Feature( {
            geometry: new MultiLineString( this.parseCoordinates(target) )
          });
          f.setId('track-' + w);
          f.setStyle(this.buildStyle(w));
          this.source.addFeature(f);
        }
      }
    }


w=> {
      let f= this.source.getFeatureById('track-' + w);
      if(f) { this.source.removeFeature(f) }
    }


mls=> {
        let lines:Array<any>= [];
        mls.forEach( line=> lines.push( mapifyCoords(line) ) )
        return lines;
    }


line=> lines.push( mapifyCoords(line) )


// ** Signal K target tracks  **
@Component({
  selector: 'ol-map > sk-target-tracks',
  template: '<ng-content></ng-content>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SKTargetTracksLayerComponent implements OnInit, OnDestroy, OnChanges {

  protected layer: Layer;
  public source: VectorSource;

  /**
   * This event is triggered after the layer is initialized
   * Use this to have access to the layer and some helper functions
   */
  @Output() layerReady: AsyncSubject<Layer> = new AsyncSubject(); // AsyncSubject will only store the last value, and only publish it when the sequence is completed

  @Input() tracks: Map<string, any>= new Map();
  @Input() minZoom: number= 10;
  @Input() mapZoom: number= 10;
  @Input() showTracks: boolean= true;
  @Input() updateIds: Array<string>= [];
  @Input() removeIds: Array<string>= [];
  @Input() opacity: number;
  @Input() visible: boolean;
  @Input() extent: Extent;
  @Input() zIndex: number;
  @Input() minResolution: number;
  @Input() maxResolution: number;
  @Input() layerProperties: { [index: string]: any };

  constructor(protected changeDetectorRef: ChangeDetectorRef,
              protected mapComponent: MapComponent) {
    this.changeDetectorRef.detach();
  }

  ngOnInit() {
    this.source = new VectorSource();
    this.layer = new VectorLayer(Object.assign(this, {...this.layerProperties}));
    this.parseItems( this.extractKeys(this.tracks) );

    const map = this.mapComponent.getMap();
    if(this.layer && map) {
      map.addLayer(this.layer);
      map.render();
      this.layerReady.next(this.layer);
      this.layerReady.complete();
    }
  }

  ngOnChanges(changes: SimpleChanges) {
    if(this.layer) {
      const properties: { [index: string]: any } = {};
      for(const key in changes) {
        if(key=='tracks' && changes[key].firstChange) {
          if(!changes[key].currentValue) { return }
          if(!this.source) { return }
          this.source.clear();
          this.parseItems(this.extractKeys(changes[key].currentValue));
        }
        else if(key=='updateIds') {
          this.parseItems(changes[key].currentValue);
        }
        else if(key=='removeIds') {
          this.removeItems(changes[key].currentValue);
        }
        else if(key=='showTracks' && !changes[key].firstChange) {
          if(changes[key].currentValue) {
            this.parseItems(this.extractKeys(this.tracks));
          }
          else { this.source.clear() }
        }
        else if(key=='labelMinZoom' || key=='mapZoom') {
          this.handleLabelZoomChange(key, changes[key]);
        }
        else if(key=='layerProperties') {
          this.layer.setProperties(properties, false);
        }
        else {
          properties[key] = changes[key].currentValue;
        }
      }
      this.layer.setProperties(properties, false);
    }
  }

  ngOnDestroy() {
    const map = this.mapComponent.getMap();
    if (this.layer && map) {
      map.removeLayer(this.layer);
      map.render();
      this.layer = null;
    }
  }

  // ** assess attribute change **
  handleLabelZoomChange(key: string, change: SimpleChange) {
    if(key=='labelMinZoom') {
      if(typeof this.mapZoom!=='undefined') {
        this.parseItems( this.extractKeys(this.tracks) );
      }
    }
    else if(key=='mapZoom') {
      if(typeof this.minZoom !=='undefined') {
        if( (change.currentValue >= this.minZoom && change.previousValue < this.minZoom) ||
            (change.currentValue < this.minZoom && change.previousValue >= this.minZoom) ) {
          this.parseItems( this.extractKeys(this.tracks) );
        }
      }
    }
  }

  extractKeys(m:Map<string,any>): Array<string> {
    let keys= [];
    m.forEach((v,k)=> { keys.push(k) });
    return keys;
  }

  // add update features
  parseItems(ids: Array<string>) {
    if(!ids || !Array.isArray(ids) ) { return }
    if(!this.source) { return }
    ids.forEach( w => {
      if(this.tracks.has(w)) {
        let target= this.tracks.get(w);
        // ** target **
        let f= this.source.getFeatureById('track-' + w);
        if(f) { // exists so update it
          f.setGeometry( new MultiLineString( this.parseCoordinates(target) ) );
          f.setStyle(this.buildStyle(w));
        }
        else {  // does not exist so create it
          f= new Feature( {
            geometry: new MultiLineString( this.parseCoordinates(target) )
          });
          f.setId('track-' + w);
          f.setStyle(this.buildStyle(w));
          this.source.addFeature(f);
        }
      }
    });
  }

  // remove features
  removeItems(ids:Array<string>) {
    if(!ids || !Array.isArray(ids) ) { return }
    if(!this.source) { return }
    ids.forEach( w=> {
      let f= this.source.getFeatureById('track-' + w);
      if(f) { this.source.removeFeature(f) }
    });
  }

  // build target style
  buildStyle(id:string):Style {
    let rgb= (id.indexOf('aircraft')!=-1) ?  '0, 0, 255' : '255, 0, 255';
    let color= (this.mapZoom < this.minZoom) ? `rgba(${rgb},0)` : `rgba(${rgb},1)`;
    color= (this.showTracks) ? `rgba(${rgb},1)` : `rgba(${rgb},0)`;
    if(this.layerProperties && this.layerProperties.style) {
      let cs= this.layerProperties.style.clone();
      let ls= cs.getStroke();
      ls.setColor(color);
      cs.setStroke(ls);
      return cs;
    }
    else {
      return new Style({
        stroke: new Stroke({
            width: 1,
            color: color,
            lineDash: [2,2]
        })
      });
    }

  }

  // ** mapify and transform MultiLineString coordinates
  parseCoordinates(trk:Array<any>) {
    // ** handle dateline crossing **
    let tc= trk.map( mls=> {
        let lines:Array<any>= [];
        mls.forEach( line=> lines.push( mapifyCoords(line) ) )
        return lines;
    });
    return fromLonLatArray(tc);
  }

}


ngOnInit() {
    this.source = new VectorSource();
    this.layer = new VectorLayer(Object.assign(this, {...this.layerProperties}));
    this.parseItems( this.extractKeys(this.tracks) );

    const map = this.mapComponent.getMap();
    if(this.layer && map) {
      map.addLayer(this.layer);
      map.render();
      this.layerReady.next(this.layer);
      this.layerReady.complete();
    }
  }


ngOnChanges(changes: SimpleChanges) {
    if(this.layer) {
      const properties: { [index: string]: any } = {};
      for(const key in changes) {
        if(key=='tracks' && changes[key].firstChange) {
          if(!changes[key].currentValue) { return }
          if(!this.source) { return }
          this.source.clear();
          this.parseItems(this.extractKeys(changes[key].currentValue));
        }
        else if(key=='updateIds') {
          this.parseItems(changes[key].currentValue);
        }
        else if(key=='removeIds') {
          this.removeItems(changes[key].currentValue);
        }
        else if(key=='showTracks' && !changes[key].firstChange) {
          if(changes[key].currentValue) {
            this.parseItems(this.extractKeys(this.tracks));
          }
          else { this.source.clear() }
        }
        else if(key=='labelMinZoom' || key=='mapZoom') {
          this.handleLabelZoomChange(key, changes[key]);
        }
        else if(key=='layerProperties') {
          this.layer.setProperties(properties, false);
        }
        else {
          properties[key] = changes[key].currentValue;
        }
      }
      this.layer.setProperties(properties, false);
    }
  }


ngOnDestroy() {
    const map = this.mapComponent.getMap();
    if (this.layer && map) {
      map.removeLayer(this.layer);
      map.render();
      this.layer = null;
    }
  }


// ** assess attribute change **
handleLabelZoomChange(key: string, change: SimpleChange) {
    if(key=='labelMinZoom') {
      if(typeof this.mapZoom!=='undefined') {
        this.parseItems( this.extractKeys(this.tracks) );
      }
    }
    else if(key=='mapZoom') {
      if(typeof this.minZoom !=='undefined') {
        if( (change.currentValue >= this.minZoom && change.previousValue < this.minZoom) ||
            (change.currentValue < this.minZoom && change.previousValue >= this.minZoom) ) {
          this.parseItems( this.extractKeys(this.tracks) );
        }
      }
    }
  }


extractKeys(m:Map<string,any>): Array<string> {
    let keys= [];
    m.forEach((v,k)=> { keys.push(k) });
    return keys;
  }


// add update features
parseItems(ids: Array<string>) {
    if(!ids || !Array.isArray(ids) ) { return }
    if(!this.source) { return }
    ids.forEach( w => {
      if(this.tracks.has(w)) {
        let target= this.tracks.get(w);
        // ** target **
        let f= this.source.getFeatureById('track-' + w);
        if(f) { // exists so update it
          f.setGeometry( new MultiLineString( this.parseCoordinates(target) ) );
          f.setStyle(this.buildStyle(w));
        }
        else {  // does not exist so create it
          f= new Feature( {
            geometry: new MultiLineString( this.parseCoordinates(target) )
          });
          f.setId('track-' + w);
          f.setStyle(this.buildStyle(w));
          this.source.addFeature(f);
        }
      }
    });
  }


// remove features
removeItems(ids:Array<string>) {
    if(!ids || !Array.isArray(ids) ) { return }
    if(!this.source) { return }
    ids.forEach( w=> {
      let f= this.source.getFeatureById('track-' + w);
      if(f) { this.source.removeFeature(f) }
    });
  }


// build target style
buildStyle(id:string):Style {
    let rgb= (id.indexOf('aircraft')!=-1) ?  '0, 0, 255' : '255, 0, 255';
    let color= (this.mapZoom < this.minZoom) ? `rgba(${rgb},0)` : `rgba(${rgb},1)`;
    color= (this.showTracks) ? `rgba(${rgb},1)` : `rgba(${rgb},0)`;
    if(this.layerProperties && this.layerProperties.style) {
      let cs= this.layerProperties.style.clone();
      let ls= cs.getStroke();
      ls.setColor(color);
      cs.setStroke(ls);
      return cs;
    }
    else {
      return new Style({
        stroke: new Stroke({
            width: 1,
            color: color,
            lineDash: [2,2]
        })
      });
    }

  }


// ** mapify and transform MultiLineString coordinates
parseCoordinates(trk:Array<any>) {
    // ** handle dateline crossing **
    let tc= trk.map( mls=> {
        let lines:Array<any>= [];
        mls.forEach( line=> lines.push( mapifyCoords(line) ) )
        return lines;
    });
    return fromLonLatArray(tc);
  }


() => {

    describe('SettingsComponent', () => {

        let comp: SettingsComponent;
        let fixture: ComponentFixture<SettingsComponent>;
        let mockAuth: any;
        let mockPrincipal: any;

        beforeEach(async(() => {
            TestBed.configureTestingModule({
                imports: [GatewayServiceTestModule],
                declarations: [SettingsComponent],
                providers: [
                ]
            })
            .overrideTemplate(SettingsComponent, '')
            .compileComponents();
        }));

        beforeEach(() => {
            fixture = TestBed.createComponent(SettingsComponent);
            comp = fixture.componentInstance;
            mockAuth = fixture.debugElement.injector.get(AccountService);
            mockPrincipal = fixture.debugElement.injector.get(Principal);
        });

        it('should send the current identity upon save', () => {
            // GIVEN
            const accountValues = {
                firstName: 'John',
                lastName: 'Doe',

                activated: true,
                email: 'john.doe@mail.com',
                langKey: 'en',
                login: 'john'
            };
            mockPrincipal.setResponse(accountValues);

            // WHEN
            comp.settingsAccount = accountValues;
            comp.save();

            // THEN
            expect(mockPrincipal.identitySpy).toHaveBeenCalled();
            expect(mockAuth.saveSpy).toHaveBeenCalledWith(accountValues);
            expect(comp.settingsAccount).toEqual(accountValues);
        });

        it('should notify of success upon successful save', () => {
            // GIVEN
            const accountValues = {
                firstName: 'John',
                lastName: 'Doe'
            };
            mockPrincipal.setResponse(accountValues);

            // WHEN
            comp.save();

            // THEN
            expect(comp.error).toBeNull();
            expect(comp.success).toBe('OK');
        });

        it('should notify of error upon failed save', () => {
            // GIVEN
            mockAuth.saveSpy.and.returnValue(Observable.throw('ERROR'));

            // WHEN
            comp.save();

            // THEN
            expect(comp.error).toEqual('ERROR');
            expect(comp.success).toBeNull();
        });
    });
}