From f7bbafb86bac7b237984f835b0cb8b9ac9d60215 Mon Sep 17 00:00:00 2001 From: till_schuetze Date: Tue, 10 Sep 2024 10:25:37 +0200 Subject: [PATCH 01/12] set up time service --- src/app/app.module.ts | 4 ++ src/app/map/interfaces/time.service.ts | 24 ++++++++++ src/app/shared/services/dayjs-time.service.ts | 46 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 src/app/map/interfaces/time.service.ts create mode 100644 src/app/shared/services/dayjs-time.service.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5b0b45f4c..7b2214ab6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -31,6 +31,8 @@ import {effectErrorHandler} from './state/app/effects/effects-error-handler.effe import {EsriMapLoaderService} from './map/services/esri-services/esri-map-loader.service'; import {MapLoaderService} from './map/interfaces/map-loader.service'; import {DevModeBannerComponent} from './shared/components/dev-mode-banner/dev-mode-banner.component'; +import {DayjsTimeService} from './shared/services/dayjs-time.service'; +import {TimeService} from './map/interfaces/time.service'; // necessary for the locale 'de-CH' to work // see https://stackoverflow.com/questions/46419026/missing-locale-data-for-the-locale-xxx-with-angular @@ -40,6 +42,7 @@ export const MAP_SERVICE = new InjectionToken('MapService'); export const MAP_LOADER_SERVICE = new InjectionToken('MapLoaderService'); export const NEWS_SERVICE = new InjectionToken('NewsService'); export const GRAV_CMS_SERVICE = new InjectionToken('GravCmsService'); +export const TIME_SERVICE = new InjectionToken('TimeService'); @NgModule({ declarations: [AppComponent], @@ -62,6 +65,7 @@ export const GRAV_CMS_SERVICE = new InjectionToken('GravCmsServi {provide: MAP_LOADER_SERVICE, useClass: EsriMapLoaderService}, {provide: NEWS_SERVICE, deps: [KTZHNewsService, KTZHNewsServiceMock, ConfigService], useFactory: newsFactory}, {provide: GRAV_CMS_SERVICE, deps: [GravCmsService, GravCmsServiceMock, ConfigService], useFactory: gravCmsFactory}, + {provide: TIME_SERVICE, useClass: DayjsTimeService}, {provide: LOCALE_ID, useValue: 'de-CH'}, { provide: EFFECTS_ERROR_HANDLER, diff --git a/src/app/map/interfaces/time.service.ts b/src/app/map/interfaces/time.service.ts new file mode 100644 index 000000000..ae9f23db0 --- /dev/null +++ b/src/app/map/interfaces/time.service.ts @@ -0,0 +1,24 @@ +import {Duration} from 'dayjs/plugin/duration'; +import {ManipulateType, UnitType} from 'dayjs'; + +export interface TimeService { + getPartial(date: string, unit: UnitType): number; + + getDateAsString(date: Date, format: string): string; + + getDate(date: string, format: string): Date; + + getUTCDate(date: Date, format?: string): Date; + + getUnixDate(created: number): Date; + + getDuration(time: string | number, unit?: ManipulateType): Duration; + + isValidDate(value: string): boolean; + + addDuration(date: Date, durationToAdd: Duration): Date; + + subtractDuration(date: Date, durationToSubtract: Duration): Date; + + calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number; +} diff --git a/src/app/shared/services/dayjs-time.service.ts b/src/app/shared/services/dayjs-time.service.ts new file mode 100644 index 000000000..81b19ef28 --- /dev/null +++ b/src/app/shared/services/dayjs-time.service.ts @@ -0,0 +1,46 @@ +import {TimeService} from '../../map/interfaces/time.service'; +import dayjs, {ManipulateType, UnitType} from 'dayjs'; +import duration, {Duration} from 'dayjs/plugin/duration'; +import {Injectable} from '@angular/core'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; +import utc from 'dayjs/plugin/utc'; + +dayjs.extend(duration); +dayjs.extend(customParseFormat); +dayjs.extend(utc); + +@Injectable({ + providedIn: 'root', +}) +export class DayjsTimeService implements TimeService { + public getPartial(date: string, unit: UnitType): number { + return dayjs(date, unit).get(unit); + } + public getDateAsString(date: Date, format: string): string { + return dayjs(date).format(format); + } + public getDate(date: string, format: string): Date { + return dayjs(date, format).toDate(); + } + public getUTCDate(date: Date, format?: string): Date { + return dayjs.utc(date, format).toDate(); + } + public getUnixDate(created: number): Date { + return dayjs.unix(created).toDate(); + } + public getDuration(time: string | number, unit?: ManipulateType): Duration { + return dayjs.duration(time, unit); + } + public isValidDate(value: string): boolean { + return dayjs(value).isValid(); + } + public addDuration(date: Date, durationToAdd: Duration): Date { + return dayjs(date).add(durationToAdd).toDate(); + } + public subtractDuration(date: Date, durationToSubtract: Duration): Date { + return dayjs(date).subtract(durationToSubtract).toDate(); + } + public calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number { + return Math.abs(dayjs(secondDate).diff(dayjs(firstDate))); + } +} From 351b9d268494d52b3d7598c5bf76dbd206982581 Mon Sep 17 00:00:00 2001 From: till_schuetze Date: Mon, 16 Sep 2024 10:25:13 +0200 Subject: [PATCH 02/12] implement time service everywhere where dayjs is used --- .../time-slider/time-slider.component.ts | 29 ++++++--- src/app/map/interfaces/time.service.ts | 15 +---- src/app/map/pipes/date-to-string.pipe.spec.ts | 18 +++-- src/app/map/pipes/date-to-string.pipe.ts | 8 ++- .../pipes/time-extent-to-string.pipe.spec.ts | 19 ++++-- .../map/pipes/time-extent-to-string.pipe.ts | 8 ++- .../esri-services/esri-map.service.spec.ts | 3 + .../esri-services/esri-map.service.ts | 16 +++-- .../map/services/favourites.service.spec.ts | 65 ++++++++++--------- src/app/map/services/favourites.service.ts | 3 +- .../map/services/time-slider.service.spec.ts | 5 +- src/app/map/services/time-slider.service.ts | 29 +++++---- .../utils/active-time-slider-layers.utils.ts | 4 +- .../apis/gb3/gb3-favourites.service.ts | 6 +- .../apis/gb3/gb3-share-link.service.spec.ts | 11 +++- .../apis/gb3/gb3-share-link.service.ts | 6 +- .../apis/grav-cms/grav-cms.service.spec.ts | 9 ++- .../apis/grav-cms/grav-cms.service.ts | 26 +++++--- src/app/shared/services/dayjs-time.service.ts | 22 ++++--- src/app/shared/utils/storage.utils.spec.ts | 10 +-- src/app/shared/utils/storage.utils.ts | 10 +-- src/app/shared/utils/time-extent.utils.ts | 30 +++------ .../auth/effects/auth-status.effects.spec.ts | 4 +- .../map/effects/share-link.effects.spec.ts | 4 +- ...ve-map-item-configuration.selector.spec.ts | 10 +-- .../map-testing/share-link-item-test.utils.ts | 4 +- src/app/testing/time-service.stub.ts | 43 ++++++++++++ 27 files changed, 260 insertions(+), 157 deletions(-) create mode 100644 src/app/testing/time-service.stub.ts diff --git a/src/app/map/components/time-slider/time-slider.component.ts b/src/app/map/components/time-slider/time-slider.component.ts index ff709a98a..e08dcfad5 100644 --- a/src/app/map/components/time-slider/time-slider.component.ts +++ b/src/app/map/components/time-slider/time-slider.component.ts @@ -1,19 +1,19 @@ -import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; +import {Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; import {TimeExtent} from '../../interfaces/time-extent.interface'; import {TimeSliderConfiguration, TimeSliderLayerSource} from '../../../shared/interfaces/topic.interface'; -import dayjs, {ManipulateType} from 'dayjs'; +import {ManipulateType, UnitType} from 'dayjs'; import {TimeSliderService} from '../../services/time-slider.service'; import {TimeExtentUtils} from '../../../shared/utils/time-extent.utils'; -import duration from 'dayjs/plugin/duration'; import {MatDatepicker} from '@angular/material/datepicker'; - -dayjs.extend(duration); +import {TIME_SERVICE} from '../../../app.module'; +import {TimeService} from '../../interfaces/time.service'; +import {DayjsTimeService} from '../../../shared/services/dayjs-time.service'; // There is an array (`allowedDatePickerManipulationUnits`) and a new union type (`DatePickerManipulationUnits`) for two reasons: // To be able to extract a union type subset of `ManipulateType` AND to have an array used to check if a given value is in said union type. // => more infos: https://stackoverflow.com/questions/50085494/how-to-check-if-a-given-value-is-in-a-union-type-array const allowedDatePickerManipulationUnits = ['years', 'months', 'days'] as const; // TS3.4 syntax -type DatePickerManipulationUnits = Extract; +export type DatePickerManipulationUnits = Extract; type DatePickerStartView = 'month' | 'year' | 'multi-year'; @Component({ @@ -47,7 +47,10 @@ export class TimeSliderComponent implements OnInit, OnChanges { public datePickerStartView: DatePickerStartView = 'month'; private datePickerUnit: DatePickerManipulationUnits = 'days'; - constructor(private readonly timeSliderService: TimeSliderService) {} + constructor( + private readonly timeSliderService: TimeSliderService, + @Inject(TIME_SERVICE) private readonly timeService: TimeService, + ) {} public ngOnInit() { this.availableDates = this.timeSliderService.createStops(this.timeSliderConfiguration); @@ -145,8 +148,12 @@ export class TimeSliderComponent implements OnInit, OnChanges { if (eventDate === null) { return; } + // format the given event date to the configured time format and back to ensure that it is a valid date within the current available dates - const date = dayjs(dayjs(eventDate).format(this.timeSliderConfiguration.dateFormat), this.timeSliderConfiguration.dateFormat).toDate(); + const date = this.timeService.getDate( + this.timeService.getDateAsString(eventDate, this.timeSliderConfiguration.dateFormat), + this.timeSliderConfiguration.dateFormat, + ); const position = this.findPositionOfDate(date); if (position !== undefined) { if (changedMinimumDate) { @@ -170,7 +177,7 @@ export class TimeSliderComponent implements OnInit, OnChanges { */ private isRangeExactlyOneOfSingleTimeUnit(range: string | null | undefined): boolean { if (range) { - const rangeDuration = dayjs.duration(range); + const rangeDuration = DayjsTimeService.getDuration(range); const unit = TimeExtentUtils.extractUniqueUnitFromDuration(rangeDuration); return unit !== undefined && TimeExtentUtils.getDurationAsNumber(rangeDuration, unit) === 1; } @@ -209,7 +216,9 @@ export class TimeSliderComponent implements OnInit, OnChanges { } private isLayerSourceContinuous(layerSource: TimeSliderLayerSource, unit: DatePickerManipulationUnits): boolean { - const dateAsAscendingSortedNumbers = layerSource.layers.map((layer) => dayjs(layer.date, unit).get(unit)).sort((a, b) => a - b); + const dateAsAscendingSortedNumbers = layerSource.layers + .map((layer) => this.timeService.getPartial(layer.date, unit as UnitType)) + .sort((a, b) => a - b); // all date numbers must be part of a continuous and strictly monotonously rising series each with exactly // one step between them: `date[0] = x` => `date[n] = x + n` return !dateAsAscendingSortedNumbers.some((dateAsNumber, index) => dateAsNumber !== dateAsAscendingSortedNumbers[0] + index); diff --git a/src/app/map/interfaces/time.service.ts b/src/app/map/interfaces/time.service.ts index ae9f23db0..f5906b396 100644 --- a/src/app/map/interfaces/time.service.ts +++ b/src/app/map/interfaces/time.service.ts @@ -1,5 +1,4 @@ -import {Duration} from 'dayjs/plugin/duration'; -import {ManipulateType, UnitType} from 'dayjs'; +import {UnitType} from 'dayjs'; export interface TimeService { getPartial(date: string, unit: UnitType): number; @@ -8,17 +7,7 @@ export interface TimeService { getDate(date: string, format: string): Date; - getUTCDate(date: Date, format?: string): Date; + getUTCDateAsString(date: Date, format?: string): string; getUnixDate(created: number): Date; - - getDuration(time: string | number, unit?: ManipulateType): Duration; - - isValidDate(value: string): boolean; - - addDuration(date: Date, durationToAdd: Duration): Date; - - subtractDuration(date: Date, durationToSubtract: Duration): Date; - - calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number; } diff --git a/src/app/map/pipes/date-to-string.pipe.spec.ts b/src/app/map/pipes/date-to-string.pipe.spec.ts index 83b8e3530..5f656aa1b 100644 --- a/src/app/map/pipes/date-to-string.pipe.spec.ts +++ b/src/app/map/pipes/date-to-string.pipe.spec.ts @@ -1,14 +1,24 @@ import {DateToStringPipe} from './date-to-string.pipe'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../interfaces/time.service'; +import {TestBed} from '@angular/core/testing'; +import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; describe('DateToStringPipe', () => { + let pipe: DateToStringPipe; + let timeService: TimeService; + + beforeEach(() => { + TestBed.configureTestingModule({providers: [{provide: TIME_SERVICE, useClass: DayjsTimeService}]}); + timeService = TestBed.inject(TIME_SERVICE); + pipe = new DateToStringPipe(timeService); + }); + it('create an instance', () => { - const pipe = new DateToStringPipe(); expect(pipe).toBeTruthy(); }); it('formats a defined date', () => { - const pipe = new DateToStringPipe(); - const date = new Date(2023, 1, 3); // monthIndex + 1 === month const dateFormat = 'YYYY-MM-DD'; @@ -19,8 +29,6 @@ describe('DateToStringPipe', () => { }); it('formats an undefined date', () => { - const pipe = new DateToStringPipe(); - const date = undefined; const dateFormat = 'YYYY-MM-DD'; diff --git a/src/app/map/pipes/date-to-string.pipe.ts b/src/app/map/pipes/date-to-string.pipe.ts index 3cbb7e61b..e8f94544c 100644 --- a/src/app/map/pipes/date-to-string.pipe.ts +++ b/src/app/map/pipes/date-to-string.pipe.ts @@ -1,12 +1,14 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import dayjs from 'dayjs'; +import {Inject, Pipe, PipeTransform} from '@angular/core'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../interfaces/time.service'; @Pipe({ name: 'dateToString', standalone: true, }) export class DateToStringPipe implements PipeTransform { + constructor(@Inject(TIME_SERVICE) private readonly timeService: TimeService) {} public transform(value: Date | undefined, dateFormat: string): string { - return value ? dayjs(value).format(dateFormat) : ''; + return value ? this.timeService.getDateAsString(value, dateFormat) : ''; } } diff --git a/src/app/map/pipes/time-extent-to-string.pipe.spec.ts b/src/app/map/pipes/time-extent-to-string.pipe.spec.ts index 572d8076c..0d399187a 100644 --- a/src/app/map/pipes/time-extent-to-string.pipe.spec.ts +++ b/src/app/map/pipes/time-extent-to-string.pipe.spec.ts @@ -1,15 +1,24 @@ import {TimeExtentToStringPipe} from './time-extent-to-string.pipe'; import {TimeExtent} from '../interfaces/time-extent.interface'; +import {TestBed} from '@angular/core/testing'; +import {TIME_SERVICE} from '../../app.module'; +import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; +import {TimeService} from '../interfaces/time.service'; describe('TimeExtentToStringPipe', () => { + let pipe: TimeExtentToStringPipe; + let timeService: TimeService; + + beforeEach(() => { + TestBed.configureTestingModule({providers: [{provide: TIME_SERVICE, useClass: DayjsTimeService}]}); + timeService = TestBed.inject(TIME_SERVICE); + pipe = new TimeExtentToStringPipe(timeService); + }); it('create an instance', () => { - const pipe = new TimeExtentToStringPipe(); expect(pipe).toBeTruthy(); }); it('formats a defined time extent', () => { - const pipe = new TimeExtentToStringPipe(); - const timeExtent: TimeExtent = { start: new Date(2023, 1, 3), end: new Date(2024, 2, 4), @@ -24,8 +33,6 @@ describe('TimeExtentToStringPipe', () => { }); it('formats a defined time extent using simple values', () => { - const pipe = new TimeExtentToStringPipe(); - const timeExtent: TimeExtent = { start: new Date(2023, 1, 3), end: new Date(2024, 2, 4), @@ -40,8 +47,6 @@ describe('TimeExtentToStringPipe', () => { }); it('formats an undefined time extent', () => { - const pipe = new TimeExtentToStringPipe(); - const timeExtent = undefined; const dateFormat = 'YYYY-MM-DD'; const hasSimpleCurrentValue = true; diff --git a/src/app/map/pipes/time-extent-to-string.pipe.ts b/src/app/map/pipes/time-extent-to-string.pipe.ts index f39eb5216..346ebddb7 100644 --- a/src/app/map/pipes/time-extent-to-string.pipe.ts +++ b/src/app/map/pipes/time-extent-to-string.pipe.ts @@ -1,12 +1,14 @@ -import {Pipe, PipeTransform} from '@angular/core'; +import {Inject, Pipe, PipeTransform} from '@angular/core'; import {TimeExtent} from '../interfaces/time-extent.interface'; -import dayjs from 'dayjs'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../interfaces/time.service'; @Pipe({ name: 'timeExtentToString', standalone: true, }) export class TimeExtentToStringPipe implements PipeTransform { + constructor(@Inject(TIME_SERVICE) private readonly timeService: TimeService) {} public transform(timeExtent: TimeExtent | undefined, dateFormat: string, hasSimpleCurrentValue: boolean): string { if (!timeExtent) { return ''; @@ -17,6 +19,6 @@ export class TimeExtentToStringPipe implements PipeTransform { } private convertDateToString(value: Date, dateFormat: string): string { - return value ? dayjs(value).format(dateFormat) : ''; + return value ? this.timeService.getDateAsString(value, dateFormat) : ''; } } diff --git a/src/app/map/services/esri-services/esri-map.service.spec.ts b/src/app/map/services/esri-services/esri-map.service.spec.ts index ef142c0a5..228c9fe34 100644 --- a/src/app/map/services/esri-services/esri-map.service.spec.ts +++ b/src/app/map/services/esri-services/esri-map.service.spec.ts @@ -12,6 +12,8 @@ import {EsriMapViewService} from './esri-map-view.service'; import {EsriToolService} from './tool-service/esri-tool.service'; import {createDrawingMapItemMock, createGb2WmsMapItemMock} from '../../../testing/map-testing/active-map-item-test.utils'; import {FilterConfiguration} from '../../../shared/interfaces/topic.interface'; +import {TIME_SERVICE} from '../../../app.module'; +import {DayjsTimeService} from '../../../shared/services/dayjs-time.service'; function compareMapItemToEsriLayer(expectedMapItem: Gb2WmsActiveMapItem, actualEsriLayer: __esri.Layer) { expect(actualEsriLayer.id).toBe(expectedMapItem.id); @@ -74,6 +76,7 @@ describe('EsriMapService', () => { provide: EsriToolService, useValue: toolServiceSpy, }, + {provide: TIME_SERVICE, useClass: DayjsTimeService}, ], }); service = TestBed.inject(EsriMapService); diff --git a/src/app/map/services/esri-services/esri-map.service.ts b/src/app/map/services/esri-services/esri-map.service.ts index 68f4008ca..ee28527fe 100644 --- a/src/app/map/services/esri-services/esri-map.service.ts +++ b/src/app/map/services/esri-services/esri-map.service.ts @@ -1,9 +1,8 @@ -import {Injectable, OnDestroy} from '@angular/core'; +import {Inject, Injectable, OnDestroy} from '@angular/core'; import esriConfig from '@arcgis/core/config'; import * as geometryEngine from '@arcgis/core/geometry/geometryEngine'; import GraphicsLayer from '@arcgis/core/layers/GraphicsLayer'; import {Store} from '@ngrx/store'; -import dayjs from 'dayjs'; import {BehaviorSubject, first, pairwise, skip, Subscription, tap, withLatestFrom} from 'rxjs'; import {filter, map} from 'rxjs/operators'; import {AuthService} from '../../../auth/auth.service'; @@ -65,6 +64,8 @@ import {InitialMapExtentService} from '../initial-map-extent.service'; import {MapConstants} from '../../../shared/constants/map.constants'; import {HitTestSelectionUtils} from './utils/hit-test-selection.utils'; import * as intl from '@arcgis/core/intl'; +import {TIME_SERVICE} from '../../../app.module'; +import {TimeService} from '../../interfaces/time.service'; import GraphicHit = __esri.GraphicHit; const DEFAULT_POINT_ZOOM_EXTENT_SCALE = 750; @@ -106,6 +107,7 @@ export class EsriMapService implements MapService, OnDestroy { private readonly esriToolService: EsriToolService, private readonly gb3TopicsService: Gb3TopicsService, private readonly initialMapExtentService: InitialMapExtentService, + @Inject(TIME_SERVICE) private readonly timeService: TimeService, ) { /** * Because the GetCapabalities response often sends a non-secure http://wms.zh.ch response, Esri Javascript API fails on https @@ -635,8 +637,14 @@ export class EsriMapService implements MapService, OnDestroy { const dateFormat = timeSliderConfiguration.dateFormat; esriLayer.customLayerParameters = esriLayer.customLayerParameters ?? {}; - esriLayer.customLayerParameters[timeSliderParameterSource.startRangeParameter] = dayjs.utc(timeSliderExtent.start).format(dateFormat); - esriLayer.customLayerParameters[timeSliderParameterSource.endRangeParameter] = dayjs.utc(timeSliderExtent.end).format(dateFormat); + esriLayer.customLayerParameters[timeSliderParameterSource.startRangeParameter] = this.timeService.getUTCDateAsString( + timeSliderExtent.start, + dateFormat, + ); + esriLayer.customLayerParameters[timeSliderParameterSource.endRangeParameter] = this.timeService.getUTCDateAsString( + timeSliderExtent.end, + dateFormat, + ); } /** diff --git a/src/app/map/services/favourites.service.spec.ts b/src/app/map/services/favourites.service.spec.ts index 6df9215f7..daf78d12a 100644 --- a/src/app/map/services/favourites.service.spec.ts +++ b/src/app/map/services/favourites.service.spec.ts @@ -22,6 +22,8 @@ import {SymbolizationToGb3ConverterUtils} from '../../shared/utils/symbolization import {Map} from '../../shared/interfaces/topic.interface'; import {TimeExtentUtils} from '../../shared/utils/time-extent.utils'; import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; +import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; +import {TIME_SERVICE} from '../../app.module'; describe('FavouritesService', () => { let service: FavouritesService; @@ -31,7 +33,12 @@ describe('FavouritesService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [provideMockStore({}), provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + providers: [ + provideMockStore({}), + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + {provide: TIME_SERVICE, useClass: DayjsTimeService}, + ], }); store = TestBed.inject(MockStore); store.overrideSelector(selectActiveMapItemConfigurations, []); @@ -384,8 +391,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1000-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, { @@ -529,8 +536,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1000-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -647,8 +654,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1000-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -708,8 +715,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1000-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -798,8 +805,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1000-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -857,8 +864,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1000-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1035,8 +1042,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1000-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1193,8 +1200,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1000-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1350,8 +1357,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('0999-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('0999-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1505,8 +1512,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1450-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('1455-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1450-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('1455-01-01T00:00:00.000Z'), }, }, ]; @@ -1660,8 +1667,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1750-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('1455-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1750-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('1455-01-01T00:00:00.000Z'), }, }, ]; @@ -1815,8 +1822,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1250-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2000-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1250-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2000-01-01T00:00:00.000Z'), }, }, ]; @@ -2081,8 +2088,8 @@ describe('FavouritesService', () => { isSingleLayer: false, attributeFilters: undefined, timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('2016-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2017-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('2016-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2017-01-01T00:00:00.000Z'), }, }, ]; @@ -2347,8 +2354,8 @@ describe('FavouritesService', () => { isSingleLayer: false, attributeFilters: undefined, timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('2016-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2017-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('2016-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2017-01-01T00:00:00.000Z'), }, }, ]; diff --git a/src/app/map/services/favourites.service.ts b/src/app/map/services/favourites.service.ts index ce11cf47e..0eaf345f1 100644 --- a/src/app/map/services/favourites.service.ts +++ b/src/app/map/services/favourites.service.ts @@ -29,6 +29,7 @@ import {Gb3StyledInternalDrawingRepresentation} from '../../shared/interfaces/in import {TimeExtent} from '../interfaces/time-extent.interface'; import {TimeExtentUtils} from '../../shared/utils/time-extent.utils'; import {TimeSliderService} from './time-slider.service'; +import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; @Injectable({ providedIn: 'root', @@ -351,7 +352,7 @@ export class FavouritesService implements OnDestroy { return isTimeExtentValid; case 'layer': { const selectedYearExists = (timeSliderConfiguration.source as TimeSliderLayerSource).layers.some( - (layer) => TimeExtentUtils.parseUTCDate(layer.date, timeSliderConfiguration.dateFormat).getTime() === timeExtent.start.getTime(), + (layer) => DayjsTimeService.parseUTCDate(layer.date, timeSliderConfiguration.dateFormat).getTime() === timeExtent.start.getTime(), ); return selectedYearExists && isTimeExtentValid; } diff --git a/src/app/map/services/time-slider.service.spec.ts b/src/app/map/services/time-slider.service.spec.ts index aa2a9997c..8a167a710 100644 --- a/src/app/map/services/time-slider.service.spec.ts +++ b/src/app/map/services/time-slider.service.spec.ts @@ -1,15 +1,16 @@ import {TestBed} from '@angular/core/testing'; - import {TimeSliderService} from './time-slider.service'; import dayjs from 'dayjs'; import {TimeSliderConfiguration, TimeSliderParameterSource} from '../../shared/interfaces/topic.interface'; import {TimeExtent} from '../interfaces/time-extent.interface'; +import {TIME_SERVICE} from '../../app.module'; +import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; describe('TimeSliderService', () => { let service: TimeSliderService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({providers: [{provide: TIME_SERVICE, useClass: DayjsTimeService}]}); service = TestBed.inject(TimeSliderService); }); diff --git a/src/app/map/services/time-slider.service.ts b/src/app/map/services/time-slider.service.ts index 4511f24aa..1d0696989 100644 --- a/src/app/map/services/time-slider.service.ts +++ b/src/app/map/services/time-slider.service.ts @@ -1,18 +1,19 @@ -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {TimeSliderConfiguration, TimeSliderLayerSource} from '../../shared/interfaces/topic.interface'; -import dayjs from 'dayjs'; -import duration, {Duration} from 'dayjs/plugin/duration'; +import {Duration} from 'dayjs/plugin/duration'; import {TimeExtentUtils} from '../../shared/utils/time-extent.utils'; import {TimeExtent} from '../interfaces/time-extent.interface'; import {InvalidTimeSliderConfiguration} from '../../shared/errors/map.errors'; - -dayjs.extend(duration); +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../interfaces/time.service'; +import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; @Injectable({ providedIn: 'root', }) export class TimeSliderService { + constructor(@Inject(TIME_SERVICE) private readonly timeService: TimeService) {} /** * Creates stops which define specific locations on the time slider where thumbs will snap to when manipulated. */ @@ -51,7 +52,7 @@ export class TimeSliderService { The start has changed as fixed ranges technically don't have an end date => the end date has to be adjusted accordingly to enforce the fixed range between start and end date */ - const range: Duration = dayjs.duration(timeSliderConfig.range); + const range: Duration = DayjsTimeService.getDuration(timeSliderConfig.range); timeExtent.end = TimeExtentUtils.addDuration(timeExtent.start, range); } else if (timeSliderConfig.minimalRange) { /* @@ -74,7 +75,7 @@ export class TimeSliderService { } const startEndDiff: number = TimeExtentUtils.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end); - const minimalRange: Duration = dayjs.duration(timeSliderConfig.minimalRange); + const minimalRange: Duration = DayjsTimeService.getDuration(timeSliderConfig.minimalRange); if (startEndDiff < minimalRange.asMilliseconds()) { if (hasStartDateChanged) { @@ -97,8 +98,8 @@ export class TimeSliderService { } public isTimeExtentValid(timeSliderConfig: TimeSliderConfiguration, timeExtent: TimeExtent): boolean { - const minDate = TimeExtentUtils.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maxDate = TimeExtentUtils.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const minDate = DayjsTimeService.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maxDate = DayjsTimeService.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); const updatedTimeExtent: TimeExtent = this.createValidTimeExtent(timeSliderConfig, timeExtent, false, minDate, maxDate); @@ -110,7 +111,7 @@ export class TimeSliderService { */ private createStopsForLayerSource(timeSliderConfig: TimeSliderConfiguration): Array { const timeSliderLayerSource = timeSliderConfig.source as TimeSliderLayerSource; - return timeSliderLayerSource.layers.map((layer) => TimeExtentUtils.parseUTCDate(layer.date, timeSliderConfig.dateFormat)); + return timeSliderLayerSource.layers.map((layer) => DayjsTimeService.parseUTCDate(layer.date, timeSliderConfig.dateFormat)); } /** @@ -122,10 +123,10 @@ export class TimeSliderService { * start to finish using the given duration; this can lead to gaps near the end but supports all cases. */ private createStopsForParameterSource(timeSliderConfig: TimeSliderConfiguration): Array { - const minimumDate: Date = TimeExtentUtils.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maximumDate: Date = TimeExtentUtils.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const minimumDate: Date = DayjsTimeService.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maximumDate: Date = DayjsTimeService.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); const initialRange: string | null = timeSliderConfig.range ?? timeSliderConfig.minimalRange ?? null; - let stopRangeDuration: Duration | null = initialRange ? dayjs.duration(initialRange) : null; + let stopRangeDuration: Duration | null = initialRange ? DayjsTimeService.getDuration(initialRange) : null; if ( stopRangeDuration && TimeExtentUtils.calculateDifferenceBetweenDates(minimumDate, maximumDate) <= stopRangeDuration.asMilliseconds() @@ -139,7 +140,7 @@ export class TimeSliderService { } // create a new duration base on the smallest unit with the lowest valid unit number (1) - stopRangeDuration = dayjs.duration(1, unit); + stopRangeDuration = DayjsTimeService.getDurationWithUnit(1, unit); } const dates: Date[] = []; diff --git a/src/app/map/utils/active-time-slider-layers.utils.ts b/src/app/map/utils/active-time-slider-layers.utils.ts index 283f9ec19..3dd8b6f3e 100644 --- a/src/app/map/utils/active-time-slider-layers.utils.ts +++ b/src/app/map/utils/active-time-slider-layers.utils.ts @@ -1,6 +1,6 @@ import {MapLayer, TimeSliderConfiguration, TimeSliderLayerSource} from '../../shared/interfaces/topic.interface'; import {TimeExtent} from '../interfaces/time-extent.interface'; -import {TimeExtentUtils} from '../../shared/utils/time-extent.utils'; +import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; export class ActiveTimeSliderLayersUtils { /** @@ -19,7 +19,7 @@ export class ActiveTimeSliderLayersUtils { const timeSliderLayerSource = timeSliderConfiguration.source as TimeSliderLayerSource; const timeSliderLayer = timeSliderLayerSource.layers.find((layer) => layer.layerName === mapLayer.layer); if (timeSliderLayer) { - const date = TimeExtentUtils.parseUTCDate(timeSliderLayer.date, timeSliderConfiguration.dateFormat); + const date = DayjsTimeService.parseUTCDate(timeSliderLayer.date, timeSliderConfiguration.dateFormat); return date >= timeExtent.start && date < timeExtent.end; } else { return undefined; diff --git a/src/app/shared/services/apis/gb3/gb3-favourites.service.ts b/src/app/shared/services/apis/gb3/gb3-favourites.service.ts index 7e9044273..27d33712b 100644 --- a/src/app/shared/services/apis/gb3/gb3-favourites.service.ts +++ b/src/app/shared/services/apis/gb3/gb3-favourites.service.ts @@ -9,8 +9,8 @@ import { import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {CreateFavourite, Favourite, FavouritesResponse} from '../../../interfaces/favourite.interface'; -import {TimeExtentUtils} from '../../../utils/time-extent.utils'; import {ApiGeojsonGeometryToGb3ConverterUtils} from '../../../utils/api-geojson-geometry-to-gb3-converter.utils'; +import {DayjsTimeService} from '../../dayjs-time.service'; @Injectable({ providedIn: 'root', @@ -67,8 +67,8 @@ export class Gb3FavouritesService extends Gb3ApiService { attributeFilters: content.attributeFilters, timeExtent: content.timeExtent ? { - start: TimeExtentUtils.parseDefaultUTCDate(content.timeExtent.start), - end: TimeExtentUtils.parseDefaultUTCDate(content.timeExtent.end), + start: DayjsTimeService.parseUTCDate(content.timeExtent.start), + end: DayjsTimeService.parseUTCDate(content.timeExtent.end), } : undefined, }; diff --git a/src/app/shared/services/apis/gb3/gb3-share-link.service.spec.ts b/src/app/shared/services/apis/gb3/gb3-share-link.service.spec.ts index 846cff574..752a70411 100644 --- a/src/app/shared/services/apis/gb3/gb3-share-link.service.spec.ts +++ b/src/app/shared/services/apis/gb3/gb3-share-link.service.spec.ts @@ -12,11 +12,15 @@ import {selectMaps} from '../../../../state/map/selectors/maps.selector'; import {selectActiveMapItemConfigurations} from '../../../../state/map/selectors/active-map-item-configuration.selector'; import {selectFavouriteBaseConfig} from '../../../../state/map/selectors/favourite-base-config.selector'; import {selectUserDrawingsVectorLayers} from '../../../../state/map/selectors/user-drawings-vector-layers.selector'; +import {DayjsTimeService} from '../../dayjs-time.service'; +import {TimeService} from '../../../../map/interfaces/time.service'; +import {TIME_SERVICE} from '../../../../app.module'; // todo: add tests for vector layers const mockedVectorLayer = {type: undefined, styles: undefined, geojson: {type: undefined, features: []}} as unknown as Gb3VectorLayer; describe('Gb3ShareLinkService', () => { let service: Gb3ShareLinkService; + let timeService: TimeService; let store: MockStore; const shareLinkItemIdMock = 'mock-id'; const serverDataMock: SharedFavorite = { @@ -104,7 +108,12 @@ describe('Gb3ShareLinkService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [provideMockStore({}), provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + providers: [ + provideMockStore({}), + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + {provide: TIME_SERVICE, useClass: DayjsTimeService}, + ], }); store = TestBed.inject(MockStore); store.overrideSelector(selectActiveMapItemConfigurations, []); diff --git a/src/app/shared/services/apis/gb3/gb3-share-link.service.ts b/src/app/shared/services/apis/gb3/gb3-share-link.service.ts index eb848ea6b..f9caa8551 100644 --- a/src/app/shared/services/apis/gb3/gb3-share-link.service.ts +++ b/src/app/shared/services/apis/gb3/gb3-share-link.service.ts @@ -11,7 +11,7 @@ import {HttpClient} from '@angular/common/http'; import {BasemapConfigService} from '../../../../map/services/basemap-config.service'; import {FavouritesService} from '../../../../map/services/favourites.service'; import {MapRestoreItem} from '../../../interfaces/map-restore-item.interface'; -import {TimeExtentUtils} from '../../../utils/time-extent.utils'; +import {DayjsTimeService} from '../../dayjs-time.service'; @Injectable({ providedIn: 'root', @@ -93,8 +93,8 @@ export class Gb3ShareLinkService extends Gb3ApiService { attributeFilters: content.attributeFilters, timeExtent: content.timeExtent ? { - start: TimeExtentUtils.parseDefaultUTCDate(content.timeExtent.start), - end: TimeExtentUtils.parseDefaultUTCDate(content.timeExtent.end), + start: DayjsTimeService.parseUTCDate(content.timeExtent.start), + end: DayjsTimeService.parseUTCDate(content.timeExtent.end), } : undefined, }; diff --git a/src/app/shared/services/apis/grav-cms/grav-cms.service.spec.ts b/src/app/shared/services/apis/grav-cms/grav-cms.service.spec.ts index 52da2e359..064f68db8 100644 --- a/src/app/shared/services/apis/grav-cms/grav-cms.service.spec.ts +++ b/src/app/shared/services/apis/grav-cms/grav-cms.service.spec.ts @@ -10,6 +10,8 @@ import {PageNotification} from '../../../interfaces/page-notification.interface' import {MainPage} from '../../../enums/main-page.enum'; import {FrequentlyUsedItem} from '../../../interfaces/frequently-used-item.interface'; import {provideMockStore} from '@ngrx/store/testing'; +import {TIME_SERVICE} from '../../../../app.module'; +import {DayjsTimeService} from '../../dayjs-time.service'; describe('GravCmsService', () => { let service: GravCmsService; @@ -18,7 +20,12 @@ describe('GravCmsService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [provideMockStore(), provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + providers: [ + provideMockStore(), + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + {provide: TIME_SERVICE, useClass: DayjsTimeService}, + ], }); service = TestBed.inject(GravCmsService); configService = TestBed.inject(ConfigService); diff --git a/src/app/shared/services/apis/grav-cms/grav-cms.service.ts b/src/app/shared/services/apis/grav-cms/grav-cms.service.ts index fe22a6822..bcba166c0 100644 --- a/src/app/shared/services/apis/grav-cms/grav-cms.service.ts +++ b/src/app/shared/services/apis/grav-cms/grav-cms.service.ts @@ -1,16 +1,17 @@ -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {BaseApiService} from '../abstract-api.service'; import {Observable} from 'rxjs'; import {DiscoverMapsItem} from '../../../interfaces/discover-maps-item.interface'; import {map} from 'rxjs/operators'; import {DiscoverMapsRoot, FrequentlyUsedRoot, PageInfosRoot, Pages} from '../../../models/grav-cms-generated.interfaces'; import {PageNotification, PageNotificationSeverity} from '../../../interfaces/page-notification.interface'; -import dayjs from 'dayjs'; import {MainPage} from '../../../enums/main-page.enum'; import {FrequentlyUsedItem} from '../../../interfaces/frequently-used-item.interface'; -import customParseFormat from 'dayjs/plugin/customParseFormat'; +import {TIME_SERVICE} from '../../../../app.module'; +import {TimeService} from '../../../../map/interfaces/time.service'; +import {HttpClient} from '@angular/common/http'; +import {ConfigService} from '../../config.service'; -dayjs.extend(customParseFormat); const DATE_FORMAT = 'DD.MM.YYYY'; @Injectable({ @@ -22,6 +23,13 @@ export class GravCmsService extends BaseApiService { private readonly pageInfosEndpoint: string = 'pageinfos.json'; private readonly frequentlyUsedItemsEndpoint: string = 'frequentlyused.json'; + constructor( + @Inject(TIME_SERVICE) private readonly timeService: TimeService, + httpClient: HttpClient, + configService: ConfigService, + ) { + super(httpClient, configService); + } public loadDiscoverMapsData(): Observable { const requestUrl = this.createFullEndpointUrl(this.discoverMapsEndpoint); return this.get(requestUrl).pipe(map((response) => this.transformDiscoverMapsData(response))); @@ -44,8 +52,8 @@ export class GravCmsService extends BaseApiService { title: discoverMapData.title, description: discoverMapData.description, mapId: discoverMapData.id, - fromDate: dayjs(discoverMapData.from_date, DATE_FORMAT).toDate(), - toDate: dayjs(discoverMapData.to_date, DATE_FORMAT).toDate(), + fromDate: this.timeService.getDate(discoverMapData.from_date, DATE_FORMAT), + toDate: this.timeService.getDate(discoverMapData.to_date, DATE_FORMAT), image: { url: this.createFullImageUrl(discoverMapData.image.path), name: discoverMapData.image.name, @@ -65,8 +73,8 @@ export class GravCmsService extends BaseApiService { title: pageInfoData.title, description: pageInfoData.description, pages: this.transformPagesToMainPages(pageInfoData.pages), - fromDate: dayjs(pageInfoData.from_date, DATE_FORMAT).toDate(), - toDate: dayjs(pageInfoData.to_date, DATE_FORMAT).toDate(), + fromDate: this.timeService.getDate(pageInfoData.from_date, DATE_FORMAT), + toDate: this.timeService.getDate(pageInfoData.to_date, DATE_FORMAT), severity: pageInfoData.severity as PageNotificationSeverity, isMarkedAsRead: false, }; @@ -90,7 +98,7 @@ export class GravCmsService extends BaseApiService { altText: frequentlyUsedData.image_alt, } : undefined, - created: dayjs.unix(+frequentlyUsedData.created).toDate(), + created: this.timeService.getUnixDate(+frequentlyUsedData.created), }; }); } diff --git a/src/app/shared/services/dayjs-time.service.ts b/src/app/shared/services/dayjs-time.service.ts index 81b19ef28..65c126f94 100644 --- a/src/app/shared/services/dayjs-time.service.ts +++ b/src/app/shared/services/dayjs-time.service.ts @@ -2,8 +2,8 @@ import {TimeService} from '../../map/interfaces/time.service'; import dayjs, {ManipulateType, UnitType} from 'dayjs'; import duration, {Duration} from 'dayjs/plugin/duration'; import {Injectable} from '@angular/core'; -import customParseFormat from 'dayjs/plugin/customParseFormat'; import utc from 'dayjs/plugin/utc'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; dayjs.extend(duration); dayjs.extend(customParseFormat); @@ -22,25 +22,31 @@ export class DayjsTimeService implements TimeService { public getDate(date: string, format: string): Date { return dayjs(date, format).toDate(); } - public getUTCDate(date: Date, format?: string): Date { - return dayjs.utc(date, format).toDate(); + public getUTCDateAsString(date: Date, format?: string): string { + return dayjs.utc(date).format(format); } public getUnixDate(created: number): Date { return dayjs.unix(created).toDate(); } - public getDuration(time: string | number, unit?: ManipulateType): Duration { + public static parseUTCDate(date: string, format?: string): Date { + return dayjs.utc(date, format).toDate(); + } + public static getDuration(time: string): Duration { + return dayjs.duration(time); + } + public static getDurationWithUnit(time: number, unit?: ManipulateType): Duration { return dayjs.duration(time, unit); } - public isValidDate(value: string): boolean { + public static isValidDate(value: string): boolean { return dayjs(value).isValid(); } - public addDuration(date: Date, durationToAdd: Duration): Date { + public static addDuration(date: Date, durationToAdd: Duration): Date { return dayjs(date).add(durationToAdd).toDate(); } - public subtractDuration(date: Date, durationToSubtract: Duration): Date { + public static subtractDuration(date: Date, durationToSubtract: Duration): Date { return dayjs(date).subtract(durationToSubtract).toDate(); } - public calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number { + public static calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number { return Math.abs(dayjs(secondDate).diff(dayjs(firstDate))); } } diff --git a/src/app/shared/utils/storage.utils.spec.ts b/src/app/shared/utils/storage.utils.spec.ts index 54d2d9800..b2f23b7ad 100644 --- a/src/app/shared/utils/storage.utils.spec.ts +++ b/src/app/shared/utils/storage.utils.spec.ts @@ -1,14 +1,14 @@ import {StorageUtils} from './storage.utils'; -import {TimeExtentUtils} from './time-extent.utils'; +import {DayjsTimeService} from '../services/dayjs-time.service'; describe('StorageUtils', () => { describe('parseJson', () => { it(`parses a Json with valid Dates correctly`, () => { - const stringToParse: string = + const stringToParse = '{"date":"1506-01-01T00:00:00.000Z", "number": 2683132, "string": "test", "stringifiedNumberParseableAsDate": "12"}'; const expectedJsonObject = { - date: TimeExtentUtils.parseDefaultUTCDate('1506-01-01T00:00:00.000Z'), + date: DayjsTimeService.parseUTCDate('1506-01-01T00:00:00.000Z'), number: 2683132, string: 'test', stringifiedNumberParseableAsDate: '12', @@ -16,13 +16,13 @@ describe('StorageUtils', () => { expect(StorageUtils.parseJson(stringToParse)).toEqual(expectedJsonObject); }); it(`does not parses an invalid Date-String `, () => { - const stringToParse: string = '{"invalidDate":"T1506-01-01T00:00:00.000Z"}'; + const stringToParse = '{"invalidDate":"T1506-01-01T00:00:00.000Z"}'; const expectedJsonObject = {invalidDate: 'T1506-01-01T00:00:00.000Z'}; expect(StorageUtils.parseJson(stringToParse)).toEqual(expectedJsonObject); }); it(`does not parses a number as Date`, () => { - const stringToParse: string = '{"validDateAsNumber":19000101}'; + const stringToParse = '{"validDateAsNumber":19000101}'; const expectedJsonObject = {validDateAsNumber: 19000101}; expect(StorageUtils.parseJson(stringToParse)).toEqual(expectedJsonObject); diff --git a/src/app/shared/utils/storage.utils.ts b/src/app/shared/utils/storage.utils.ts index 26a5cd6e3..1d5b8fe02 100644 --- a/src/app/shared/utils/storage.utils.ts +++ b/src/app/shared/utils/storage.utils.ts @@ -1,11 +1,13 @@ -import dayjs from 'dayjs'; -import {TimeExtentUtils} from './time-extent.utils'; +import {DayjsTimeService} from '../services/dayjs-time.service'; export class StorageUtils { /** * Returns a date in the correct format after parsing from stringified Object. This is used within the JSON.parse method, hence the * "any" type for the value parameter. * + * The comment below refers to the implementation in the DayjsTimeService, which has been created to have all dayjs related methods in one place. + * This comment is only really refering to this specific usecase of the isValidDate method, which is why the comment is not in the DayjsTimeService itself. + * * Note that we're not using the "strict" mode of "dayjs.isValid" here, because we parse a stringified date that was created by using * JSON.stringify(). This uses the ISO8601 format, which looks like YYYY-MM-DDTHH:mm:ss.SSSZ. The strict mode for dayjs() does not work * properly with the "Z" parameter (see e.g. https://github.com/iamkun/dayjs/issues/1729), although it theoretically should. Instead of @@ -14,8 +16,8 @@ export class StorageUtils { * the original string. If it is, we return the parsed date, otherwise the original string (see GB3-1597). */ private static reviver(key: string, value: any): any { - if (typeof value === 'string' && dayjs(value).isValid()) { - const parsed = TimeExtentUtils.parseDefaultUTCDate(value); + if (typeof value === 'string' && DayjsTimeService.isValidDate(value)) { + const parsed = DayjsTimeService.parseUTCDate(value); return parsed.toISOString() === value ? parsed : value; } return value; diff --git a/src/app/shared/utils/time-extent.utils.ts b/src/app/shared/utils/time-extent.utils.ts index b643e5d67..f178f7024 100644 --- a/src/app/shared/utils/time-extent.utils.ts +++ b/src/app/shared/utils/time-extent.utils.ts @@ -1,33 +1,23 @@ import {Duration} from 'dayjs/plugin/duration'; -import utc from 'dayjs/plugin/utc'; -import dayjs, {ManipulateType} from 'dayjs'; +import {ManipulateType} from 'dayjs'; import {TimeSliderConfiguration} from '../interfaces/topic.interface'; import {TimeExtent} from '../../map/interfaces/time-extent.interface'; - -dayjs.extend(utc); +import {DayjsTimeService} from '../services/dayjs-time.service'; export class TimeExtentUtils { /** * Creates an initial time extent based on the given time slider configuration. */ public static createInitialTimeSliderExtent(timeSliderConfig: TimeSliderConfiguration): TimeExtent { - const minimumDate: Date = TimeExtentUtils.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maximumDate: Date = TimeExtentUtils.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); - const range: Duration | null = timeSliderConfig.range ? dayjs.duration(timeSliderConfig.range) : null; + const minimumDate: Date = DayjsTimeService.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maximumDate: Date = DayjsTimeService.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const range: Duration | null = timeSliderConfig.range ? DayjsTimeService.getDuration(timeSliderConfig.range) : null; return { start: minimumDate, end: range ? TimeExtentUtils.addDuration(minimumDate, range) : maximumDate, }; } - public static parseUTCDate(date: string, format: string): Date { - return dayjs.utc(date, format).toDate(); - } - - public static parseDefaultUTCDate(date: string): Date { - return dayjs.utc(date).toDate(); - } - /** * Extracts the unit from the given duration or if it contains values with multiple units. * @@ -110,10 +100,10 @@ export class TimeExtentUtils { public static addDuration(date: Date, duration: Duration): Date { const unit = TimeExtentUtils.extractUniqueUnitFromDuration(duration); if (!unit) { - return dayjs(date).add(duration).toDate(); + return DayjsTimeService.addDuration(date, duration); } const value = TimeExtentUtils.getDurationAsNumber(duration, unit); - return dayjs(date).add(value, unit).toDate(); + return DayjsTimeService.addDuration(date, DayjsTimeService.getDurationWithUnit(value, unit)); } /** @@ -132,10 +122,10 @@ export class TimeExtentUtils { public static subtractDuration(date: Date, duration: Duration): Date { const unit = TimeExtentUtils.extractUniqueUnitFromDuration(duration); if (!unit) { - return dayjs(date).subtract(duration).toDate(); + return DayjsTimeService.subtractDuration(date, duration); } const value = TimeExtentUtils.getDurationAsNumber(duration, unit); - return dayjs(date).subtract(value, unit).toDate(); + return DayjsTimeService.subtractDuration(date, DayjsTimeService.getDurationWithUnit(value, unit)); } /** @@ -183,6 +173,6 @@ export class TimeExtentUtils { * Returns the difference in milliseconds between the two given dates. */ public static calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number { - return Math.abs(dayjs(firstDate).diff(secondDate)); + return DayjsTimeService.calculateDifferenceBetweenDates(firstDate, secondDate); } } diff --git a/src/app/state/auth/effects/auth-status.effects.spec.ts b/src/app/state/auth/effects/auth-status.effects.spec.ts index a2478b255..1bc657d3d 100644 --- a/src/app/state/auth/effects/auth-status.effects.spec.ts +++ b/src/app/state/auth/effects/auth-status.effects.spec.ts @@ -15,7 +15,7 @@ import {selectActiveMapItemConfigurations} from '../../map/selectors/active-map- import {selectMaps} from '../../map/selectors/maps.selector'; import {selectFavouriteBaseConfig} from '../../map/selectors/favourite-base-config.selector'; import {selectUserDrawingsVectorLayers} from '../../map/selectors/user-drawings-vector-layers.selector'; -import {MAP_SERVICE} from '../../../app.module'; +import {MAP_SERVICE, TIME_SERVICE} from '../../../app.module'; import {MapServiceStub} from '../../../testing/map-testing/map.service.stub'; import {LayerCatalogActions} from '../../map/actions/layer-catalog.actions'; import {Gb3ShareLinkService} from '../../../shared/services/apis/gb3/gb3-share-link.service'; @@ -29,6 +29,7 @@ import {selectItems} from '../../map/selectors/active-map-items.selector'; import {selectDrawings} from '../../map/reducers/drawing.reducer'; import {ToolService} from '../../../map/interfaces/tool.service'; import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; +import {DayjsTimeService} from '../../../shared/services/dayjs-time.service'; const mockOAuthService = jasmine.createSpyObj({ logout: void 0, @@ -52,6 +53,7 @@ describe('AuthStatusEffects', () => { {provide: AuthService, useValue: mockOAuthService}, AuthStatusEffects, {provide: MAP_SERVICE, useClass: MapServiceStub}, + {provide: TIME_SERVICE, useClass: DayjsTimeService}, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), ], diff --git a/src/app/state/map/effects/share-link.effects.spec.ts b/src/app/state/map/effects/share-link.effects.spec.ts index f9f21108d..d7ea7c3d9 100644 --- a/src/app/state/map/effects/share-link.effects.spec.ts +++ b/src/app/state/map/effects/share-link.effects.spec.ts @@ -34,8 +34,8 @@ import {DrawingLayerPrefix, UserDrawingLayer} from '../../../shared/enums/drawin import {ActiveMapItemFactory} from '../../../shared/factories/active-map-item.factory'; import {selectIsAuthenticated, selectIsAuthenticationInitialized} from '../../auth/reducers/auth-status.reducer'; import {MapRestoreItem} from '../../../shared/interfaces/map-restore-item.interface'; -import {TimeExtentUtils} from '../../../shared/utils/time-extent.utils'; import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; +import {DayjsTimeService} from '../../../shared/services/dayjs-time.service'; function createActiveMapItemsFromConfigs(activeMapItemConfigurations: ActiveMapItemConfiguration[]): ActiveMapItem[] { return activeMapItemConfigurations.map( @@ -83,7 +83,7 @@ describe('ShareLinkEffects', () => { opacity: 0.5, visible: true, isSingleLayer: false, - timeExtent: {start: TimeExtentUtils.parseDefaultUTCDate('1000'), end: TimeExtentUtils.parseDefaultUTCDate('2020')}, + timeExtent: {start: DayjsTimeService.parseUTCDate('1000'), end: DayjsTimeService.parseUTCDate('2020')}, attributeFilters: [ { parameter: 'FILTER_GEBART', diff --git a/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts b/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts index a77f9248c..148ba0856 100644 --- a/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts +++ b/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts @@ -1,8 +1,8 @@ import {selectActiveMapItemConfigurations} from './active-map-item-configuration.selector'; -import {TimeExtentUtils} from '../../../shared/utils/time-extent.utils'; import {ActiveMapItemConfiguration} from '../../../shared/interfaces/active-map-item-configuration.interface'; import {ActiveMapItemFactory} from '../../../shared/factories/active-map-item.factory'; import {Map} from '../../../shared/interfaces/topic.interface'; +import {DayjsTimeService} from '../../../shared/services/dayjs-time.service'; describe('selectActiveMapItemConfiguration', () => { it('returns activeMapItemConfigurations from ActiveMapItmes', () => { @@ -31,8 +31,8 @@ describe('selectActiveMapItemConfiguration', () => { true, 0.71, { - start: TimeExtentUtils.parseDefaultUTCDate('1000-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), }, [ { @@ -90,8 +90,8 @@ describe('selectActiveMapItemConfiguration', () => { }, ], timeExtent: { - start: TimeExtentUtils.parseDefaultUTCDate('1000-01-01T00:00:00.000Z'), - end: TimeExtentUtils.parseDefaultUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; diff --git a/src/app/testing/map-testing/share-link-item-test.utils.ts b/src/app/testing/map-testing/share-link-item-test.utils.ts index 4eda3ad0d..1e2bc1c0e 100644 --- a/src/app/testing/map-testing/share-link-item-test.utils.ts +++ b/src/app/testing/map-testing/share-link-item-test.utils.ts @@ -1,6 +1,6 @@ import {ShareLinkItem} from '../../shared/interfaces/share-link.interface'; import {MinimalGeometriesUtils} from './minimal-geometries.utils'; -import {TimeExtentUtils} from '../../shared/utils/time-extent.utils'; +import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; export class ShareLinkItemTestUtils { public static createShareLinkItem(): ShareLinkItem { @@ -33,7 +33,7 @@ export class ShareLinkItemTestUtils { opacity: 0.5, visible: true, isSingleLayer: false, - timeExtent: {start: TimeExtentUtils.parseDefaultUTCDate('1000'), end: TimeExtentUtils.parseDefaultUTCDate('2020')}, + timeExtent: {start: DayjsTimeService.parseUTCDate('1000'), end: DayjsTimeService.parseUTCDate('2020')}, attributeFilters: [ { parameter: 'FILTER_GEBART', diff --git a/src/app/testing/time-service.stub.ts b/src/app/testing/time-service.stub.ts new file mode 100644 index 000000000..b6ebc801c --- /dev/null +++ b/src/app/testing/time-service.stub.ts @@ -0,0 +1,43 @@ +import {TimeService} from '../map/interfaces/time.service'; + +export class TimeServiceStub implements TimeService { + public getPartial(date: string, unit: any): number { + return 0; + } + + public getDateAsString(date: Date, format: string): string { + return ''; + } + + public getDate(date: string, format: string): Date { + return new Date(); + } + + public getUTCDate(date: Date, format?: string): Date { + return new Date(); + } + + public getUnixDate(created: number): Date { + return new Date(); + } + + public getDuration(time: string | number, unit?: any): any { + return {}; + } + + public isValidDate(value: string): boolean { + return true; + } + + public addDuration(date: Date, durationToAdd: any): Date { + return new Date(); + } + + public subtractDuration(date: Date, durationToSubtract: any): Date { + return new Date(); + } + + public calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number { + return 0; + } +} From 29e13311ce885d23672f08d7a029f800ab5d1cca Mon Sep 17 00:00:00 2001 From: till_schuetze Date: Wed, 18 Sep 2024 11:15:23 +0200 Subject: [PATCH 03/12] refactor time.service to dayjs.utils.ts --- src/app/app.module.ts | 4 -- .../time-slider/time-slider.component.ts | 19 ++---- src/app/map/interfaces/time.service.ts | 13 ---- src/app/map/pipes/date-to-string.pipe.spec.ts | 9 +-- src/app/map/pipes/date-to-string.pipe.ts | 9 ++- .../pipes/time-extent-to-string.pipe.spec.ts | 9 +-- .../map/pipes/time-extent-to-string.pipe.ts | 9 ++- .../esri-services/esri-map.service.spec.ts | 3 - .../esri-services/esri-map.service.ts | 10 ++- .../map/services/favourites.service.spec.ts | 66 +++++++++---------- src/app/map/services/favourites.service.ts | 4 +- .../map/services/time-slider.service.spec.ts | 4 +- src/app/map/services/time-slider.service.ts | 26 ++++---- .../utils/active-time-slider-layers.utils.ts | 4 +- .../apis/gb3/gb3-favourites.service.ts | 6 +- .../apis/gb3/gb3-share-link.service.spec.ts | 11 +--- .../apis/gb3/gb3-share-link.service.ts | 6 +- .../apis/grav-cms/grav-cms.service.spec.ts | 9 +-- .../apis/grav-cms/grav-cms.service.ts | 21 +++--- .../dayjs.utils.ts} | 17 ++--- src/app/shared/utils/storage.utils.spec.ts | 4 +- src/app/shared/utils/storage.utils.ts | 6 +- src/app/shared/utils/time-extent.utils.ts | 18 ++--- .../auth/effects/auth-status.effects.spec.ts | 4 +- .../map/effects/share-link.effects.spec.ts | 4 +- ...ve-map-item-configuration.selector.spec.ts | 10 +-- .../map-testing/share-link-item-test.utils.ts | 4 +- src/app/testing/time-service.stub.ts | 43 ------------ 28 files changed, 115 insertions(+), 237 deletions(-) delete mode 100644 src/app/map/interfaces/time.service.ts rename src/app/shared/{services/dayjs-time.service.ts => utils/dayjs.utils.ts} (74%) delete mode 100644 src/app/testing/time-service.stub.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7b2214ab6..5b0b45f4c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -31,8 +31,6 @@ import {effectErrorHandler} from './state/app/effects/effects-error-handler.effe import {EsriMapLoaderService} from './map/services/esri-services/esri-map-loader.service'; import {MapLoaderService} from './map/interfaces/map-loader.service'; import {DevModeBannerComponent} from './shared/components/dev-mode-banner/dev-mode-banner.component'; -import {DayjsTimeService} from './shared/services/dayjs-time.service'; -import {TimeService} from './map/interfaces/time.service'; // necessary for the locale 'de-CH' to work // see https://stackoverflow.com/questions/46419026/missing-locale-data-for-the-locale-xxx-with-angular @@ -42,7 +40,6 @@ export const MAP_SERVICE = new InjectionToken('MapService'); export const MAP_LOADER_SERVICE = new InjectionToken('MapLoaderService'); export const NEWS_SERVICE = new InjectionToken('NewsService'); export const GRAV_CMS_SERVICE = new InjectionToken('GravCmsService'); -export const TIME_SERVICE = new InjectionToken('TimeService'); @NgModule({ declarations: [AppComponent], @@ -65,7 +62,6 @@ export const TIME_SERVICE = new InjectionToken('TimeService'); {provide: MAP_LOADER_SERVICE, useClass: EsriMapLoaderService}, {provide: NEWS_SERVICE, deps: [KTZHNewsService, KTZHNewsServiceMock, ConfigService], useFactory: newsFactory}, {provide: GRAV_CMS_SERVICE, deps: [GravCmsService, GravCmsServiceMock, ConfigService], useFactory: gravCmsFactory}, - {provide: TIME_SERVICE, useClass: DayjsTimeService}, {provide: LOCALE_ID, useValue: 'de-CH'}, { provide: EFFECTS_ERROR_HANDLER, diff --git a/src/app/map/components/time-slider/time-slider.component.ts b/src/app/map/components/time-slider/time-slider.component.ts index e08dcfad5..770e8546b 100644 --- a/src/app/map/components/time-slider/time-slider.component.ts +++ b/src/app/map/components/time-slider/time-slider.component.ts @@ -1,13 +1,11 @@ -import {Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; +import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; import {TimeExtent} from '../../interfaces/time-extent.interface'; import {TimeSliderConfiguration, TimeSliderLayerSource} from '../../../shared/interfaces/topic.interface'; import {ManipulateType, UnitType} from 'dayjs'; import {TimeSliderService} from '../../services/time-slider.service'; import {TimeExtentUtils} from '../../../shared/utils/time-extent.utils'; import {MatDatepicker} from '@angular/material/datepicker'; -import {TIME_SERVICE} from '../../../app.module'; -import {TimeService} from '../../interfaces/time.service'; -import {DayjsTimeService} from '../../../shared/services/dayjs-time.service'; +import {DayjsUtils} from '../../../shared/utils/dayjs.utils'; // There is an array (`allowedDatePickerManipulationUnits`) and a new union type (`DatePickerManipulationUnits`) for two reasons: // To be able to extract a union type subset of `ManipulateType` AND to have an array used to check if a given value is in said union type. @@ -47,10 +45,7 @@ export class TimeSliderComponent implements OnInit, OnChanges { public datePickerStartView: DatePickerStartView = 'month'; private datePickerUnit: DatePickerManipulationUnits = 'days'; - constructor( - private readonly timeSliderService: TimeSliderService, - @Inject(TIME_SERVICE) private readonly timeService: TimeService, - ) {} + constructor(private readonly timeSliderService: TimeSliderService) {} public ngOnInit() { this.availableDates = this.timeSliderService.createStops(this.timeSliderConfiguration); @@ -150,8 +145,8 @@ export class TimeSliderComponent implements OnInit, OnChanges { } // format the given event date to the configured time format and back to ensure that it is a valid date within the current available dates - const date = this.timeService.getDate( - this.timeService.getDateAsString(eventDate, this.timeSliderConfiguration.dateFormat), + const date = DayjsUtils.getDate( + DayjsUtils.getDateAsString(eventDate, this.timeSliderConfiguration.dateFormat), this.timeSliderConfiguration.dateFormat, ); const position = this.findPositionOfDate(date); @@ -177,7 +172,7 @@ export class TimeSliderComponent implements OnInit, OnChanges { */ private isRangeExactlyOneOfSingleTimeUnit(range: string | null | undefined): boolean { if (range) { - const rangeDuration = DayjsTimeService.getDuration(range); + const rangeDuration = DayjsUtils.getDuration(range); const unit = TimeExtentUtils.extractUniqueUnitFromDuration(rangeDuration); return unit !== undefined && TimeExtentUtils.getDurationAsNumber(rangeDuration, unit) === 1; } @@ -217,7 +212,7 @@ export class TimeSliderComponent implements OnInit, OnChanges { private isLayerSourceContinuous(layerSource: TimeSliderLayerSource, unit: DatePickerManipulationUnits): boolean { const dateAsAscendingSortedNumbers = layerSource.layers - .map((layer) => this.timeService.getPartial(layer.date, unit as UnitType)) + .map((layer) => DayjsUtils.getPartial(layer.date, unit as UnitType)) .sort((a, b) => a - b); // all date numbers must be part of a continuous and strictly monotonously rising series each with exactly // one step between them: `date[0] = x` => `date[n] = x + n` diff --git a/src/app/map/interfaces/time.service.ts b/src/app/map/interfaces/time.service.ts deleted file mode 100644 index f5906b396..000000000 --- a/src/app/map/interfaces/time.service.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {UnitType} from 'dayjs'; - -export interface TimeService { - getPartial(date: string, unit: UnitType): number; - - getDateAsString(date: Date, format: string): string; - - getDate(date: string, format: string): Date; - - getUTCDateAsString(date: Date, format?: string): string; - - getUnixDate(created: number): Date; -} diff --git a/src/app/map/pipes/date-to-string.pipe.spec.ts b/src/app/map/pipes/date-to-string.pipe.spec.ts index 5f656aa1b..833af0a14 100644 --- a/src/app/map/pipes/date-to-string.pipe.spec.ts +++ b/src/app/map/pipes/date-to-string.pipe.spec.ts @@ -1,17 +1,12 @@ import {DateToStringPipe} from './date-to-string.pipe'; -import {TIME_SERVICE} from '../../app.module'; -import {TimeService} from '../interfaces/time.service'; import {TestBed} from '@angular/core/testing'; -import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; describe('DateToStringPipe', () => { let pipe: DateToStringPipe; - let timeService: TimeService; beforeEach(() => { - TestBed.configureTestingModule({providers: [{provide: TIME_SERVICE, useClass: DayjsTimeService}]}); - timeService = TestBed.inject(TIME_SERVICE); - pipe = new DateToStringPipe(timeService); + TestBed.configureTestingModule({}); + pipe = new DateToStringPipe(); }); it('create an instance', () => { diff --git a/src/app/map/pipes/date-to-string.pipe.ts b/src/app/map/pipes/date-to-string.pipe.ts index e8f94544c..2dd04372a 100644 --- a/src/app/map/pipes/date-to-string.pipe.ts +++ b/src/app/map/pipes/date-to-string.pipe.ts @@ -1,14 +1,13 @@ -import {Inject, Pipe, PipeTransform} from '@angular/core'; -import {TIME_SERVICE} from '../../app.module'; -import {TimeService} from '../interfaces/time.service'; +import {Pipe, PipeTransform} from '@angular/core'; +import {DayjsUtils} from '../../shared/utils/dayjs.utils'; @Pipe({ name: 'dateToString', standalone: true, }) export class DateToStringPipe implements PipeTransform { - constructor(@Inject(TIME_SERVICE) private readonly timeService: TimeService) {} + constructor() {} public transform(value: Date | undefined, dateFormat: string): string { - return value ? this.timeService.getDateAsString(value, dateFormat) : ''; + return value ? DayjsUtils.getDateAsString(value, dateFormat) : ''; } } diff --git a/src/app/map/pipes/time-extent-to-string.pipe.spec.ts b/src/app/map/pipes/time-extent-to-string.pipe.spec.ts index 0d399187a..11adc8643 100644 --- a/src/app/map/pipes/time-extent-to-string.pipe.spec.ts +++ b/src/app/map/pipes/time-extent-to-string.pipe.spec.ts @@ -1,18 +1,13 @@ import {TimeExtentToStringPipe} from './time-extent-to-string.pipe'; import {TimeExtent} from '../interfaces/time-extent.interface'; import {TestBed} from '@angular/core/testing'; -import {TIME_SERVICE} from '../../app.module'; -import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; -import {TimeService} from '../interfaces/time.service'; describe('TimeExtentToStringPipe', () => { let pipe: TimeExtentToStringPipe; - let timeService: TimeService; beforeEach(() => { - TestBed.configureTestingModule({providers: [{provide: TIME_SERVICE, useClass: DayjsTimeService}]}); - timeService = TestBed.inject(TIME_SERVICE); - pipe = new TimeExtentToStringPipe(timeService); + TestBed.configureTestingModule({}); + pipe = new TimeExtentToStringPipe(); }); it('create an instance', () => { expect(pipe).toBeTruthy(); diff --git a/src/app/map/pipes/time-extent-to-string.pipe.ts b/src/app/map/pipes/time-extent-to-string.pipe.ts index 346ebddb7..fe2067420 100644 --- a/src/app/map/pipes/time-extent-to-string.pipe.ts +++ b/src/app/map/pipes/time-extent-to-string.pipe.ts @@ -1,14 +1,13 @@ -import {Inject, Pipe, PipeTransform} from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import {TimeExtent} from '../interfaces/time-extent.interface'; -import {TIME_SERVICE} from '../../app.module'; -import {TimeService} from '../interfaces/time.service'; +import {DayjsUtils} from '../../shared/utils/dayjs.utils'; @Pipe({ name: 'timeExtentToString', standalone: true, }) export class TimeExtentToStringPipe implements PipeTransform { - constructor(@Inject(TIME_SERVICE) private readonly timeService: TimeService) {} + constructor() {} public transform(timeExtent: TimeExtent | undefined, dateFormat: string, hasSimpleCurrentValue: boolean): string { if (!timeExtent) { return ''; @@ -19,6 +18,6 @@ export class TimeExtentToStringPipe implements PipeTransform { } private convertDateToString(value: Date, dateFormat: string): string { - return value ? this.timeService.getDateAsString(value, dateFormat) : ''; + return value ? DayjsUtils.getDateAsString(value, dateFormat) : ''; } } diff --git a/src/app/map/services/esri-services/esri-map.service.spec.ts b/src/app/map/services/esri-services/esri-map.service.spec.ts index 228c9fe34..ef142c0a5 100644 --- a/src/app/map/services/esri-services/esri-map.service.spec.ts +++ b/src/app/map/services/esri-services/esri-map.service.spec.ts @@ -12,8 +12,6 @@ import {EsriMapViewService} from './esri-map-view.service'; import {EsriToolService} from './tool-service/esri-tool.service'; import {createDrawingMapItemMock, createGb2WmsMapItemMock} from '../../../testing/map-testing/active-map-item-test.utils'; import {FilterConfiguration} from '../../../shared/interfaces/topic.interface'; -import {TIME_SERVICE} from '../../../app.module'; -import {DayjsTimeService} from '../../../shared/services/dayjs-time.service'; function compareMapItemToEsriLayer(expectedMapItem: Gb2WmsActiveMapItem, actualEsriLayer: __esri.Layer) { expect(actualEsriLayer.id).toBe(expectedMapItem.id); @@ -76,7 +74,6 @@ describe('EsriMapService', () => { provide: EsriToolService, useValue: toolServiceSpy, }, - {provide: TIME_SERVICE, useClass: DayjsTimeService}, ], }); service = TestBed.inject(EsriMapService); diff --git a/src/app/map/services/esri-services/esri-map.service.ts b/src/app/map/services/esri-services/esri-map.service.ts index ee28527fe..83d08fb59 100644 --- a/src/app/map/services/esri-services/esri-map.service.ts +++ b/src/app/map/services/esri-services/esri-map.service.ts @@ -1,4 +1,4 @@ -import {Inject, Injectable, OnDestroy} from '@angular/core'; +import {Injectable, OnDestroy} from '@angular/core'; import esriConfig from '@arcgis/core/config'; import * as geometryEngine from '@arcgis/core/geometry/geometryEngine'; import GraphicsLayer from '@arcgis/core/layers/GraphicsLayer'; @@ -64,8 +64,7 @@ import {InitialMapExtentService} from '../initial-map-extent.service'; import {MapConstants} from '../../../shared/constants/map.constants'; import {HitTestSelectionUtils} from './utils/hit-test-selection.utils'; import * as intl from '@arcgis/core/intl'; -import {TIME_SERVICE} from '../../../app.module'; -import {TimeService} from '../../interfaces/time.service'; +import {DayjsUtils} from '../../../shared/utils/dayjs.utils'; import GraphicHit = __esri.GraphicHit; const DEFAULT_POINT_ZOOM_EXTENT_SCALE = 750; @@ -107,7 +106,6 @@ export class EsriMapService implements MapService, OnDestroy { private readonly esriToolService: EsriToolService, private readonly gb3TopicsService: Gb3TopicsService, private readonly initialMapExtentService: InitialMapExtentService, - @Inject(TIME_SERVICE) private readonly timeService: TimeService, ) { /** * Because the GetCapabalities response often sends a non-secure http://wms.zh.ch response, Esri Javascript API fails on https @@ -637,11 +635,11 @@ export class EsriMapService implements MapService, OnDestroy { const dateFormat = timeSliderConfiguration.dateFormat; esriLayer.customLayerParameters = esriLayer.customLayerParameters ?? {}; - esriLayer.customLayerParameters[timeSliderParameterSource.startRangeParameter] = this.timeService.getUTCDateAsString( + esriLayer.customLayerParameters[timeSliderParameterSource.startRangeParameter] = DayjsUtils.getUTCDateAsString( timeSliderExtent.start, dateFormat, ); - esriLayer.customLayerParameters[timeSliderParameterSource.endRangeParameter] = this.timeService.getUTCDateAsString( + esriLayer.customLayerParameters[timeSliderParameterSource.endRangeParameter] = DayjsUtils.getUTCDateAsString( timeSliderExtent.end, dateFormat, ); diff --git a/src/app/map/services/favourites.service.spec.ts b/src/app/map/services/favourites.service.spec.ts index daf78d12a..bcade2d0f 100644 --- a/src/app/map/services/favourites.service.spec.ts +++ b/src/app/map/services/favourites.service.spec.ts @@ -22,8 +22,7 @@ import {SymbolizationToGb3ConverterUtils} from '../../shared/utils/symbolization import {Map} from '../../shared/interfaces/topic.interface'; import {TimeExtentUtils} from '../../shared/utils/time-extent.utils'; import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; -import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; -import {TIME_SERVICE} from '../../app.module'; +import {DayjsUtils} from '../../shared/utils/dayjs.utils'; describe('FavouritesService', () => { let service: FavouritesService; @@ -33,12 +32,7 @@ describe('FavouritesService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [ - provideMockStore({}), - provideHttpClient(withInterceptorsFromDi()), - provideHttpClientTesting(), - {provide: TIME_SERVICE, useClass: DayjsTimeService}, - ], + providers: [provideMockStore({}), provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], }); store = TestBed.inject(MockStore); store.overrideSelector(selectActiveMapItemConfigurations, []); @@ -391,8 +385,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, { @@ -536,8 +530,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -654,8 +648,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -715,8 +709,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -805,8 +799,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -864,8 +858,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1042,8 +1036,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1200,8 +1194,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1357,8 +1351,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('0999-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('0999-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1512,8 +1506,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1450-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('1455-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1450-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('1455-01-01T00:00:00.000Z'), }, }, ]; @@ -1667,8 +1661,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1750-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('1455-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1750-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('1455-01-01T00:00:00.000Z'), }, }, ]; @@ -1822,8 +1816,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1250-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2000-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1250-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2000-01-01T00:00:00.000Z'), }, }, ]; @@ -2088,8 +2082,8 @@ describe('FavouritesService', () => { isSingleLayer: false, attributeFilters: undefined, timeExtent: { - start: DayjsTimeService.parseUTCDate('2016-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2017-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('2016-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2017-01-01T00:00:00.000Z'), }, }, ]; @@ -2354,8 +2348,8 @@ describe('FavouritesService', () => { isSingleLayer: false, attributeFilters: undefined, timeExtent: { - start: DayjsTimeService.parseUTCDate('2016-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2017-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('2016-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2017-01-01T00:00:00.000Z'), }, }, ]; diff --git a/src/app/map/services/favourites.service.ts b/src/app/map/services/favourites.service.ts index 0eaf345f1..b96fe4cb1 100644 --- a/src/app/map/services/favourites.service.ts +++ b/src/app/map/services/favourites.service.ts @@ -29,7 +29,7 @@ import {Gb3StyledInternalDrawingRepresentation} from '../../shared/interfaces/in import {TimeExtent} from '../interfaces/time-extent.interface'; import {TimeExtentUtils} from '../../shared/utils/time-extent.utils'; import {TimeSliderService} from './time-slider.service'; -import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; +import {DayjsUtils} from '../../shared/utils/dayjs.utils'; @Injectable({ providedIn: 'root', @@ -352,7 +352,7 @@ export class FavouritesService implements OnDestroy { return isTimeExtentValid; case 'layer': { const selectedYearExists = (timeSliderConfiguration.source as TimeSliderLayerSource).layers.some( - (layer) => DayjsTimeService.parseUTCDate(layer.date, timeSliderConfiguration.dateFormat).getTime() === timeExtent.start.getTime(), + (layer) => DayjsUtils.parseUTCDate(layer.date, timeSliderConfiguration.dateFormat).getTime() === timeExtent.start.getTime(), ); return selectedYearExists && isTimeExtentValid; } diff --git a/src/app/map/services/time-slider.service.spec.ts b/src/app/map/services/time-slider.service.spec.ts index 8a167a710..de2ddbaa9 100644 --- a/src/app/map/services/time-slider.service.spec.ts +++ b/src/app/map/services/time-slider.service.spec.ts @@ -3,14 +3,12 @@ import {TimeSliderService} from './time-slider.service'; import dayjs from 'dayjs'; import {TimeSliderConfiguration, TimeSliderParameterSource} from '../../shared/interfaces/topic.interface'; import {TimeExtent} from '../interfaces/time-extent.interface'; -import {TIME_SERVICE} from '../../app.module'; -import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; describe('TimeSliderService', () => { let service: TimeSliderService; beforeEach(() => { - TestBed.configureTestingModule({providers: [{provide: TIME_SERVICE, useClass: DayjsTimeService}]}); + TestBed.configureTestingModule({}); service = TestBed.inject(TimeSliderService); }); diff --git a/src/app/map/services/time-slider.service.ts b/src/app/map/services/time-slider.service.ts index 1d0696989..8b6288d95 100644 --- a/src/app/map/services/time-slider.service.ts +++ b/src/app/map/services/time-slider.service.ts @@ -1,19 +1,15 @@ -import {Inject, Injectable} from '@angular/core'; +import {Injectable} from '@angular/core'; import {TimeSliderConfiguration, TimeSliderLayerSource} from '../../shared/interfaces/topic.interface'; import {Duration} from 'dayjs/plugin/duration'; import {TimeExtentUtils} from '../../shared/utils/time-extent.utils'; import {TimeExtent} from '../interfaces/time-extent.interface'; - import {InvalidTimeSliderConfiguration} from '../../shared/errors/map.errors'; -import {TIME_SERVICE} from '../../app.module'; -import {TimeService} from '../interfaces/time.service'; -import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; +import {DayjsUtils} from '../../shared/utils/dayjs.utils'; @Injectable({ providedIn: 'root', }) export class TimeSliderService { - constructor(@Inject(TIME_SERVICE) private readonly timeService: TimeService) {} /** * Creates stops which define specific locations on the time slider where thumbs will snap to when manipulated. */ @@ -52,7 +48,7 @@ export class TimeSliderService { The start has changed as fixed ranges technically don't have an end date => the end date has to be adjusted accordingly to enforce the fixed range between start and end date */ - const range: Duration = DayjsTimeService.getDuration(timeSliderConfig.range); + const range: Duration = DayjsUtils.getDuration(timeSliderConfig.range); timeExtent.end = TimeExtentUtils.addDuration(timeExtent.start, range); } else if (timeSliderConfig.minimalRange) { /* @@ -75,7 +71,7 @@ export class TimeSliderService { } const startEndDiff: number = TimeExtentUtils.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end); - const minimalRange: Duration = DayjsTimeService.getDuration(timeSliderConfig.minimalRange); + const minimalRange: Duration = DayjsUtils.getDuration(timeSliderConfig.minimalRange); if (startEndDiff < minimalRange.asMilliseconds()) { if (hasStartDateChanged) { @@ -98,8 +94,8 @@ export class TimeSliderService { } public isTimeExtentValid(timeSliderConfig: TimeSliderConfiguration, timeExtent: TimeExtent): boolean { - const minDate = DayjsTimeService.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maxDate = DayjsTimeService.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const minDate = DayjsUtils.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maxDate = DayjsUtils.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); const updatedTimeExtent: TimeExtent = this.createValidTimeExtent(timeSliderConfig, timeExtent, false, minDate, maxDate); @@ -111,7 +107,7 @@ export class TimeSliderService { */ private createStopsForLayerSource(timeSliderConfig: TimeSliderConfiguration): Array { const timeSliderLayerSource = timeSliderConfig.source as TimeSliderLayerSource; - return timeSliderLayerSource.layers.map((layer) => DayjsTimeService.parseUTCDate(layer.date, timeSliderConfig.dateFormat)); + return timeSliderLayerSource.layers.map((layer) => DayjsUtils.parseUTCDate(layer.date, timeSliderConfig.dateFormat)); } /** @@ -123,10 +119,10 @@ export class TimeSliderService { * start to finish using the given duration; this can lead to gaps near the end but supports all cases. */ private createStopsForParameterSource(timeSliderConfig: TimeSliderConfiguration): Array { - const minimumDate: Date = DayjsTimeService.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maximumDate: Date = DayjsTimeService.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const minimumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maximumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); const initialRange: string | null = timeSliderConfig.range ?? timeSliderConfig.minimalRange ?? null; - let stopRangeDuration: Duration | null = initialRange ? DayjsTimeService.getDuration(initialRange) : null; + let stopRangeDuration: Duration | null = initialRange ? DayjsUtils.getDuration(initialRange) : null; if ( stopRangeDuration && TimeExtentUtils.calculateDifferenceBetweenDates(minimumDate, maximumDate) <= stopRangeDuration.asMilliseconds() @@ -140,7 +136,7 @@ export class TimeSliderService { } // create a new duration base on the smallest unit with the lowest valid unit number (1) - stopRangeDuration = DayjsTimeService.getDurationWithUnit(1, unit); + stopRangeDuration = DayjsUtils.getDurationWithUnit(1, unit); } const dates: Date[] = []; diff --git a/src/app/map/utils/active-time-slider-layers.utils.ts b/src/app/map/utils/active-time-slider-layers.utils.ts index 3dd8b6f3e..7df820f4a 100644 --- a/src/app/map/utils/active-time-slider-layers.utils.ts +++ b/src/app/map/utils/active-time-slider-layers.utils.ts @@ -1,6 +1,6 @@ import {MapLayer, TimeSliderConfiguration, TimeSliderLayerSource} from '../../shared/interfaces/topic.interface'; import {TimeExtent} from '../interfaces/time-extent.interface'; -import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; +import {DayjsUtils} from '../../shared/utils/dayjs.utils'; export class ActiveTimeSliderLayersUtils { /** @@ -19,7 +19,7 @@ export class ActiveTimeSliderLayersUtils { const timeSliderLayerSource = timeSliderConfiguration.source as TimeSliderLayerSource; const timeSliderLayer = timeSliderLayerSource.layers.find((layer) => layer.layerName === mapLayer.layer); if (timeSliderLayer) { - const date = DayjsTimeService.parseUTCDate(timeSliderLayer.date, timeSliderConfiguration.dateFormat); + const date = DayjsUtils.parseUTCDate(timeSliderLayer.date, timeSliderConfiguration.dateFormat); return date >= timeExtent.start && date < timeExtent.end; } else { return undefined; diff --git a/src/app/shared/services/apis/gb3/gb3-favourites.service.ts b/src/app/shared/services/apis/gb3/gb3-favourites.service.ts index 27d33712b..ec1d7ebff 100644 --- a/src/app/shared/services/apis/gb3/gb3-favourites.service.ts +++ b/src/app/shared/services/apis/gb3/gb3-favourites.service.ts @@ -10,7 +10,7 @@ import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {CreateFavourite, Favourite, FavouritesResponse} from '../../../interfaces/favourite.interface'; import {ApiGeojsonGeometryToGb3ConverterUtils} from '../../../utils/api-geojson-geometry-to-gb3-converter.utils'; -import {DayjsTimeService} from '../../dayjs-time.service'; +import {DayjsUtils} from '../../../utils/dayjs.utils'; @Injectable({ providedIn: 'root', @@ -67,8 +67,8 @@ export class Gb3FavouritesService extends Gb3ApiService { attributeFilters: content.attributeFilters, timeExtent: content.timeExtent ? { - start: DayjsTimeService.parseUTCDate(content.timeExtent.start), - end: DayjsTimeService.parseUTCDate(content.timeExtent.end), + start: DayjsUtils.parseUTCDate(content.timeExtent.start), + end: DayjsUtils.parseUTCDate(content.timeExtent.end), } : undefined, }; diff --git a/src/app/shared/services/apis/gb3/gb3-share-link.service.spec.ts b/src/app/shared/services/apis/gb3/gb3-share-link.service.spec.ts index 752a70411..846cff574 100644 --- a/src/app/shared/services/apis/gb3/gb3-share-link.service.spec.ts +++ b/src/app/shared/services/apis/gb3/gb3-share-link.service.spec.ts @@ -12,15 +12,11 @@ import {selectMaps} from '../../../../state/map/selectors/maps.selector'; import {selectActiveMapItemConfigurations} from '../../../../state/map/selectors/active-map-item-configuration.selector'; import {selectFavouriteBaseConfig} from '../../../../state/map/selectors/favourite-base-config.selector'; import {selectUserDrawingsVectorLayers} from '../../../../state/map/selectors/user-drawings-vector-layers.selector'; -import {DayjsTimeService} from '../../dayjs-time.service'; -import {TimeService} from '../../../../map/interfaces/time.service'; -import {TIME_SERVICE} from '../../../../app.module'; // todo: add tests for vector layers const mockedVectorLayer = {type: undefined, styles: undefined, geojson: {type: undefined, features: []}} as unknown as Gb3VectorLayer; describe('Gb3ShareLinkService', () => { let service: Gb3ShareLinkService; - let timeService: TimeService; let store: MockStore; const shareLinkItemIdMock = 'mock-id'; const serverDataMock: SharedFavorite = { @@ -108,12 +104,7 @@ describe('Gb3ShareLinkService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [ - provideMockStore({}), - provideHttpClient(withInterceptorsFromDi()), - provideHttpClientTesting(), - {provide: TIME_SERVICE, useClass: DayjsTimeService}, - ], + providers: [provideMockStore({}), provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], }); store = TestBed.inject(MockStore); store.overrideSelector(selectActiveMapItemConfigurations, []); diff --git a/src/app/shared/services/apis/gb3/gb3-share-link.service.ts b/src/app/shared/services/apis/gb3/gb3-share-link.service.ts index f9caa8551..234d7a0ca 100644 --- a/src/app/shared/services/apis/gb3/gb3-share-link.service.ts +++ b/src/app/shared/services/apis/gb3/gb3-share-link.service.ts @@ -11,7 +11,7 @@ import {HttpClient} from '@angular/common/http'; import {BasemapConfigService} from '../../../../map/services/basemap-config.service'; import {FavouritesService} from '../../../../map/services/favourites.service'; import {MapRestoreItem} from '../../../interfaces/map-restore-item.interface'; -import {DayjsTimeService} from '../../dayjs-time.service'; +import {DayjsUtils} from '../../../utils/dayjs.utils'; @Injectable({ providedIn: 'root', @@ -93,8 +93,8 @@ export class Gb3ShareLinkService extends Gb3ApiService { attributeFilters: content.attributeFilters, timeExtent: content.timeExtent ? { - start: DayjsTimeService.parseUTCDate(content.timeExtent.start), - end: DayjsTimeService.parseUTCDate(content.timeExtent.end), + start: DayjsUtils.parseUTCDate(content.timeExtent.start), + end: DayjsUtils.parseUTCDate(content.timeExtent.end), } : undefined, }; diff --git a/src/app/shared/services/apis/grav-cms/grav-cms.service.spec.ts b/src/app/shared/services/apis/grav-cms/grav-cms.service.spec.ts index 064f68db8..52da2e359 100644 --- a/src/app/shared/services/apis/grav-cms/grav-cms.service.spec.ts +++ b/src/app/shared/services/apis/grav-cms/grav-cms.service.spec.ts @@ -10,8 +10,6 @@ import {PageNotification} from '../../../interfaces/page-notification.interface' import {MainPage} from '../../../enums/main-page.enum'; import {FrequentlyUsedItem} from '../../../interfaces/frequently-used-item.interface'; import {provideMockStore} from '@ngrx/store/testing'; -import {TIME_SERVICE} from '../../../../app.module'; -import {DayjsTimeService} from '../../dayjs-time.service'; describe('GravCmsService', () => { let service: GravCmsService; @@ -20,12 +18,7 @@ describe('GravCmsService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [ - provideMockStore(), - provideHttpClient(withInterceptorsFromDi()), - provideHttpClientTesting(), - {provide: TIME_SERVICE, useClass: DayjsTimeService}, - ], + providers: [provideMockStore(), provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], }); service = TestBed.inject(GravCmsService); configService = TestBed.inject(ConfigService); diff --git a/src/app/shared/services/apis/grav-cms/grav-cms.service.ts b/src/app/shared/services/apis/grav-cms/grav-cms.service.ts index bcba166c0..f3310c53e 100644 --- a/src/app/shared/services/apis/grav-cms/grav-cms.service.ts +++ b/src/app/shared/services/apis/grav-cms/grav-cms.service.ts @@ -1,4 +1,4 @@ -import {Inject, Injectable} from '@angular/core'; +import {Injectable} from '@angular/core'; import {BaseApiService} from '../abstract-api.service'; import {Observable} from 'rxjs'; import {DiscoverMapsItem} from '../../../interfaces/discover-maps-item.interface'; @@ -7,10 +7,9 @@ import {DiscoverMapsRoot, FrequentlyUsedRoot, PageInfosRoot, Pages} from '../../ import {PageNotification, PageNotificationSeverity} from '../../../interfaces/page-notification.interface'; import {MainPage} from '../../../enums/main-page.enum'; import {FrequentlyUsedItem} from '../../../interfaces/frequently-used-item.interface'; -import {TIME_SERVICE} from '../../../../app.module'; -import {TimeService} from '../../../../map/interfaces/time.service'; import {HttpClient} from '@angular/common/http'; import {ConfigService} from '../../config.service'; +import {DayjsUtils} from '../../../utils/dayjs.utils'; const DATE_FORMAT = 'DD.MM.YYYY'; @@ -23,11 +22,7 @@ export class GravCmsService extends BaseApiService { private readonly pageInfosEndpoint: string = 'pageinfos.json'; private readonly frequentlyUsedItemsEndpoint: string = 'frequentlyused.json'; - constructor( - @Inject(TIME_SERVICE) private readonly timeService: TimeService, - httpClient: HttpClient, - configService: ConfigService, - ) { + constructor(httpClient: HttpClient, configService: ConfigService) { super(httpClient, configService); } public loadDiscoverMapsData(): Observable { @@ -52,8 +47,8 @@ export class GravCmsService extends BaseApiService { title: discoverMapData.title, description: discoverMapData.description, mapId: discoverMapData.id, - fromDate: this.timeService.getDate(discoverMapData.from_date, DATE_FORMAT), - toDate: this.timeService.getDate(discoverMapData.to_date, DATE_FORMAT), + fromDate: DayjsUtils.getDate(discoverMapData.from_date, DATE_FORMAT), + toDate: DayjsUtils.getDate(discoverMapData.to_date, DATE_FORMAT), image: { url: this.createFullImageUrl(discoverMapData.image.path), name: discoverMapData.image.name, @@ -73,8 +68,8 @@ export class GravCmsService extends BaseApiService { title: pageInfoData.title, description: pageInfoData.description, pages: this.transformPagesToMainPages(pageInfoData.pages), - fromDate: this.timeService.getDate(pageInfoData.from_date, DATE_FORMAT), - toDate: this.timeService.getDate(pageInfoData.to_date, DATE_FORMAT), + fromDate: DayjsUtils.getDate(pageInfoData.from_date, DATE_FORMAT), + toDate: DayjsUtils.getDate(pageInfoData.to_date, DATE_FORMAT), severity: pageInfoData.severity as PageNotificationSeverity, isMarkedAsRead: false, }; @@ -98,7 +93,7 @@ export class GravCmsService extends BaseApiService { altText: frequentlyUsedData.image_alt, } : undefined, - created: this.timeService.getUnixDate(+frequentlyUsedData.created), + created: DayjsUtils.getUnixDate(+frequentlyUsedData.created), }; }); } diff --git a/src/app/shared/services/dayjs-time.service.ts b/src/app/shared/utils/dayjs.utils.ts similarity index 74% rename from src/app/shared/services/dayjs-time.service.ts rename to src/app/shared/utils/dayjs.utils.ts index 65c126f94..84fd92e9e 100644 --- a/src/app/shared/services/dayjs-time.service.ts +++ b/src/app/shared/utils/dayjs.utils.ts @@ -1,7 +1,5 @@ -import {TimeService} from '../../map/interfaces/time.service'; import dayjs, {ManipulateType, UnitType} from 'dayjs'; import duration, {Duration} from 'dayjs/plugin/duration'; -import {Injectable} from '@angular/core'; import utc from 'dayjs/plugin/utc'; import customParseFormat from 'dayjs/plugin/customParseFormat'; @@ -9,23 +7,20 @@ dayjs.extend(duration); dayjs.extend(customParseFormat); dayjs.extend(utc); -@Injectable({ - providedIn: 'root', -}) -export class DayjsTimeService implements TimeService { - public getPartial(date: string, unit: UnitType): number { +export class DayjsUtils { + public static getPartial(date: string, unit: UnitType): number { return dayjs(date, unit).get(unit); } - public getDateAsString(date: Date, format: string): string { + public static getDateAsString(date: Date, format: string): string { return dayjs(date).format(format); } - public getDate(date: string, format: string): Date { + public static getDate(date: string, format: string): Date { return dayjs(date, format).toDate(); } - public getUTCDateAsString(date: Date, format?: string): string { + public static getUTCDateAsString(date: Date, format?: string): string { return dayjs.utc(date).format(format); } - public getUnixDate(created: number): Date { + public static getUnixDate(created: number): Date { return dayjs.unix(created).toDate(); } public static parseUTCDate(date: string, format?: string): Date { diff --git a/src/app/shared/utils/storage.utils.spec.ts b/src/app/shared/utils/storage.utils.spec.ts index b2f23b7ad..95f00e943 100644 --- a/src/app/shared/utils/storage.utils.spec.ts +++ b/src/app/shared/utils/storage.utils.spec.ts @@ -1,5 +1,5 @@ import {StorageUtils} from './storage.utils'; -import {DayjsTimeService} from '../services/dayjs-time.service'; +import {DayjsUtils} from './dayjs.utils'; describe('StorageUtils', () => { describe('parseJson', () => { @@ -8,7 +8,7 @@ describe('StorageUtils', () => { '{"date":"1506-01-01T00:00:00.000Z", "number": 2683132, "string": "test", "stringifiedNumberParseableAsDate": "12"}'; const expectedJsonObject = { - date: DayjsTimeService.parseUTCDate('1506-01-01T00:00:00.000Z'), + date: DayjsUtils.parseUTCDate('1506-01-01T00:00:00.000Z'), number: 2683132, string: 'test', stringifiedNumberParseableAsDate: '12', diff --git a/src/app/shared/utils/storage.utils.ts b/src/app/shared/utils/storage.utils.ts index 1d5b8fe02..a49687a92 100644 --- a/src/app/shared/utils/storage.utils.ts +++ b/src/app/shared/utils/storage.utils.ts @@ -1,4 +1,4 @@ -import {DayjsTimeService} from '../services/dayjs-time.service'; +import {DayjsUtils} from './dayjs.utils'; export class StorageUtils { /** @@ -16,8 +16,8 @@ export class StorageUtils { * the original string. If it is, we return the parsed date, otherwise the original string (see GB3-1597). */ private static reviver(key: string, value: any): any { - if (typeof value === 'string' && DayjsTimeService.isValidDate(value)) { - const parsed = DayjsTimeService.parseUTCDate(value); + if (typeof value === 'string' && DayjsUtils.isValidDate(value)) { + const parsed = DayjsUtils.parseUTCDate(value); return parsed.toISOString() === value ? parsed : value; } return value; diff --git a/src/app/shared/utils/time-extent.utils.ts b/src/app/shared/utils/time-extent.utils.ts index f178f7024..809efcb76 100644 --- a/src/app/shared/utils/time-extent.utils.ts +++ b/src/app/shared/utils/time-extent.utils.ts @@ -2,16 +2,16 @@ import {Duration} from 'dayjs/plugin/duration'; import {ManipulateType} from 'dayjs'; import {TimeSliderConfiguration} from '../interfaces/topic.interface'; import {TimeExtent} from '../../map/interfaces/time-extent.interface'; -import {DayjsTimeService} from '../services/dayjs-time.service'; +import {DayjsUtils} from './dayjs.utils'; export class TimeExtentUtils { /** * Creates an initial time extent based on the given time slider configuration. */ public static createInitialTimeSliderExtent(timeSliderConfig: TimeSliderConfiguration): TimeExtent { - const minimumDate: Date = DayjsTimeService.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maximumDate: Date = DayjsTimeService.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); - const range: Duration | null = timeSliderConfig.range ? DayjsTimeService.getDuration(timeSliderConfig.range) : null; + const minimumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maximumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const range: Duration | null = timeSliderConfig.range ? DayjsUtils.getDuration(timeSliderConfig.range) : null; return { start: minimumDate, end: range ? TimeExtentUtils.addDuration(minimumDate, range) : maximumDate, @@ -100,10 +100,10 @@ export class TimeExtentUtils { public static addDuration(date: Date, duration: Duration): Date { const unit = TimeExtentUtils.extractUniqueUnitFromDuration(duration); if (!unit) { - return DayjsTimeService.addDuration(date, duration); + return DayjsUtils.addDuration(date, duration); } const value = TimeExtentUtils.getDurationAsNumber(duration, unit); - return DayjsTimeService.addDuration(date, DayjsTimeService.getDurationWithUnit(value, unit)); + return DayjsUtils.addDuration(date, DayjsUtils.getDurationWithUnit(value, unit)); } /** @@ -122,10 +122,10 @@ export class TimeExtentUtils { public static subtractDuration(date: Date, duration: Duration): Date { const unit = TimeExtentUtils.extractUniqueUnitFromDuration(duration); if (!unit) { - return DayjsTimeService.subtractDuration(date, duration); + return DayjsUtils.subtractDuration(date, duration); } const value = TimeExtentUtils.getDurationAsNumber(duration, unit); - return DayjsTimeService.subtractDuration(date, DayjsTimeService.getDurationWithUnit(value, unit)); + return DayjsUtils.subtractDuration(date, DayjsUtils.getDurationWithUnit(value, unit)); } /** @@ -173,6 +173,6 @@ export class TimeExtentUtils { * Returns the difference in milliseconds between the two given dates. */ public static calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number { - return DayjsTimeService.calculateDifferenceBetweenDates(firstDate, secondDate); + return DayjsUtils.calculateDifferenceBetweenDates(firstDate, secondDate); } } diff --git a/src/app/state/auth/effects/auth-status.effects.spec.ts b/src/app/state/auth/effects/auth-status.effects.spec.ts index 1bc657d3d..a2478b255 100644 --- a/src/app/state/auth/effects/auth-status.effects.spec.ts +++ b/src/app/state/auth/effects/auth-status.effects.spec.ts @@ -15,7 +15,7 @@ import {selectActiveMapItemConfigurations} from '../../map/selectors/active-map- import {selectMaps} from '../../map/selectors/maps.selector'; import {selectFavouriteBaseConfig} from '../../map/selectors/favourite-base-config.selector'; import {selectUserDrawingsVectorLayers} from '../../map/selectors/user-drawings-vector-layers.selector'; -import {MAP_SERVICE, TIME_SERVICE} from '../../../app.module'; +import {MAP_SERVICE} from '../../../app.module'; import {MapServiceStub} from '../../../testing/map-testing/map.service.stub'; import {LayerCatalogActions} from '../../map/actions/layer-catalog.actions'; import {Gb3ShareLinkService} from '../../../shared/services/apis/gb3/gb3-share-link.service'; @@ -29,7 +29,6 @@ import {selectItems} from '../../map/selectors/active-map-items.selector'; import {selectDrawings} from '../../map/reducers/drawing.reducer'; import {ToolService} from '../../../map/interfaces/tool.service'; import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; -import {DayjsTimeService} from '../../../shared/services/dayjs-time.service'; const mockOAuthService = jasmine.createSpyObj({ logout: void 0, @@ -53,7 +52,6 @@ describe('AuthStatusEffects', () => { {provide: AuthService, useValue: mockOAuthService}, AuthStatusEffects, {provide: MAP_SERVICE, useClass: MapServiceStub}, - {provide: TIME_SERVICE, useClass: DayjsTimeService}, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), ], diff --git a/src/app/state/map/effects/share-link.effects.spec.ts b/src/app/state/map/effects/share-link.effects.spec.ts index d7ea7c3d9..2b095e4ce 100644 --- a/src/app/state/map/effects/share-link.effects.spec.ts +++ b/src/app/state/map/effects/share-link.effects.spec.ts @@ -35,7 +35,7 @@ import {ActiveMapItemFactory} from '../../../shared/factories/active-map-item.fa import {selectIsAuthenticated, selectIsAuthenticationInitialized} from '../../auth/reducers/auth-status.reducer'; import {MapRestoreItem} from '../../../shared/interfaces/map-restore-item.interface'; import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; -import {DayjsTimeService} from '../../../shared/services/dayjs-time.service'; +import {DayjsUtils} from '../../../shared/utils/dayjs.utils'; function createActiveMapItemsFromConfigs(activeMapItemConfigurations: ActiveMapItemConfiguration[]): ActiveMapItem[] { return activeMapItemConfigurations.map( @@ -83,7 +83,7 @@ describe('ShareLinkEffects', () => { opacity: 0.5, visible: true, isSingleLayer: false, - timeExtent: {start: DayjsTimeService.parseUTCDate('1000'), end: DayjsTimeService.parseUTCDate('2020')}, + timeExtent: {start: DayjsUtils.parseUTCDate('1000'), end: DayjsUtils.parseUTCDate('2020')}, attributeFilters: [ { parameter: 'FILTER_GEBART', diff --git a/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts b/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts index 148ba0856..31a672c12 100644 --- a/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts +++ b/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts @@ -2,7 +2,7 @@ import {selectActiveMapItemConfigurations} from './active-map-item-configuration import {ActiveMapItemConfiguration} from '../../../shared/interfaces/active-map-item-configuration.interface'; import {ActiveMapItemFactory} from '../../../shared/factories/active-map-item.factory'; import {Map} from '../../../shared/interfaces/topic.interface'; -import {DayjsTimeService} from '../../../shared/services/dayjs-time.service'; +import {DayjsUtils} from '../../../shared/utils/dayjs.utils'; describe('selectActiveMapItemConfiguration', () => { it('returns activeMapItemConfigurations from ActiveMapItmes', () => { @@ -31,8 +31,8 @@ describe('selectActiveMapItemConfiguration', () => { true, 0.71, { - start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), }, [ { @@ -90,8 +90,8 @@ describe('selectActiveMapItemConfiguration', () => { }, ], timeExtent: { - start: DayjsTimeService.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsTimeService.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), + end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), }, }, ]; diff --git a/src/app/testing/map-testing/share-link-item-test.utils.ts b/src/app/testing/map-testing/share-link-item-test.utils.ts index 1e2bc1c0e..dbf7914aa 100644 --- a/src/app/testing/map-testing/share-link-item-test.utils.ts +++ b/src/app/testing/map-testing/share-link-item-test.utils.ts @@ -1,6 +1,6 @@ import {ShareLinkItem} from '../../shared/interfaces/share-link.interface'; import {MinimalGeometriesUtils} from './minimal-geometries.utils'; -import {DayjsTimeService} from '../../shared/services/dayjs-time.service'; +import {DayjsUtils} from '../../shared/utils/dayjs.utils'; export class ShareLinkItemTestUtils { public static createShareLinkItem(): ShareLinkItem { @@ -33,7 +33,7 @@ export class ShareLinkItemTestUtils { opacity: 0.5, visible: true, isSingleLayer: false, - timeExtent: {start: DayjsTimeService.parseUTCDate('1000'), end: DayjsTimeService.parseUTCDate('2020')}, + timeExtent: {start: DayjsUtils.parseUTCDate('1000'), end: DayjsUtils.parseUTCDate('2020')}, attributeFilters: [ { parameter: 'FILTER_GEBART', diff --git a/src/app/testing/time-service.stub.ts b/src/app/testing/time-service.stub.ts deleted file mode 100644 index b6ebc801c..000000000 --- a/src/app/testing/time-service.stub.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {TimeService} from '../map/interfaces/time.service'; - -export class TimeServiceStub implements TimeService { - public getPartial(date: string, unit: any): number { - return 0; - } - - public getDateAsString(date: Date, format: string): string { - return ''; - } - - public getDate(date: string, format: string): Date { - return new Date(); - } - - public getUTCDate(date: Date, format?: string): Date { - return new Date(); - } - - public getUnixDate(created: number): Date { - return new Date(); - } - - public getDuration(time: string | number, unit?: any): any { - return {}; - } - - public isValidDate(value: string): boolean { - return true; - } - - public addDuration(date: Date, durationToAdd: any): Date { - return new Date(); - } - - public subtractDuration(date: Date, durationToSubtract: any): Date { - return new Date(); - } - - public calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number { - return 0; - } -} From 7e205fe8ae389c4f0263393080afcd4f0978ff47 Mon Sep 17 00:00:00 2001 From: till_schuetze Date: Wed, 18 Sep 2024 15:29:44 +0200 Subject: [PATCH 04/12] update tests --- .../map/services/time-slider.service.spec.ts | 240 ++++++++---------- src/app/shared/utils/dayjs.utils.spec.ts | 90 +++++++ src/app/shared/utils/dayjs.utils.ts | 4 +- 3 files changed, 195 insertions(+), 139 deletions(-) create mode 100644 src/app/shared/utils/dayjs.utils.spec.ts diff --git a/src/app/map/services/time-slider.service.spec.ts b/src/app/map/services/time-slider.service.spec.ts index de2ddbaa9..b3ef994a3 100644 --- a/src/app/map/services/time-slider.service.spec.ts +++ b/src/app/map/services/time-slider.service.spec.ts @@ -3,6 +3,7 @@ import {TimeSliderService} from './time-slider.service'; import dayjs from 'dayjs'; import {TimeSliderConfiguration, TimeSliderParameterSource} from '../../shared/interfaces/topic.interface'; import {TimeExtent} from '../interfaces/time-extent.interface'; +import {DayjsUtils} from '../../shared/utils/dayjs.utils'; describe('TimeSliderService', () => { let service: TimeSliderService; @@ -19,8 +20,8 @@ describe('TimeSliderService', () => { describe('createValidTimeExtent', () => { describe('using "alwaysMaxRange"', () => { const dateFormat = 'YYYY-MM'; - const minimumDate = dayjs('2000-01', dateFormat); - const maximumDate = dayjs('2001-03', dateFormat); + const minimumDate = DayjsUtils.getDateAsString(DayjsUtils.getDate('2000-01', dateFormat), dateFormat); + const maximumDate = DayjsUtils.getDateAsString(DayjsUtils.getDate('2001-03', dateFormat), dateFormat); const alwaysMaxRange = true; const range = undefined; const minimalRange = undefined; @@ -28,8 +29,8 @@ describe('TimeSliderService', () => { const timeSliderConfig: TimeSliderConfiguration = { name: 'mockTimeSlider', dateFormat: dateFormat, - minimumDate: minimumDate.format(dateFormat), - maximumDate: maximumDate.format(dateFormat), + minimumDate, + maximumDate, alwaysMaxRange: alwaysMaxRange, range: range, minimalRange: minimalRange, @@ -43,25 +44,28 @@ describe('TimeSliderService', () => { it('should create always the same time extent using min-/max values', () => { const newValue: TimeExtent = { - start: dayjs('2000-02', dateFormat).toDate(), - end: dayjs('2000-03', dateFormat).toDate(), + start: DayjsUtils.getDate('2000-02', dateFormat), + end: DayjsUtils.getDate('2000-03', dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent( timeSliderConfig, newValue, true, - minimumDate.toDate(), - maximumDate.toDate(), + DayjsUtils.getDate(minimumDate), + DayjsUtils.getDate(maximumDate), ); - expect(dayjs(calculatedTimeExtent.start).diff(minimumDate)).toBe(0); - expect(dayjs(calculatedTimeExtent.end).diff(maximumDate)).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate(minimumDate))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate(maximumDate))).toBe(0); }); }); describe('using "range"', () => { const dateFormat = 'YYYY-MM'; - const minimumDate = dayjs('2000-01', dateFormat); - const maximumDate = dayjs('2001-03', dateFormat); + const minimumDate = DayjsUtils.getDate('2000-01', dateFormat); + const maximumDate = DayjsUtils.getDate('2001-03', dateFormat); + const minimumDateString = DayjsUtils.getDateAsString(minimumDate, dateFormat); + const maximumDateString = DayjsUtils.getDateAsString(maximumDate, dateFormat); + const alwaysMaxRange = false; const range = 'P1M'; const minimalRange = undefined; @@ -69,8 +73,8 @@ describe('TimeSliderService', () => { const timeSliderConfig: TimeSliderConfiguration = { name: 'mockTimeSlider', dateFormat: dateFormat, - minimumDate: minimumDate.format(dateFormat), - maximumDate: maximumDate.format(dateFormat), + minimumDate: minimumDateString, + maximumDate: maximumDateString, alwaysMaxRange: alwaysMaxRange, range: range, minimalRange: minimalRange, @@ -84,63 +88,47 @@ describe('TimeSliderService', () => { it('should not create a new time extent if it is already valid', () => { const newValue: TimeExtent = { - start: dayjs('2000-02', dateFormat).toDate(), - end: dayjs('2000-03', dateFormat).toDate(), + start: DayjsUtils.getDate('2000-02', dateFormat), + end: DayjsUtils.getDate('2000-03', dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent( timeSliderConfig, newValue, true, - minimumDate.toDate(), - maximumDate.toDate(), + DayjsUtils.getDate(minimumDateString), + DayjsUtils.getDate(maximumDateString), ); - expect(dayjs(calculatedTimeExtent.start).diff(newValue.start)).toBe(0); - expect(dayjs(calculatedTimeExtent.end).diff(newValue.end)).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, newValue.start)).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, newValue.end)).toBe(0); }); it('should create a new end date if it is valid', () => { - const newValue: TimeExtent = {start: maximumDate.toDate(), end: minimumDate.toDate()}; - const calculatedTimeExtent = service.createValidTimeExtent( - timeSliderConfig, - newValue, - true, - minimumDate.toDate(), - maximumDate.toDate(), - ); - expect(dayjs(calculatedTimeExtent.start).diff(dayjs('2001-03', dateFormat))).toBe(0); - expect(dayjs(calculatedTimeExtent.end).diff(dayjs('2001-04', dateFormat))).toBe(0); + const newValue: TimeExtent = {start: maximumDate, end: minimumDate}; + const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2001-03', dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2001-04', dateFormat))).toBe(0); }); it('should create a new start date if it is smaller than the minimum date', () => { - const newValue: TimeExtent = {start: minimumDate.subtract(dayjs.duration('P1M')).toDate(), end: minimumDate.toDate()}; - const calculatedTimeExtent = service.createValidTimeExtent( - timeSliderConfig, - newValue, - true, - minimumDate.toDate(), - maximumDate.toDate(), - ); - expect(dayjs(calculatedTimeExtent.start).diff(dayjs('2000-01', dateFormat))).toBe(0); - expect(dayjs(calculatedTimeExtent.end).diff(dayjs('2000-02', dateFormat))).toBe(0); + const newValue: TimeExtent = {start: DayjsUtils.subtractDuration(minimumDate, DayjsUtils.getDuration('P1M')), end: minimumDate}; + const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2000-01', dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2000-02', dateFormat))).toBe(0); }); it('should create a new start date if it is bigger than the maximum date', () => { - const newValue: TimeExtent = {start: maximumDate.add(dayjs.duration('P1M')).toDate(), end: minimumDate.toDate()}; - const calculatedTimeExtent = service.createValidTimeExtent( - timeSliderConfig, - newValue, - true, - minimumDate.toDate(), - maximumDate.toDate(), - ); - expect(dayjs(calculatedTimeExtent.start).diff(dayjs('2001-03', dateFormat))).toBe(0); - expect(dayjs(calculatedTimeExtent.end).diff(dayjs('2001-04', dateFormat))).toBe(0); + const newValue: TimeExtent = {start: DayjsUtils.addDuration(maximumDate, DayjsUtils.getDuration('P1M')), end: minimumDate}; + const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2001-03', dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2001-04', dateFormat))).toBe(0); }); }); describe('using "minimalRange"', () => { const dateFormat = 'YYYY-MM'; - const minimumDate = dayjs('2000-01', dateFormat); - const maximumDate = dayjs('2001-03', dateFormat); + const minimumDate = DayjsUtils.getDate('2000-01', dateFormat); + const maximumDate = DayjsUtils.getDate('2001-03', dateFormat); + const minimumDateString = DayjsUtils.getDateAsString(minimumDate, dateFormat); + const maximumDateString = DayjsUtils.getDateAsString(maximumDate, dateFormat); const alwaysMaxRange = false; const range = undefined; const minimalRange = 'P2M'; @@ -148,8 +136,8 @@ describe('TimeSliderService', () => { const timeSliderConfig: TimeSliderConfiguration = { name: 'mockTimeSlider', dateFormat: dateFormat, - minimumDate: minimumDate.format(dateFormat), - maximumDate: maximumDate.format(dateFormat), + minimumDate: minimumDateString, + maximumDate: maximumDateString, alwaysMaxRange: alwaysMaxRange, range: range, minimalRange: minimalRange, @@ -166,15 +154,9 @@ describe('TimeSliderService', () => { start: dayjs('2000-02', dateFormat).toDate(), end: dayjs('2000-05', dateFormat).toDate(), }; - const calculatedTimeExtent = service.createValidTimeExtent( - timeSliderConfig, - newValue, - true, - minimumDate.toDate(), - maximumDate.toDate(), - ); - expect(dayjs(calculatedTimeExtent.start).diff(newValue.start)).toBe(0); - expect(dayjs(calculatedTimeExtent.end).diff(newValue.end)).toBe(0); + const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, newValue.start)).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, newValue.end)).toBe(0); }); it('should create a new start/end date if it is over/under the limits', () => { @@ -182,15 +164,9 @@ describe('TimeSliderService', () => { start: dayjs('1999-12', dateFormat).toDate(), end: dayjs('2001-04', dateFormat).toDate(), }; - const calculatedTimeExtent = service.createValidTimeExtent( - timeSliderConfig, - newValue, - true, - minimumDate.toDate(), - maximumDate.toDate(), - ); - expect(dayjs(calculatedTimeExtent.start).diff(minimumDate)).toBe(0); - expect(dayjs(calculatedTimeExtent.end).diff(maximumDate)).toBe(0); + const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, minimumDate)).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, maximumDate)).toBe(0); }); it('should adjust the start date if the new start date is too close to the original start date', () => { @@ -198,15 +174,9 @@ describe('TimeSliderService', () => { start: dayjs('2000-03', dateFormat).toDate(), end: dayjs('2000-04', dateFormat).toDate(), }; - const calculatedTimeExtent = service.createValidTimeExtent( - timeSliderConfig, - newValue, - true, - minimumDate.toDate(), - maximumDate.toDate(), - ); - expect(dayjs(calculatedTimeExtent.start).diff(dayjs('2000-02', dateFormat))).toBe(0); - expect(dayjs(calculatedTimeExtent.end).diff(dayjs('2000-04', dateFormat))).toBe(0); + const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2000-02', dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2000-04', dateFormat))).toBe(0); }); it('should adjust the end date if the new end date is too close to the original end date', () => { @@ -214,15 +184,9 @@ describe('TimeSliderService', () => { start: dayjs('2000-02', dateFormat).toDate(), end: dayjs('2000-03', dateFormat).toDate(), }; - const calculatedTimeExtent = service.createValidTimeExtent( - timeSliderConfig, - newValue, - false, - minimumDate.toDate(), - maximumDate.toDate(), - ); - expect(dayjs(calculatedTimeExtent.start).diff(dayjs('2000-02', dateFormat))).toBe(0); - expect(dayjs(calculatedTimeExtent.end).diff(dayjs('2000-04', dateFormat))).toBe(0); + const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, false, minimumDate, maximumDate); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2000-02', dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2000-04', dateFormat))).toBe(0); }); it('should create a new start date if if is too close to the end date and the end date is the maximum possible date', () => { @@ -230,22 +194,18 @@ describe('TimeSliderService', () => { start: dayjs('2001-02', dateFormat).toDate(), end: dayjs('2001-03', dateFormat).toDate(), }; - const calculatedTimeExtent = service.createValidTimeExtent( - timeSliderConfig, - newValue, - true, - minimumDate.toDate(), - maximumDate.toDate(), - ); - expect(dayjs(calculatedTimeExtent.start).diff(dayjs('2001-01', dateFormat))).toBe(0); - expect(dayjs(calculatedTimeExtent.end).diff(dayjs('2001-03', dateFormat))).toBe(0); + const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2001-01', dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2001-03', dateFormat))).toBe(0); }); }); it('should use the correct range in case of years', () => { const dateFormat = 'YYYY'; - const minimumDate = dayjs('2000', dateFormat); - const maximumDate = dayjs('2002', dateFormat); + const minimumDate = DayjsUtils.getDate('2000-01', dateFormat); + const maximumDate = DayjsUtils.getDate('2001-03', dateFormat); + const minimumDateString = DayjsUtils.getDateAsString(minimumDate, dateFormat); + const maximumDateString = DayjsUtils.getDateAsString(maximumDate, dateFormat); const alwaysMaxRange = false; const range = 'P1Y'; const minimalRange = undefined; @@ -253,8 +213,8 @@ describe('TimeSliderService', () => { const timeSliderConfig: TimeSliderConfiguration = { name: 'mockTimeSlider', dateFormat: dateFormat, - minimumDate: minimumDate.format(dateFormat), - maximumDate: maximumDate.format(dateFormat), + minimumDate: minimumDateString, + maximumDate: maximumDateString, alwaysMaxRange: alwaysMaxRange, range: range, minimalRange: minimalRange, @@ -265,19 +225,16 @@ describe('TimeSliderService', () => { layerIdentifiers: [], }, }; - const newValue: TimeExtent = {start: dayjs(minimumDate, dateFormat).toDate(), end: dayjs(minimumDate, dateFormat).toDate()}; + const newValue: TimeExtent = { + start: DayjsUtils.getDate(minimumDateString, dateFormat), + end: DayjsUtils.getDate(minimumDateString, dateFormat), + }; - const calculatedTimeExtent = service.createValidTimeExtent( - timeSliderConfig, - newValue, - true, - minimumDate.toDate(), - maximumDate.toDate(), - ); - expect(dayjs(calculatedTimeExtent.start).diff(dayjs('2000', dateFormat).toDate())).toBe(0); - expect(dayjs(calculatedTimeExtent.start).format(timeSliderConfig.dateFormat)).toBe('2000'); - expect(dayjs(calculatedTimeExtent.end).diff(dayjs('2001', dateFormat).toDate())).toBe(0); - expect(dayjs(calculatedTimeExtent.end).format(timeSliderConfig.dateFormat)).toBe('2001'); + const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2000', dateFormat))).toBe(0); + expect(DayjsUtils.getDateAsString(calculatedTimeExtent.start, timeSliderConfig.dateFormat)).toBe('2000'); + expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2001', dateFormat))).toBe(0); + expect(DayjsUtils.getDateAsString(calculatedTimeExtent.end, timeSliderConfig.dateFormat)).toBe('2001'); }); }); @@ -307,16 +264,18 @@ describe('TimeSliderService', () => { it('should create the correct stops', () => { const stops = service.createStops(timeSliderConfig); expect(stops.length).toBe(3); - expect(dayjs(stops[0]).diff(dayjs.utc(firstStop, dateFormat))).toBe(0); - expect(dayjs(stops[1]).diff(dayjs.utc(secondStop, dateFormat))).toBe(0); - expect(dayjs(stops[2]).diff(dayjs.utc(thirdStop, dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(stops[0], DayjsUtils.parseUTCDate(firstStop, dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(stops[1], DayjsUtils.parseUTCDate(secondStop, dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(stops[2], DayjsUtils.parseUTCDate(thirdStop, dateFormat))).toBe(0); }); }); describe('using a parameter source', () => { describe('with a single time unit and a range', () => { const dateFormat = 'YYYY-MM'; - const minimumDate = dayjs('2000-01', dateFormat); - const maximumDate = dayjs('2001-03', dateFormat); + const minimumDate = DayjsUtils.getDate('2000-01', dateFormat); + const maximumDate = DayjsUtils.getDate('2001-03', dateFormat); + const minimumDateString = DayjsUtils.getDateAsString(minimumDate, dateFormat); + const maximumDateString = DayjsUtils.getDateAsString(maximumDate, dateFormat); const alwaysMaxRange = false; const range = 'P1M'; // one month const minimalRange = undefined; @@ -324,8 +283,8 @@ describe('TimeSliderService', () => { const timeSliderConfig: TimeSliderConfiguration = { name: 'mockTimeSlider', dateFormat: dateFormat, - minimumDate: minimumDate.format(dateFormat), - maximumDate: maximumDate.format(dateFormat), + minimumDate: minimumDateString, + maximumDate: maximumDateString, alwaysMaxRange: alwaysMaxRange, range: range, minimalRange: minimalRange, @@ -340,15 +299,17 @@ describe('TimeSliderService', () => { it('should create the correct stops', () => { const stops = service.createStops(timeSliderConfig); expect(stops.length).toBe(15); - expect(dayjs(stops[0]).diff(dayjs.utc('2000-01', dateFormat))).toBe(0); - expect(dayjs(stops[1]).diff(dayjs.utc('2000-02', dateFormat))).toBe(0); - expect(dayjs(stops[stops.length - 1]).diff(dayjs.utc('2001-03', dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(stops[0], DayjsUtils.parseUTCDate('2000-01', dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(stops[1], DayjsUtils.parseUTCDate('2000-02', dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(stops[stops.length - 1], DayjsUtils.parseUTCDate('2001-03', dateFormat))).toBe( + 0, + ); }); }); describe('with mixed time units', () => { const dateFormat = 'YYYY-MM'; - const minimumDate = dayjs.utc('2000-01', dateFormat); - const maximumDate = dayjs.utc('2001-03', dateFormat); + const minimumDate = DayjsUtils.parseUTCDate('2000-01', dateFormat); + const maximumDate = DayjsUtils.parseUTCDate('2001-03', dateFormat); const parameterSource: TimeSliderParameterSource = { startRangeParameter: '', endRangeParameter: '', @@ -360,8 +321,8 @@ describe('TimeSliderService', () => { const timeSliderConfig: TimeSliderConfiguration = { name: 'mockTimeSlider', dateFormat: dateFormat, - minimumDate: minimumDate.format(dateFormat), - maximumDate: maximumDate.format(dateFormat), + minimumDate: DayjsUtils.getUTCDateAsString(minimumDate, dateFormat), + maximumDate: DayjsUtils.getUTCDateAsString(maximumDate, dateFormat), alwaysMaxRange: false, range: range, // one month and 10 days minimalRange: undefined, @@ -379,17 +340,19 @@ describe('TimeSliderService', () => { */ const expectedNumberOfStops = 12; expect(stops.length).toBe(expectedNumberOfStops); - expect(dayjs(stops[0]).diff(dayjs(minimumDate, dateFormat))).toBe(0); - expect(dayjs(stops[1]).diff(dayjs(minimumDate, dateFormat).add(dayjs.duration(range)))).toBe(0); - expect(dayjs(stops[stops.length - 1]).diff(dayjs(maximumDate, dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(stops[0], minimumDate)).toBe(0); + expect( + DayjsUtils.calculateDifferenceBetweenDates(stops[1], DayjsUtils.addDuration(minimumDate, DayjsUtils.getDuration(range))), + ).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(stops[stops.length - 1], maximumDate)).toBe(0); }); }); describe('and no range', () => { const timeSliderConfig: TimeSliderConfiguration = { name: 'mockTimeSlider', dateFormat: dateFormat, - minimumDate: minimumDate.format(dateFormat), - maximumDate: maximumDate.format(dateFormat), + minimumDate: DayjsUtils.getUTCDateAsString(minimumDate, dateFormat), + maximumDate: DayjsUtils.getUTCDateAsString(maximumDate, dateFormat), alwaysMaxRange: false, range: undefined, minimalRange: undefined, @@ -406,9 +369,12 @@ describe('TimeSliderService', () => { */ const expectedNumberOfStops = 15; expect(stops.length).toBe(expectedNumberOfStops); - expect(dayjs(stops[0]).diff(dayjs.utc('2000-01', dateFormat))).toBe(0); - expect(dayjs(stops[1]).diff(dayjs.utc('2000-02', dateFormat))).toBe(0); - expect(dayjs(stops[stops.length - 1]).diff(dayjs.utc('2001-03', dateFormat))).toBe(0); + + expect(DayjsUtils.calculateDifferenceBetweenDates(stops[0], DayjsUtils.parseUTCDate('2000-01', dateFormat))).toBe(0); + expect(DayjsUtils.calculateDifferenceBetweenDates(stops[1], DayjsUtils.parseUTCDate('2000-02', dateFormat))).toBe(0); + expect( + DayjsUtils.calculateDifferenceBetweenDates(stops[stops.length - 1], DayjsUtils.parseUTCDate('2001-03', dateFormat)), + ).toBe(0); }); }); }); diff --git a/src/app/shared/utils/dayjs.utils.spec.ts b/src/app/shared/utils/dayjs.utils.spec.ts new file mode 100644 index 000000000..bcc5892c7 --- /dev/null +++ b/src/app/shared/utils/dayjs.utils.spec.ts @@ -0,0 +1,90 @@ +import {DayjsUtils} from './dayjs.utils'; +import dayjs from 'dayjs'; + +describe('DayjsUtils', () => { + describe('getPartial', () => { + it('returns the correct partial value', () => { + expect(DayjsUtils.getPartial('2023-10-01', 'years')).toBe(2023); + expect(DayjsUtils.getPartial('2023-10-01', 'month')).toBe(9); // month is 0-indexed + }); + }); + + describe('getDateAsString', () => { + it('returns the date as a formatted string', () => { + const date = new Date(2023, 9, 1); // October 1, 2023 + expect(DayjsUtils.getDateAsString(date, 'YYYY-MM-DD')).toBe('2023-10-01'); + }); + }); + + describe('getDate', () => { + it('returns the date object from a string', () => { + expect(DayjsUtils.getDate('2023-10-01', 'YYYY-MM-DD')).toEqual(new Date(2023, 9, 1)); + expect(DayjsUtils.getDate('2023-10-01')).toEqual(new Date(2023, 9, 1)); + }); + }); + + describe('getUTCDateAsString', () => { + it('returns the UTC date as a formatted string', () => { + const date = new Date(Date.UTC(2023, 9, 1)); // October 1, 2023 UTC + expect(DayjsUtils.getUTCDateAsString(date, 'YYYY-MM-DD')).toBe('2023-10-01'); + }); + }); + + describe('getUnixDate', () => { + it('returns the date object from a Unix timestamp', () => { + const expectedDate = new Date(Date.UTC(2000, 0, 1)); + // 946684800 is the Unix timestamp for 2000-01-01T00:00:00.000Z (from https://timestampgenerator.com/946684800/+00:00) + expect(DayjsUtils.getUnixDate(946684800).getTime()).toEqual(expectedDate.getTime()); + }); + }); + + describe('parseUTCDate', () => { + it('parses the UTC date from a string', () => { + expect(DayjsUtils.parseUTCDate('2023-10-01', 'YYYY-MM-DD')).toEqual(new Date(Date.UTC(2023, 9, 1))); + expect(DayjsUtils.parseUTCDate('2023-10-01')).toEqual(new Date(Date.UTC(2023, 9, 1))); + }); + }); + + describe('getDuration', () => { + it('returns the duration object from a time string', () => { + expect(DayjsUtils.getDuration('P1D').asSeconds()).toBe(86400); + }); + }); + + describe('getDurationWithUnit', () => { + it('returns the duration object from a time and unit', () => { + expect(DayjsUtils.getDurationWithUnit(1, 'year').asYears()).toBe(1); + }); + }); + + describe('isValidDate', () => { + it('validates the date string', () => { + expect(DayjsUtils.isValidDate('2023-10-01')).toBe(true); + expect(DayjsUtils.isValidDate('invalid-date')).toBe(false); + }); + }); + + describe('addDuration', () => { + it('adds the duration to the date', () => { + const date = new Date(2023, 9, 1); + const duration = dayjs.duration({days: 1}); + expect(DayjsUtils.addDuration(date, duration)).toEqual(new Date(2023, 9, 2)); + }); + }); + + describe('subtractDuration', () => { + it('subtracts the duration from the date', () => { + const date = new Date(2023, 9, 1); + const duration = dayjs.duration({days: 1}); + expect(DayjsUtils.subtractDuration(date, duration)).toEqual(new Date(2023, 8, 30)); + }); + }); + + describe('calculateDifferenceBetweenDates', () => { + it('calculates the difference between two dates', () => { + const date1 = new Date(2023, 9, 1); + const date2 = new Date(2023, 9, 2); + expect(DayjsUtils.calculateDifferenceBetweenDates(date1, date2)).toBe(86400000); // 1 day in milliseconds + }); + }); +}); diff --git a/src/app/shared/utils/dayjs.utils.ts b/src/app/shared/utils/dayjs.utils.ts index 84fd92e9e..088f571b6 100644 --- a/src/app/shared/utils/dayjs.utils.ts +++ b/src/app/shared/utils/dayjs.utils.ts @@ -14,8 +14,8 @@ export class DayjsUtils { public static getDateAsString(date: Date, format: string): string { return dayjs(date).format(format); } - public static getDate(date: string, format: string): Date { - return dayjs(date, format).toDate(); + public static getDate(date: string, format?: string): Date { + return format ? dayjs(date, format).toDate() : dayjs(date).toDate(); } public static getUTCDateAsString(date: Date, format?: string): string { return dayjs.utc(date).format(format); From 698fd3cd51c906c8a23e54a7c42420e3e0719284 Mon Sep 17 00:00:00 2001 From: till_schuetze Date: Thu, 19 Sep 2024 15:30:17 +0200 Subject: [PATCH 05/12] fix getPartial Method --- src/app/shared/utils/dayjs.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/utils/dayjs.utils.ts b/src/app/shared/utils/dayjs.utils.ts index 088f571b6..d42f6d90c 100644 --- a/src/app/shared/utils/dayjs.utils.ts +++ b/src/app/shared/utils/dayjs.utils.ts @@ -9,7 +9,7 @@ dayjs.extend(utc); export class DayjsUtils { public static getPartial(date: string, unit: UnitType): number { - return dayjs(date, unit).get(unit); + return dayjs(date).get(unit); } public static getDateAsString(date: Date, format: string): string { return dayjs(date).format(format); From 5a3b8e77fe126d4c25096cb6c6b7b7356af258e6 Mon Sep 17 00:00:00 2001 From: till_schuetze Date: Thu, 19 Sep 2024 15:50:00 +0200 Subject: [PATCH 06/12] add type alias for used dayjs types to further reduce dependency from dayjs --- src/app/map/components/time-slider/time-slider.component.ts | 2 +- src/app/shared/types/dayjs-alias-type.ts | 4 ++++ src/app/shared/utils/dayjs.utils.ts | 3 ++- src/app/shared/utils/time-extent.utils.ts | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 src/app/shared/types/dayjs-alias-type.ts diff --git a/src/app/map/components/time-slider/time-slider.component.ts b/src/app/map/components/time-slider/time-slider.component.ts index 770e8546b..8ecf54841 100644 --- a/src/app/map/components/time-slider/time-slider.component.ts +++ b/src/app/map/components/time-slider/time-slider.component.ts @@ -1,7 +1,7 @@ import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; import {TimeExtent} from '../../interfaces/time-extent.interface'; import {TimeSliderConfiguration, TimeSliderLayerSource} from '../../../shared/interfaces/topic.interface'; -import {ManipulateType, UnitType} from 'dayjs'; +import {ManipulateTypeAlias as ManipulateType, UnitTypeAlias as UnitType} from '../../../shared/types/dayjs-alias-type'; import {TimeSliderService} from '../../services/time-slider.service'; import {TimeExtentUtils} from '../../../shared/utils/time-extent.utils'; import {MatDatepicker} from '@angular/material/datepicker'; diff --git a/src/app/shared/types/dayjs-alias-type.ts b/src/app/shared/types/dayjs-alias-type.ts new file mode 100644 index 000000000..b9b2b7fe3 --- /dev/null +++ b/src/app/shared/types/dayjs-alias-type.ts @@ -0,0 +1,4 @@ +import {ManipulateType, UnitType} from 'dayjs'; + +export type ManipulateTypeAlias = ManipulateType; +export type UnitTypeAlias = UnitType; diff --git a/src/app/shared/utils/dayjs.utils.ts b/src/app/shared/utils/dayjs.utils.ts index d42f6d90c..4def7b034 100644 --- a/src/app/shared/utils/dayjs.utils.ts +++ b/src/app/shared/utils/dayjs.utils.ts @@ -1,4 +1,5 @@ -import dayjs, {ManipulateType, UnitType} from 'dayjs'; +import dayjs from 'dayjs'; +import {ManipulateTypeAlias as ManipulateType, UnitTypeAlias as UnitType} from '../types/dayjs-alias-type'; import duration, {Duration} from 'dayjs/plugin/duration'; import utc from 'dayjs/plugin/utc'; import customParseFormat from 'dayjs/plugin/customParseFormat'; diff --git a/src/app/shared/utils/time-extent.utils.ts b/src/app/shared/utils/time-extent.utils.ts index 809efcb76..adbb3f355 100644 --- a/src/app/shared/utils/time-extent.utils.ts +++ b/src/app/shared/utils/time-extent.utils.ts @@ -1,5 +1,5 @@ import {Duration} from 'dayjs/plugin/duration'; -import {ManipulateType} from 'dayjs'; +import {ManipulateTypeAlias as ManipulateType} from '../types/dayjs-alias-type'; import {TimeSliderConfiguration} from '../interfaces/topic.interface'; import {TimeExtent} from '../../map/interfaces/time-extent.interface'; import {DayjsUtils} from './dayjs.utils'; From cca7241cddfe4ad005b0e72f5ee8fc6ae3dee170 Mon Sep 17 00:00:00 2001 From: Lukas Merz Date: Mon, 23 Sep 2024 16:53:22 +0200 Subject: [PATCH 07/12] GB3-1361: Add TimeService and first refactors --- README.md | 4 + src/app/app.module.ts | 4 + .../time-slider/time-slider.component.ts | 30 +- .../models/implementations/gb2-wms.model.ts | 4 +- src/app/map/pipes/date-to-string.pipe.spec.ts | 6 +- src/app/map/pipes/date-to-string.pipe.ts | 10 +- .../pipes/time-extent-to-string.pipe.spec.ts | 6 +- .../map/pipes/time-extent-to-string.pipe.ts | 10 +- .../esri-services/esri-map.service.ts | 10 +- .../map/services/favourites.service.spec.ts | 76 ++-- src/app/map/services/favourites.service.ts | 12 +- .../map/services/time-slider.service.spec.ts | 377 ++++++++++-------- src/app/map/services/time-slider.service.ts | 206 +++++++++- .../services/onboarding-guide.service.ts | 3 +- .../factories/time-service.factory.spec.ts | 10 + .../shared/factories/time-service.factory.ts | 5 + .../interfaces/time-service.interface.ts | 33 ++ .../abstract-storage.service.ts} | 41 +- .../services/apis/abstract-api.service.ts | 5 +- .../services/apis/gb3/gb3-export.service.ts | 5 +- .../apis/gb3/gb3-favourites.service.ts | 5 +- .../services/apis/gb3/gb3-import.service.ts | 10 +- .../services/apis/gb3/gb3-print.service.ts | 5 +- .../apis/gb3/gb3-share-link.service.ts | 10 +- .../apis/grav-cms/grav-cms.service.ts | 19 +- src/app/shared/services/dayjs.service.spec.ts | 70 ++++ src/app/shared/services/dayjs.service.ts | 48 +++ .../shared/services/local-storage.service.ts | 15 +- .../services/session-storage.service.ts | 11 +- src/app/shared/utils/dayjs.utils.spec.ts | 51 --- src/app/shared/utils/dayjs.utils.ts | 19 +- src/app/shared/utils/storage.utils.spec.ts | 31 -- src/app/shared/utils/time-extent.utils.ts | 178 --------- .../auth/effects/auth-status.effects.spec.ts | 20 +- .../state/auth/effects/auth-status.effects.ts | 7 +- .../map-testing/share-link-item-test.utils.ts | 5 +- src/test.ts | 14 +- 37 files changed, 803 insertions(+), 572 deletions(-) create mode 100644 src/app/shared/factories/time-service.factory.spec.ts create mode 100644 src/app/shared/factories/time-service.factory.ts create mode 100644 src/app/shared/interfaces/time-service.interface.ts rename src/app/shared/{utils/storage.utils.ts => services/abstract-storage.service.ts} (55%) create mode 100644 src/app/shared/services/dayjs.service.spec.ts create mode 100644 src/app/shared/services/dayjs.service.ts delete mode 100644 src/app/shared/utils/storage.utils.spec.ts delete mode 100644 src/app/shared/utils/time-extent.utils.ts diff --git a/README.md b/README.md index 9c67e2282..ded958302 100644 --- a/README.md +++ b/README.md @@ -661,3 +661,7 @@ It will be used later within a Teams announcement. 3. Change the type of article by choosing **Ankündigung** (left of **Veröffentlichen**) 4. Enter the release title as _Überschrift_. E.g. `Release 42` 5. Add the changelog from above as text and slightly format it. Use previous changelogs as styleguide. + +## Contributors + +### Individual contributors diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5b0b45f4c..3a5cdf052 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -31,6 +31,8 @@ import {effectErrorHandler} from './state/app/effects/effects-error-handler.effe import {EsriMapLoaderService} from './map/services/esri-services/esri-map-loader.service'; import {MapLoaderService} from './map/interfaces/map-loader.service'; import {DevModeBannerComponent} from './shared/components/dev-mode-banner/dev-mode-banner.component'; +import {TimeService} from './shared/interfaces/time-service.interface'; +import {timeServiceFactory} from './shared/factories/time-service.factory'; // necessary for the locale 'de-CH' to work // see https://stackoverflow.com/questions/46419026/missing-locale-data-for-the-locale-xxx-with-angular @@ -40,6 +42,7 @@ export const MAP_SERVICE = new InjectionToken('MapService'); export const MAP_LOADER_SERVICE = new InjectionToken('MapLoaderService'); export const NEWS_SERVICE = new InjectionToken('NewsService'); export const GRAV_CMS_SERVICE = new InjectionToken('GravCmsService'); +export const TIME_SERVICE = new InjectionToken('TimeService'); @NgModule({ declarations: [AppComponent], @@ -60,6 +63,7 @@ export const GRAV_CMS_SERVICE = new InjectionToken('GravCmsServi {provide: ErrorHandler, deps: [Router, ErrorHandlerService, EmbeddedErrorHandlerService], useFactory: errorHandlerServiceFactory}, {provide: MAP_SERVICE, useClass: EsriMapService}, {provide: MAP_LOADER_SERVICE, useClass: EsriMapLoaderService}, + {provide: TIME_SERVICE, useFactory: timeServiceFactory}, {provide: NEWS_SERVICE, deps: [KTZHNewsService, KTZHNewsServiceMock, ConfigService], useFactory: newsFactory}, {provide: GRAV_CMS_SERVICE, deps: [GravCmsService, GravCmsServiceMock, ConfigService], useFactory: gravCmsFactory}, {provide: LOCALE_ID, useValue: 'de-CH'}, diff --git a/src/app/map/components/time-slider/time-slider.component.ts b/src/app/map/components/time-slider/time-slider.component.ts index 8ecf54841..c60316469 100644 --- a/src/app/map/components/time-slider/time-slider.component.ts +++ b/src/app/map/components/time-slider/time-slider.component.ts @@ -1,11 +1,12 @@ -import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; +import {Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; import {TimeExtent} from '../../interfaces/time-extent.interface'; import {TimeSliderConfiguration, TimeSliderLayerSource} from '../../../shared/interfaces/topic.interface'; import {ManipulateTypeAlias as ManipulateType, UnitTypeAlias as UnitType} from '../../../shared/types/dayjs-alias-type'; import {TimeSliderService} from '../../services/time-slider.service'; -import {TimeExtentUtils} from '../../../shared/utils/time-extent.utils'; import {MatDatepicker} from '@angular/material/datepicker'; import {DayjsUtils} from '../../../shared/utils/dayjs.utils'; +import {TIME_SERVICE} from '../../../app.module'; +import {TimeService} from '../../../shared/interfaces/time-service.interface'; // There is an array (`allowedDatePickerManipulationUnits`) and a new union type (`DatePickerManipulationUnits`) for two reasons: // To be able to extract a union type subset of `ManipulateType` AND to have an array used to check if a given value is in said union type. @@ -45,7 +46,10 @@ export class TimeSliderComponent implements OnInit, OnChanges { public datePickerStartView: DatePickerStartView = 'month'; private datePickerUnit: DatePickerManipulationUnits = 'days'; - constructor(private readonly timeSliderService: TimeSliderService) {} + constructor( + private readonly timeSliderService: TimeSliderService, + @Inject(TIME_SERVICE) private readonly timeService: TimeService, + ) {} public ngOnInit() { this.availableDates = this.timeSliderService.createStops(this.timeSliderConfiguration); @@ -99,19 +103,19 @@ export class TimeSliderComponent implements OnInit, OnChanges { // correct the thumb that was modified with the calculated time extent if necessary (e.g. enforcing a minimal range) const hasStartTimeBeenCorrected = - TimeExtentUtils.calculateDifferenceBetweenDates(newValidatedTimeExtent.start, newTimeExtent.start) > 0; + this.timeService.calculateDifferenceBetweenDates(newValidatedTimeExtent.start, newTimeExtent.start) > 0; if (hasStartTimeBeenCorrected) { this.firstSliderPosition = this.findPositionOfDate(newValidatedTimeExtent.start) ?? 0; } - const hasEndTimeBeenCorrected = TimeExtentUtils.calculateDifferenceBetweenDates(newValidatedTimeExtent.end, newTimeExtent.end) > 0; + const hasEndTimeBeenCorrected = this.timeService.calculateDifferenceBetweenDates(newValidatedTimeExtent.end, newTimeExtent.end) > 0; if (!this.timeSliderConfiguration.range && hasEndTimeBeenCorrected) { this.secondSliderPosition = this.findPositionOfDate(newValidatedTimeExtent.end) ?? 0; } // overwrite the current time extent and trigger the corresponding event if the new validated time extent is different from the previous one if ( - TimeExtentUtils.calculateDifferenceBetweenDates(this.timeExtent.start, newValidatedTimeExtent.start) > 0 || - TimeExtentUtils.calculateDifferenceBetweenDates(this.timeExtent.end, newValidatedTimeExtent.end) > 0 + this.timeService.calculateDifferenceBetweenDates(this.timeExtent.start, newValidatedTimeExtent.start) > 0 || + this.timeService.calculateDifferenceBetweenDates(this.timeExtent.end, newValidatedTimeExtent.end) > 0 ) { this.timeExtent = newValidatedTimeExtent; this.changeTimeExtentEvent.emit(this.timeExtent); @@ -145,8 +149,8 @@ export class TimeSliderComponent implements OnInit, OnChanges { } // format the given event date to the configured time format and back to ensure that it is a valid date within the current available dates - const date = DayjsUtils.getDate( - DayjsUtils.getDateAsString(eventDate, this.timeSliderConfiguration.dateFormat), + const date = this.timeService.getDateFromString( + this.timeService.getDateAsFormattedString(eventDate, this.timeSliderConfiguration.dateFormat), this.timeSliderConfiguration.dateFormat, ); const position = this.findPositionOfDate(date); @@ -173,8 +177,8 @@ export class TimeSliderComponent implements OnInit, OnChanges { private isRangeExactlyOneOfSingleTimeUnit(range: string | null | undefined): boolean { if (range) { const rangeDuration = DayjsUtils.getDuration(range); - const unit = TimeExtentUtils.extractUniqueUnitFromDuration(rangeDuration); - return unit !== undefined && TimeExtentUtils.getDurationAsNumber(rangeDuration, unit) === 1; + const unit = TimeSliderService.extractUniqueUnitFromDuration(rangeDuration); + return unit !== undefined && TimeSliderService.getDurationAsNumber(rangeDuration, unit) === 1; } return false; } @@ -203,7 +207,7 @@ export class TimeSliderComponent implements OnInit, OnChanges { } private extractUniqueDatePickerUnitFromDateFormat(dateFormat: string): DatePickerManipulationUnits | undefined { - const unit = TimeExtentUtils.extractUniqueUnitFromDateFormat(dateFormat); + const unit = this.timeSliderService.extractUniqueUnitFromDateFormat(dateFormat); if (unit !== undefined && allowedDatePickerManipulationUnits.some((allowedUnit) => allowedUnit === unit)) { return unit as DatePickerManipulationUnits; } @@ -221,7 +225,7 @@ export class TimeSliderComponent implements OnInit, OnChanges { private findPositionOfDate(date: Date): number | undefined { const index = this.availableDates.findIndex( - (availableDate) => TimeExtentUtils.calculateDifferenceBetweenDates(availableDate, date) === 0, + (availableDate) => this.timeService.calculateDifferenceBetweenDates(availableDate, date) === 0, ); return index === -1 ? undefined : index; } diff --git a/src/app/map/models/implementations/gb2-wms.model.ts b/src/app/map/models/implementations/gb2-wms.model.ts index 01265ece1..5efee3fcb 100644 --- a/src/app/map/models/implementations/gb2-wms.model.ts +++ b/src/app/map/models/implementations/gb2-wms.model.ts @@ -1,8 +1,8 @@ import {FilterConfiguration, Map, MapLayer, SearchConfiguration, TimeSliderConfiguration} from '../../../shared/interfaces/topic.interface'; import {TimeExtent} from '../../interfaces/time-extent.interface'; -import {TimeExtentUtils} from '../../../shared/utils/time-extent.utils'; import {AbstractActiveMapItemSettings, ActiveMapItem} from '../active-map-item.model'; import {AddToMapVisitor} from '../../interfaces/add-to-map.visitor'; +import {TimeSliderService} from '../../services/time-slider.service'; export class Gb2WmsSettings extends AbstractActiveMapItemSettings { public readonly type = 'gb2Wms'; @@ -23,7 +23,7 @@ export class Gb2WmsSettings extends AbstractActiveMapItemSettings { this.layers = layer ? [layer] : map.layers; this.timeSliderConfiguration = map.timeSliderConfiguration; if (map.timeSliderConfiguration) { - this.timeSliderExtent = timeExtent ?? TimeExtentUtils.createInitialTimeSliderExtent(map.timeSliderConfiguration); + this.timeSliderExtent = timeExtent ?? TimeSliderService.createInitialTimeSliderExtent(map.timeSliderConfiguration); } this.filterConfigurations = filterConfigurations ?? map.filterConfigurations; this.searchConfigurations = map.searchConfigurations; diff --git a/src/app/map/pipes/date-to-string.pipe.spec.ts b/src/app/map/pipes/date-to-string.pipe.spec.ts index 833af0a14..f74f4274d 100644 --- a/src/app/map/pipes/date-to-string.pipe.spec.ts +++ b/src/app/map/pipes/date-to-string.pipe.spec.ts @@ -1,12 +1,16 @@ import {DateToStringPipe} from './date-to-string.pipe'; import {TestBed} from '@angular/core/testing'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../../shared/interfaces/time-service.interface'; describe('DateToStringPipe', () => { let pipe: DateToStringPipe; + let timeService: TimeService; beforeEach(() => { TestBed.configureTestingModule({}); - pipe = new DateToStringPipe(); + timeService = TestBed.inject(TIME_SERVICE); + pipe = new DateToStringPipe(timeService); }); it('create an instance', () => { diff --git a/src/app/map/pipes/date-to-string.pipe.ts b/src/app/map/pipes/date-to-string.pipe.ts index 2dd04372a..b40439a41 100644 --- a/src/app/map/pipes/date-to-string.pipe.ts +++ b/src/app/map/pipes/date-to-string.pipe.ts @@ -1,13 +1,15 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {DayjsUtils} from '../../shared/utils/dayjs.utils'; +import {Inject, Pipe, PipeTransform} from '@angular/core'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../../shared/interfaces/time-service.interface'; @Pipe({ name: 'dateToString', standalone: true, }) export class DateToStringPipe implements PipeTransform { - constructor() {} + constructor(@Inject(TIME_SERVICE) private readonly timeService: TimeService) {} + public transform(value: Date | undefined, dateFormat: string): string { - return value ? DayjsUtils.getDateAsString(value, dateFormat) : ''; + return value ? this.timeService.getDateAsFormattedString(value, dateFormat) : ''; } } diff --git a/src/app/map/pipes/time-extent-to-string.pipe.spec.ts b/src/app/map/pipes/time-extent-to-string.pipe.spec.ts index 11adc8643..2d20a6dc7 100644 --- a/src/app/map/pipes/time-extent-to-string.pipe.spec.ts +++ b/src/app/map/pipes/time-extent-to-string.pipe.spec.ts @@ -1,13 +1,17 @@ import {TimeExtentToStringPipe} from './time-extent-to-string.pipe'; import {TimeExtent} from '../interfaces/time-extent.interface'; import {TestBed} from '@angular/core/testing'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../../shared/interfaces/time-service.interface'; describe('TimeExtentToStringPipe', () => { let pipe: TimeExtentToStringPipe; + let timeService: TimeService; beforeEach(() => { TestBed.configureTestingModule({}); - pipe = new TimeExtentToStringPipe(); + timeService = TestBed.inject(TIME_SERVICE); + pipe = new TimeExtentToStringPipe(timeService); }); it('create an instance', () => { expect(pipe).toBeTruthy(); diff --git a/src/app/map/pipes/time-extent-to-string.pipe.ts b/src/app/map/pipes/time-extent-to-string.pipe.ts index fe2067420..c328ce1e4 100644 --- a/src/app/map/pipes/time-extent-to-string.pipe.ts +++ b/src/app/map/pipes/time-extent-to-string.pipe.ts @@ -1,13 +1,15 @@ -import {Pipe, PipeTransform} from '@angular/core'; +import {Inject, Pipe, PipeTransform} from '@angular/core'; import {TimeExtent} from '../interfaces/time-extent.interface'; -import {DayjsUtils} from '../../shared/utils/dayjs.utils'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../../shared/interfaces/time-service.interface'; @Pipe({ name: 'timeExtentToString', standalone: true, }) export class TimeExtentToStringPipe implements PipeTransform { - constructor() {} + constructor(@Inject(TIME_SERVICE) private readonly timeService: TimeService) {} + public transform(timeExtent: TimeExtent | undefined, dateFormat: string, hasSimpleCurrentValue: boolean): string { if (!timeExtent) { return ''; @@ -18,6 +20,6 @@ export class TimeExtentToStringPipe implements PipeTransform { } private convertDateToString(value: Date, dateFormat: string): string { - return value ? DayjsUtils.getDateAsString(value, dateFormat) : ''; + return value ? this.timeService.getDateAsFormattedString(value, dateFormat) : ''; } } diff --git a/src/app/map/services/esri-services/esri-map.service.ts b/src/app/map/services/esri-services/esri-map.service.ts index 83d08fb59..c91d2ab61 100644 --- a/src/app/map/services/esri-services/esri-map.service.ts +++ b/src/app/map/services/esri-services/esri-map.service.ts @@ -1,4 +1,4 @@ -import {Injectable, OnDestroy} from '@angular/core'; +import {Inject, Injectable, OnDestroy} from '@angular/core'; import esriConfig from '@arcgis/core/config'; import * as geometryEngine from '@arcgis/core/geometry/geometryEngine'; import GraphicsLayer from '@arcgis/core/layers/GraphicsLayer'; @@ -64,7 +64,8 @@ import {InitialMapExtentService} from '../initial-map-extent.service'; import {MapConstants} from '../../../shared/constants/map.constants'; import {HitTestSelectionUtils} from './utils/hit-test-selection.utils'; import * as intl from '@arcgis/core/intl'; -import {DayjsUtils} from '../../../shared/utils/dayjs.utils'; +import {TimeService} from '../../../shared/interfaces/time-service.interface'; +import {TIME_SERVICE} from '../../../app.module'; import GraphicHit = __esri.GraphicHit; const DEFAULT_POINT_ZOOM_EXTENT_SCALE = 750; @@ -106,6 +107,7 @@ export class EsriMapService implements MapService, OnDestroy { private readonly esriToolService: EsriToolService, private readonly gb3TopicsService: Gb3TopicsService, private readonly initialMapExtentService: InitialMapExtentService, + @Inject(TIME_SERVICE) private readonly timeService: TimeService, ) { /** * Because the GetCapabalities response often sends a non-secure http://wms.zh.ch response, Esri Javascript API fails on https @@ -635,11 +637,11 @@ export class EsriMapService implements MapService, OnDestroy { const dateFormat = timeSliderConfiguration.dateFormat; esriLayer.customLayerParameters = esriLayer.customLayerParameters ?? {}; - esriLayer.customLayerParameters[timeSliderParameterSource.startRangeParameter] = DayjsUtils.getUTCDateAsString( + esriLayer.customLayerParameters[timeSliderParameterSource.startRangeParameter] = this.timeService.getDateAsUTCString( timeSliderExtent.start, dateFormat, ); - esriLayer.customLayerParameters[timeSliderParameterSource.endRangeParameter] = DayjsUtils.getUTCDateAsString( + esriLayer.customLayerParameters[timeSliderParameterSource.endRangeParameter] = this.timeService.getDateAsUTCString( timeSliderExtent.end, dateFormat, ); diff --git a/src/app/map/services/favourites.service.spec.ts b/src/app/map/services/favourites.service.spec.ts index bcade2d0f..221556973 100644 --- a/src/app/map/services/favourites.service.spec.ts +++ b/src/app/map/services/favourites.service.spec.ts @@ -20,14 +20,16 @@ import {DrawingActiveMapItem} from '../models/implementations/drawing.model'; import {DrawingLayerPrefix, UserDrawingLayer} from '../../shared/enums/drawing-layer.enum'; import {SymbolizationToGb3ConverterUtils} from '../../shared/utils/symbolization-to-gb3-converter.utils'; import {Map} from '../../shared/interfaces/topic.interface'; -import {TimeExtentUtils} from '../../shared/utils/time-extent.utils'; import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; -import {DayjsUtils} from '../../shared/utils/dayjs.utils'; +import {TimeService} from '../../shared/interfaces/time-service.interface'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeSliderService} from './time-slider.service'; describe('FavouritesService', () => { let service: FavouritesService; let store: MockStore; let gb3FavouritesService: Gb3FavouritesService; + let timeService: TimeService; beforeEach(() => { TestBed.configureTestingModule({ @@ -35,6 +37,7 @@ describe('FavouritesService', () => { providers: [provideMockStore({}), provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], }); store = TestBed.inject(MockStore); + timeService = TestBed.inject(TIME_SERVICE); store.overrideSelector(selectActiveMapItemConfigurations, []); store.overrideSelector(selectMaps, []); store.overrideSelector(selectFavouriteBaseConfig, {center: {x: 0, y: 0}, scale: 0, basemap: ''}); @@ -385,8 +388,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, { @@ -530,8 +533,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -648,8 +651,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -709,8 +712,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -799,8 +802,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -858,8 +861,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -872,6 +875,7 @@ describe('FavouritesService', () => { }); it('throws a FavouriteIsInvalidError if a new filterConfiguration has been added', () => { + // eslint-disable-next-line @typescript-eslint/dot-notation service['availableMaps'] = [ { id: 'StatGebAlterZH', @@ -1036,8 +1040,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1050,6 +1054,7 @@ describe('FavouritesService', () => { }); it('throws a FavouriteIsInvalidError if a new filter has been added', () => { + // eslint-disable-next-line @typescript-eslint/dot-notation service['availableMaps'] = [ { id: 'StatGebAlterZH', @@ -1194,8 +1199,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1208,6 +1213,7 @@ describe('FavouritesService', () => { }); it('throws a FavouriteIsInvalidError if the timeSliderConfiguration for a parameter configuration is invalid (start < minimumDate)', () => { + // eslint-disable-next-line @typescript-eslint/dot-notation service['availableMaps'] = [ { id: 'StatGebAlterZH', @@ -1351,8 +1357,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('0999-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('0999-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1363,6 +1369,7 @@ describe('FavouritesService', () => { }); it('throws a FavouriteIsInvalidError if the timeSliderConfiguration for a parameter configuration is invalid (range to small)', () => { + // eslint-disable-next-line @typescript-eslint/dot-notation service['availableMaps'] = [ { id: 'StatGebAlterZH', @@ -1506,8 +1513,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('1450-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('1455-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('1450-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('1455-01-01T00:00:00.000Z'), }, }, ]; @@ -1518,6 +1525,7 @@ describe('FavouritesService', () => { }); it('throws a FavouriteIsInvalidError if the timeSliderConfiguration for a parameter configuration is invalid (start and end date mixed up)', () => { + // eslint-disable-next-line @typescript-eslint/dot-notation service['availableMaps'] = [ { id: 'StatGebAlterZH', @@ -1661,8 +1669,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('1750-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('1455-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('1750-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('1455-01-01T00:00:00.000Z'), }, }, ]; @@ -1673,6 +1681,7 @@ describe('FavouritesService', () => { }); it('throws a FavouriteIsInvalidError if the timeSliderConfiguration for a parameter configuration is invalid (not max range if Flag is set)', () => { + // eslint-disable-next-line @typescript-eslint/dot-notation service['availableMaps'] = [ { id: 'StatGebAlterZH', @@ -1816,8 +1825,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: DayjsUtils.parseUTCDate('1250-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2000-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('1250-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2000-01-01T00:00:00.000Z'), }, }, ]; @@ -1828,6 +1837,7 @@ describe('FavouritesService', () => { }); it('throws a FavouriteIsInvalidError if the timeSliderConfiguration for a layer configuration is invalid', () => { + // eslint-disable-next-line @typescript-eslint/dot-notation service['availableMaps'] = [ { id: 'OrthoFCIRZH', @@ -2082,8 +2092,8 @@ describe('FavouritesService', () => { isSingleLayer: false, attributeFilters: undefined, timeExtent: { - start: DayjsUtils.parseUTCDate('2016-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2017-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('2016-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2017-01-01T00:00:00.000Z'), }, }, ]; @@ -2094,6 +2104,7 @@ describe('FavouritesService', () => { }); it('returns the initalTimsliderExtent if the timeExtent is invalid but ignoreErrors is set to true', () => { + // eslint-disable-next-line @typescript-eslint/dot-notation service['availableMaps'] = [ { id: 'OrthoFCIRZH', @@ -2348,21 +2359,24 @@ describe('FavouritesService', () => { isSingleLayer: false, attributeFilters: undefined, timeExtent: { - start: DayjsUtils.parseUTCDate('2016-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2017-01-01T00:00:00.000Z'), + start: timeService.getUTCDateFromString('2016-01-01T00:00:00.000Z'), + end: timeService.getUTCDateFromString('2017-01-01T00:00:00.000Z'), }, }, ]; const result = service.getActiveMapItemsForFavourite(activeMapItemConfigurations, true); - const initialTimeExtent = TimeExtentUtils.createInitialTimeSliderExtent(service['availableMaps'][0].timeSliderConfiguration!); + // eslint-disable-next-line @typescript-eslint/dot-notation + const initialTimeExtent = TimeSliderService.createInitialTimeSliderExtent(service['availableMaps'][0].timeSliderConfiguration!); const activeMapItems: ActiveMapItem[] = [ ActiveMapItemFactory.createGb2WmsMapItem( + // eslint-disable-next-line @typescript-eslint/dot-notation service['availableMaps'][0], undefined, true, 1, initialTimeExtent, + // eslint-disable-next-line @typescript-eslint/dot-notation service['availableMaps'][0].filterConfigurations, ), ]; diff --git a/src/app/map/services/favourites.service.ts b/src/app/map/services/favourites.service.ts index b96fe4cb1..6522b86eb 100644 --- a/src/app/map/services/favourites.service.ts +++ b/src/app/map/services/favourites.service.ts @@ -1,4 +1,4 @@ -import {Injectable, OnDestroy} from '@angular/core'; +import {Inject, Injectable, OnDestroy} from '@angular/core'; import {Store} from '@ngrx/store'; import {Gb3FavouritesService} from '../../shared/services/apis/gb3/gb3-favourites.service'; import {Observable, Subscription, switchMap, tap, withLatestFrom} from 'rxjs'; @@ -27,9 +27,9 @@ import {SymbolizationToGb3ConverterUtils} from '../../shared/utils/symbolization import {DrawingActiveMapItem} from '../models/implementations/drawing.model'; import {Gb3StyledInternalDrawingRepresentation} from '../../shared/interfaces/internal-drawing-representation.interface'; import {TimeExtent} from '../interfaces/time-extent.interface'; -import {TimeExtentUtils} from '../../shared/utils/time-extent.utils'; import {TimeSliderService} from './time-slider.service'; -import {DayjsUtils} from '../../shared/utils/dayjs.utils'; +import {TimeService} from '../../shared/interfaces/time-service.interface'; +import {TIME_SERVICE} from '../../app.module'; @Injectable({ providedIn: 'root', @@ -47,6 +47,7 @@ export class FavouritesService implements OnDestroy { private readonly store: Store, private readonly gb3FavouritesService: Gb3FavouritesService, private readonly timeSliderService: TimeSliderService, + @Inject(TIME_SERVICE) private readonly timeService: TimeService, ) { this.initSubscriptions(); } @@ -253,7 +254,7 @@ export class FavouritesService implements OnDestroy { const isValid = this.validateTimeSlider(timeSliderConfiguration, timeExtent); if (!isValid) { if (ignoreErrors) { - return TimeExtentUtils.createInitialTimeSliderExtent(timeSliderConfiguration); + return TimeSliderService.createInitialTimeSliderExtent(timeSliderConfiguration); } else { throw new FavouriteIsInvalid(`Die Konfiguration für den Zeitschieberegler der Karte '${title}' ist ungültig.`); } @@ -352,7 +353,8 @@ export class FavouritesService implements OnDestroy { return isTimeExtentValid; case 'layer': { const selectedYearExists = (timeSliderConfiguration.source as TimeSliderLayerSource).layers.some( - (layer) => DayjsUtils.parseUTCDate(layer.date, timeSliderConfiguration.dateFormat).getTime() === timeExtent.start.getTime(), + (layer) => + this.timeService.getUTCDateFromString(layer.date, timeSliderConfiguration.dateFormat).getTime() === timeExtent.start.getTime(), ); return selectedYearExists && isTimeExtentValid; } diff --git a/src/app/map/services/time-slider.service.spec.ts b/src/app/map/services/time-slider.service.spec.ts index b3ef994a3..0ddf6cb7e 100644 --- a/src/app/map/services/time-slider.service.spec.ts +++ b/src/app/map/services/time-slider.service.spec.ts @@ -4,13 +4,17 @@ import dayjs from 'dayjs'; import {TimeSliderConfiguration, TimeSliderParameterSource} from '../../shared/interfaces/topic.interface'; import {TimeExtent} from '../interfaces/time-extent.interface'; import {DayjsUtils} from '../../shared/utils/dayjs.utils'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../../shared/interfaces/time-service.interface'; describe('TimeSliderService', () => { let service: TimeSliderService; + let timeService: TimeService; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(TimeSliderService); + timeService = TestBed.inject(TIME_SERVICE); }); it('should be created', () => { @@ -20,134 +24,165 @@ describe('TimeSliderService', () => { describe('createValidTimeExtent', () => { describe('using "alwaysMaxRange"', () => { const dateFormat = 'YYYY-MM'; - const minimumDate = DayjsUtils.getDateAsString(DayjsUtils.getDate('2000-01', dateFormat), dateFormat); - const maximumDate = DayjsUtils.getDateAsString(DayjsUtils.getDate('2001-03', dateFormat), dateFormat); const alwaysMaxRange = true; const range = undefined; const minimalRange = undefined; + let minimumDate: string; + let maximumDate: string; + let timeSliderConfig: TimeSliderConfiguration; - const timeSliderConfig: TimeSliderConfiguration = { - name: 'mockTimeSlider', - dateFormat: dateFormat, - minimumDate, - maximumDate, - alwaysMaxRange: alwaysMaxRange, - range: range, - minimalRange: minimalRange, - sourceType: 'parameter', - source: { - startRangeParameter: '', - endRangeParameter: '', - layerIdentifiers: [], - }, - }; + beforeEach(() => { + minimumDate = timeService.getDateAsFormattedString(timeService.getDateFromString('2000-01', dateFormat), dateFormat); + maximumDate = timeService.getDateAsFormattedString(timeService.getDateFromString('2001-03', dateFormat), dateFormat); + timeSliderConfig = { + name: 'mockTimeSlider', + dateFormat: dateFormat, + minimumDate, + maximumDate, + alwaysMaxRange: alwaysMaxRange, + range: range, + minimalRange: minimalRange, + sourceType: 'parameter', + source: { + startRangeParameter: '', + endRangeParameter: '', + layerIdentifiers: [], + }, + }; + }); it('should create always the same time extent using min-/max values', () => { const newValue: TimeExtent = { - start: DayjsUtils.getDate('2000-02', dateFormat), - end: DayjsUtils.getDate('2000-03', dateFormat), + start: timeService.getDateFromString('2000-02', dateFormat), + end: timeService.getDateFromString('2000-03', dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent( timeSliderConfig, newValue, true, - DayjsUtils.getDate(minimumDate), - DayjsUtils.getDate(maximumDate), + timeService.getDateFromString(minimumDate), + timeService.getDateFromString(maximumDate), ); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate(minimumDate))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate(maximumDate))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString(minimumDate))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString(maximumDate))).toBe(0); }); }); + describe('using "range"', () => { const dateFormat = 'YYYY-MM'; - const minimumDate = DayjsUtils.getDate('2000-01', dateFormat); - const maximumDate = DayjsUtils.getDate('2001-03', dateFormat); - const minimumDateString = DayjsUtils.getDateAsString(minimumDate, dateFormat); - const maximumDateString = DayjsUtils.getDateAsString(maximumDate, dateFormat); - const alwaysMaxRange = false; const range = 'P1M'; const minimalRange = undefined; + let minimumDate: Date; + let maximumDate: Date; + let minimumDateString: string; + let maximumDateString: string; + let timeSliderConfig: TimeSliderConfiguration; - const timeSliderConfig: TimeSliderConfiguration = { - name: 'mockTimeSlider', - dateFormat: dateFormat, - minimumDate: minimumDateString, - maximumDate: maximumDateString, - alwaysMaxRange: alwaysMaxRange, - range: range, - minimalRange: minimalRange, - sourceType: 'parameter', - source: { - startRangeParameter: '', - endRangeParameter: '', - layerIdentifiers: [], - }, - }; + beforeEach(() => { + minimumDate = timeService.getDateFromString('2000-01', dateFormat); + maximumDate = timeService.getDateFromString('2001-03', dateFormat); + minimumDateString = timeService.getDateAsFormattedString(minimumDate, dateFormat); + maximumDateString = timeService.getDateAsFormattedString(maximumDate, dateFormat); + timeSliderConfig = { + name: 'mockTimeSlider', + dateFormat: dateFormat, + minimumDate: minimumDateString, + maximumDate: maximumDateString, + alwaysMaxRange: alwaysMaxRange, + range: range, + minimalRange: minimalRange, + sourceType: 'parameter', + source: { + startRangeParameter: '', + endRangeParameter: '', + layerIdentifiers: [], + }, + }; + }); it('should not create a new time extent if it is already valid', () => { const newValue: TimeExtent = { - start: DayjsUtils.getDate('2000-02', dateFormat), - end: DayjsUtils.getDate('2000-03', dateFormat), + start: timeService.getDateFromString('2000-02', dateFormat), + end: timeService.getDateFromString('2000-03', dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent( timeSliderConfig, newValue, true, - DayjsUtils.getDate(minimumDateString), - DayjsUtils.getDate(maximumDateString), + timeService.getDateFromString(minimumDateString), + timeService.getDateFromString(maximumDateString), ); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, newValue.start)).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, newValue.end)).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, newValue.start)).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, newValue.end)).toBe(0); }); it('should create a new end date if it is valid', () => { const newValue: TimeExtent = {start: maximumDate, end: minimumDate}; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2001-03', dateFormat))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2001-04', dateFormat))).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2001-03', dateFormat)), + ).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2001-04', dateFormat)), + ).toBe(0); }); it('should create a new start date if it is smaller than the minimum date', () => { const newValue: TimeExtent = {start: DayjsUtils.subtractDuration(minimumDate, DayjsUtils.getDuration('P1M')), end: minimumDate}; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2000-01', dateFormat))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2000-02', dateFormat))).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2000-01', dateFormat)), + ).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2000-02', dateFormat)), + ).toBe(0); }); it('should create a new start date if it is bigger than the maximum date', () => { const newValue: TimeExtent = {start: DayjsUtils.addDuration(maximumDate, DayjsUtils.getDuration('P1M')), end: minimumDate}; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2001-03', dateFormat))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2001-04', dateFormat))).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2001-03', dateFormat)), + ).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2001-04', dateFormat)), + ).toBe(0); }); }); + describe('using "minimalRange"', () => { const dateFormat = 'YYYY-MM'; - const minimumDate = DayjsUtils.getDate('2000-01', dateFormat); - const maximumDate = DayjsUtils.getDate('2001-03', dateFormat); - const minimumDateString = DayjsUtils.getDateAsString(minimumDate, dateFormat); - const maximumDateString = DayjsUtils.getDateAsString(maximumDate, dateFormat); const alwaysMaxRange = false; const range = undefined; const minimalRange = 'P2M'; + let timeSliderConfig: TimeSliderConfiguration; + let minimumDate: Date; + let maximumDate: Date; + let minimumDateString: string; + let maximumDateString: string; - const timeSliderConfig: TimeSliderConfiguration = { - name: 'mockTimeSlider', - dateFormat: dateFormat, - minimumDate: minimumDateString, - maximumDate: maximumDateString, - alwaysMaxRange: alwaysMaxRange, - range: range, - minimalRange: minimalRange, - sourceType: 'parameter', - source: { - startRangeParameter: '', - endRangeParameter: '', - layerIdentifiers: [], - }, - }; + beforeEach(() => { + minimumDate = timeService.getDateFromString('2000-01', dateFormat); + maximumDate = timeService.getDateFromString('2001-03', dateFormat); + maximumDateString = timeService.getDateAsFormattedString(maximumDate, dateFormat); + timeSliderConfig = { + name: 'mockTimeSlider', + dateFormat: dateFormat, + minimumDate: minimumDateString, + maximumDate: maximumDateString, + alwaysMaxRange: alwaysMaxRange, + range: range, + minimalRange: minimalRange, + sourceType: 'parameter', + source: { + startRangeParameter: '', + endRangeParameter: '', + layerIdentifiers: [], + }, + }; + }); it('should not create a new time extent if it is already valid', () => { const newValue: TimeExtent = { @@ -155,8 +190,8 @@ describe('TimeSliderService', () => { end: dayjs('2000-05', dateFormat).toDate(), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, newValue.start)).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, newValue.end)).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, newValue.start)).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, newValue.end)).toBe(0); }); it('should create a new start/end date if it is over/under the limits', () => { @@ -165,8 +200,8 @@ describe('TimeSliderService', () => { end: dayjs('2001-04', dateFormat).toDate(), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, minimumDate)).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, maximumDate)).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, minimumDate)).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, maximumDate)).toBe(0); }); it('should adjust the start date if the new start date is too close to the original start date', () => { @@ -175,8 +210,12 @@ describe('TimeSliderService', () => { end: dayjs('2000-04', dateFormat).toDate(), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2000-02', dateFormat))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2000-04', dateFormat))).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2000-02', dateFormat)), + ).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2000-04', dateFormat)), + ).toBe(0); }); it('should adjust the end date if the new end date is too close to the original end date', () => { @@ -185,8 +224,12 @@ describe('TimeSliderService', () => { end: dayjs('2000-03', dateFormat).toDate(), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, false, minimumDate, maximumDate); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2000-02', dateFormat))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2000-04', dateFormat))).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2000-02', dateFormat)), + ).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2000-04', dateFormat)), + ).toBe(0); }); it('should create a new start date if if is too close to the end date and the end date is the maximum possible date', () => { @@ -195,17 +238,21 @@ describe('TimeSliderService', () => { end: dayjs('2001-03', dateFormat).toDate(), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2001-01', dateFormat))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2001-03', dateFormat))).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2001-01', dateFormat)), + ).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2001-03', dateFormat)), + ).toBe(0); }); }); it('should use the correct range in case of years', () => { const dateFormat = 'YYYY'; - const minimumDate = DayjsUtils.getDate('2000-01', dateFormat); - const maximumDate = DayjsUtils.getDate('2001-03', dateFormat); - const minimumDateString = DayjsUtils.getDateAsString(minimumDate, dateFormat); - const maximumDateString = DayjsUtils.getDateAsString(maximumDate, dateFormat); + const minimumDate = timeService.getDateFromString('2000-01', dateFormat); + const maximumDate = timeService.getDateFromString('2001-03', dateFormat); + const minimumDateString = timeService.getDateAsFormattedString(minimumDate, dateFormat); + const maximumDateString = timeService.getDateAsFormattedString(maximumDate, dateFormat); const alwaysMaxRange = false; const range = 'P1Y'; const minimalRange = undefined; @@ -226,15 +273,19 @@ describe('TimeSliderService', () => { }, }; const newValue: TimeExtent = { - start: DayjsUtils.getDate(minimumDateString, dateFormat), - end: DayjsUtils.getDate(minimumDateString, dateFormat), + start: timeService.getDateFromString(minimumDateString, dateFormat), + end: timeService.getDateFromString(minimumDateString, dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.start, DayjsUtils.getDate('2000', dateFormat))).toBe(0); - expect(DayjsUtils.getDateAsString(calculatedTimeExtent.start, timeSliderConfig.dateFormat)).toBe('2000'); - expect(DayjsUtils.calculateDifferenceBetweenDates(calculatedTimeExtent.end, DayjsUtils.getDate('2001', dateFormat))).toBe(0); - expect(DayjsUtils.getDateAsString(calculatedTimeExtent.end, timeSliderConfig.dateFormat)).toBe('2001'); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2000', dateFormat)), + ).toBe(0); + expect(timeService.getDateAsFormattedString(calculatedTimeExtent.start, timeSliderConfig.dateFormat)).toBe('2000'); + expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2001', dateFormat))).toBe( + 0, + ); + expect(timeService.getDateAsFormattedString(calculatedTimeExtent.end, timeSliderConfig.dateFormat)).toBe('2001'); }); }); @@ -264,71 +315,79 @@ describe('TimeSliderService', () => { it('should create the correct stops', () => { const stops = service.createStops(timeSliderConfig); expect(stops.length).toBe(3); - expect(DayjsUtils.calculateDifferenceBetweenDates(stops[0], DayjsUtils.parseUTCDate(firstStop, dateFormat))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(stops[1], DayjsUtils.parseUTCDate(secondStop, dateFormat))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(stops[2], DayjsUtils.parseUTCDate(thirdStop, dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[0], timeService.getUTCDateFromString(firstStop, dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[1], timeService.getUTCDateFromString(secondStop, dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[2], timeService.getUTCDateFromString(thirdStop, dateFormat))).toBe(0); }); }); describe('using a parameter source', () => { describe('with a single time unit and a range', () => { - const dateFormat = 'YYYY-MM'; - const minimumDate = DayjsUtils.getDate('2000-01', dateFormat); - const maximumDate = DayjsUtils.getDate('2001-03', dateFormat); - const minimumDateString = DayjsUtils.getDateAsString(minimumDate, dateFormat); - const maximumDateString = DayjsUtils.getDateAsString(maximumDate, dateFormat); - const alwaysMaxRange = false; - const range = 'P1M'; // one month - const minimalRange = undefined; - - const timeSliderConfig: TimeSliderConfiguration = { - name: 'mockTimeSlider', - dateFormat: dateFormat, - minimumDate: minimumDateString, - maximumDate: maximumDateString, - alwaysMaxRange: alwaysMaxRange, - range: range, - minimalRange: minimalRange, - sourceType: 'parameter', - source: { - startRangeParameter: '', - endRangeParameter: '', - layerIdentifiers: [], - }, - }; - it('should create the correct stops', () => { + const dateFormat = 'YYYY-MM'; + const minimumDate = timeService.getDateFromString('2000-01', dateFormat); + const maximumDate = timeService.getDateFromString('2001-03', dateFormat); + const minimumDateString = timeService.getDateAsFormattedString(minimumDate, dateFormat); + const maximumDateString = timeService.getDateAsFormattedString(maximumDate, dateFormat); + const alwaysMaxRange = false; + const range = 'P1M'; // one month + const minimalRange = undefined; + + const timeSliderConfig: TimeSliderConfiguration = { + name: 'mockTimeSlider', + dateFormat: dateFormat, + minimumDate: minimumDateString, + maximumDate: maximumDateString, + alwaysMaxRange: alwaysMaxRange, + range: range, + minimalRange: minimalRange, + sourceType: 'parameter', + source: { + startRangeParameter: '', + endRangeParameter: '', + layerIdentifiers: [], + }, + }; const stops = service.createStops(timeSliderConfig); expect(stops.length).toBe(15); - expect(DayjsUtils.calculateDifferenceBetweenDates(stops[0], DayjsUtils.parseUTCDate('2000-01', dateFormat))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(stops[1], DayjsUtils.parseUTCDate('2000-02', dateFormat))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(stops[stops.length - 1], DayjsUtils.parseUTCDate('2001-03', dateFormat))).toBe( - 0, - ); + expect(timeService.calculateDifferenceBetweenDates(stops[0], timeService.getUTCDateFromString('2000-01', dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[1], timeService.getUTCDateFromString('2000-02', dateFormat))).toBe(0); + expect( + timeService.calculateDifferenceBetweenDates(stops[stops.length - 1], timeService.getUTCDateFromString('2001-03', dateFormat)), + ).toBe(0); }); }); describe('with mixed time units', () => { const dateFormat = 'YYYY-MM'; - const minimumDate = DayjsUtils.parseUTCDate('2000-01', dateFormat); - const maximumDate = DayjsUtils.parseUTCDate('2001-03', dateFormat); + let minimumDate: Date; + let maximumDate: Date; const parameterSource: TimeSliderParameterSource = { startRangeParameter: '', endRangeParameter: '', layerIdentifiers: [], }; + beforeEach(() => { + minimumDate = timeService.getUTCDateFromString('2000-01', dateFormat); + maximumDate = timeService.getUTCDateFromString('2001-03', dateFormat); + }); + describe('and a range', () => { const range = 'P1M10D'; - const timeSliderConfig: TimeSliderConfiguration = { - name: 'mockTimeSlider', - dateFormat: dateFormat, - minimumDate: DayjsUtils.getUTCDateAsString(minimumDate, dateFormat), - maximumDate: DayjsUtils.getUTCDateAsString(maximumDate, dateFormat), - alwaysMaxRange: false, - range: range, // one month and 10 days - minimalRange: undefined, - sourceType: 'parameter', - source: parameterSource, - }; + let timeSliderConfig: TimeSliderConfiguration; + + beforeEach(() => { + timeSliderConfig = { + name: 'mockTimeSlider', + dateFormat: dateFormat, + minimumDate: timeService.getDateAsUTCString(minimumDate, dateFormat), + maximumDate: timeService.getDateAsUTCString(maximumDate, dateFormat), + alwaysMaxRange: false, + range: range, // one month and 10 days + minimalRange: undefined, + sourceType: 'parameter', + source: parameterSource, + }; + }); it('should create the correct stops', () => { const stops = service.createStops(timeSliderConfig); @@ -340,25 +399,29 @@ describe('TimeSliderService', () => { */ const expectedNumberOfStops = 12; expect(stops.length).toBe(expectedNumberOfStops); - expect(DayjsUtils.calculateDifferenceBetweenDates(stops[0], minimumDate)).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[0], minimumDate)).toBe(0); expect( - DayjsUtils.calculateDifferenceBetweenDates(stops[1], DayjsUtils.addDuration(minimumDate, DayjsUtils.getDuration(range))), + timeService.calculateDifferenceBetweenDates(stops[1], DayjsUtils.addDuration(minimumDate, DayjsUtils.getDuration(range))), ).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(stops[stops.length - 1], maximumDate)).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[stops.length - 1], maximumDate)).toBe(0); }); }); describe('and no range', () => { - const timeSliderConfig: TimeSliderConfiguration = { - name: 'mockTimeSlider', - dateFormat: dateFormat, - minimumDate: DayjsUtils.getUTCDateAsString(minimumDate, dateFormat), - maximumDate: DayjsUtils.getUTCDateAsString(maximumDate, dateFormat), - alwaysMaxRange: false, - range: undefined, - minimalRange: undefined, - sourceType: 'parameter', - source: parameterSource, - }; + let timeSliderConfig: TimeSliderConfiguration; + + beforeEach(() => { + timeSliderConfig = { + name: 'mockTimeSlider', + dateFormat: dateFormat, + minimumDate: timeService.getDateAsUTCString(minimumDate, dateFormat), + maximumDate: timeService.getDateAsUTCString(maximumDate, dateFormat), + alwaysMaxRange: false, + range: undefined, + minimalRange: undefined, + sourceType: 'parameter', + source: parameterSource, + }; + }); it('should create the correct stops', () => { const stops = service.createStops(timeSliderConfig); @@ -370,10 +433,10 @@ describe('TimeSliderService', () => { const expectedNumberOfStops = 15; expect(stops.length).toBe(expectedNumberOfStops); - expect(DayjsUtils.calculateDifferenceBetweenDates(stops[0], DayjsUtils.parseUTCDate('2000-01', dateFormat))).toBe(0); - expect(DayjsUtils.calculateDifferenceBetweenDates(stops[1], DayjsUtils.parseUTCDate('2000-02', dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[0], timeService.getUTCDateFromString('2000-01', dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[1], timeService.getUTCDateFromString('2000-02', dateFormat))).toBe(0); expect( - DayjsUtils.calculateDifferenceBetweenDates(stops[stops.length - 1], DayjsUtils.parseUTCDate('2001-03', dateFormat)), + timeService.calculateDifferenceBetweenDates(stops[stops.length - 1], timeService.getUTCDateFromString('2001-03', dateFormat)), ).toBe(0); }); }); diff --git a/src/app/map/services/time-slider.service.ts b/src/app/map/services/time-slider.service.ts index 8b6288d95..5183685ef 100644 --- a/src/app/map/services/time-slider.service.ts +++ b/src/app/map/services/time-slider.service.ts @@ -1,15 +1,120 @@ -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {TimeSliderConfiguration, TimeSliderLayerSource} from '../../shared/interfaces/topic.interface'; import {Duration} from 'dayjs/plugin/duration'; -import {TimeExtentUtils} from '../../shared/utils/time-extent.utils'; import {TimeExtent} from '../interfaces/time-extent.interface'; import {InvalidTimeSliderConfiguration} from '../../shared/errors/map.errors'; import {DayjsUtils} from '../../shared/utils/dayjs.utils'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../../shared/interfaces/time-service.interface'; +import {ManipulateType} from 'dayjs'; @Injectable({ providedIn: 'root', }) export class TimeSliderService { + constructor(@Inject(TIME_SERVICE) private readonly timeService: TimeService) {} + + /** + * Creates an initial time extent based on the given time slider configuration. + */ + public static createInitialTimeSliderExtent(timeSliderConfig: TimeSliderConfiguration): TimeExtent { + const minimumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maximumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const range: Duration | null = timeSliderConfig.range ? DayjsUtils.getDuration(timeSliderConfig.range) : null; + return { + start: minimumDate, + end: range ? this.addDuration(minimumDate, range) : maximumDate, + }; + } + + /** + * Extracts the unit from the given duration or if it contains values with multiple units. + * + * @remarks + * It does return a unit ('years'/'months'/...) only if the given duration contains values of this unit and nothing else; + * otherwise. + * + * @example + * 'P3Y' is a duration of 3 years. The duration only contains years and therefore this method returns 'years' + * 'P1Y6M' is a duration of 1 year and 6 months. It contains years (1) and months (6) which is a mix of two units. The return value will + * be . + * */ + public static extractUniqueUnitFromDuration(duration: Duration): ManipulateType | undefined { + // todo: this could be a utils class still + if (duration.years() === duration.asYears()) return 'years'; + if (duration.months() === duration.asMonths()) return 'months'; + if (duration.days() === duration.asDays()) return 'days'; + if (duration.hours() === duration.asHours()) return 'hours'; + if (duration.minutes() === duration.asMinutes()) return 'minutes'; + if (duration.seconds() === duration.asSeconds()) return 'seconds'; + if (duration.milliseconds() === duration.asMilliseconds()) return 'milliseconds'; + return undefined; + } + + /** + * Gets the whole given duration as a number value in the desired unit. + */ + public static getDurationAsNumber(duration: Duration, unit: ManipulateType): number { + // todo: this one as well + switch (unit) { + case 'ms': + case 'millisecond': + case 'milliseconds': + return duration.asMilliseconds(); + case 'second': + case 'seconds': + case 's': + return duration.asSeconds(); + case 'minute': + case 'minutes': + case 'm': + return duration.asMinutes(); + case 'hour': + case 'hours': + case 'h': + return duration.asHours(); + case 'd': + case 'D': + case 'day': + case 'days': + return duration.asDays(); + case 'M': + case 'month': + case 'months': + return duration.asMonths(); + case 'y': + case 'year': + case 'years': + return duration.asYears(); + case 'w': + case 'week': + case 'weeks': + return duration.asWeeks(); + } + } + + /** + * Adds the duration to the given date as exact as possible. + * + * @remarks + * It does more than a simple `dayjs(date).add(duration)`. It will add values of a specific unit to the date in case that + * the duration contains only values of one specific unit (e.g. 'years'). This has the advantage that it does not use + * a generic solution which would be 365 days in case of a year. + * + * @example + * addDuration(01.01.2000, duration(1, 'years')) === 01.01.2001 + * while the default way using `dayjs.add` would lead to an error: dayjs(01.01.2000).add(duration(1, 'years')) === 01.01.2000 + 365 days + * === 31.12.2000 + * */ + private static addDuration(date: Date, duration: Duration): Date { + const unit = TimeSliderService.extractUniqueUnitFromDuration(duration); + if (!unit) { + return DayjsUtils.addDuration(date, duration); + } + const value = TimeSliderService.getDurationAsNumber(duration, unit); + return DayjsUtils.addDuration(date, DayjsUtils.getDurationWithUnit(value, unit)); + } + /** * Creates stops which define specific locations on the time slider where thumbs will snap to when manipulated. */ @@ -49,7 +154,7 @@ export class TimeSliderService { => the end date has to be adjusted accordingly to enforce the fixed range between start and end date */ const range: Duration = DayjsUtils.getDuration(timeSliderConfig.range); - timeExtent.end = TimeExtentUtils.addDuration(timeExtent.start, range); + timeExtent.end = TimeSliderService.addDuration(timeExtent.start, range); } else if (timeSliderConfig.minimalRange) { /* Minimal range @@ -70,21 +175,21 @@ export class TimeSliderService { timeExtent.end = startDate; } - const startEndDiff: number = TimeExtentUtils.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end); + const startEndDiff: number = this.timeService.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end); const minimalRange: Duration = DayjsUtils.getDuration(timeSliderConfig.minimalRange); if (startEndDiff < minimalRange.asMilliseconds()) { if (hasStartDateChanged) { - const newStartDate = TimeExtentUtils.subtractDuration(timeExtent.end, minimalRange); + const newStartDate = this.subtractDuration(timeExtent.end, minimalRange); timeExtent.start = this.validateDateWithinLimits(newStartDate, minimumDate, maximumDate); - if (TimeExtentUtils.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end) < minimalRange.asMilliseconds()) { - timeExtent.end = TimeExtentUtils.addDuration(timeExtent.start, minimalRange); + if (this.timeService.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end) < minimalRange.asMilliseconds()) { + timeExtent.end = TimeSliderService.addDuration(timeExtent.start, minimalRange); } } else { - const newEndDate = TimeExtentUtils.addDuration(timeExtent.start, minimalRange); + const newEndDate = TimeSliderService.addDuration(timeExtent.start, minimalRange); timeExtent.end = this.validateDateWithinLimits(newEndDate, minimumDate, maximumDate); - if (TimeExtentUtils.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end) < minimalRange.asMilliseconds()) { - timeExtent.start = TimeExtentUtils.subtractDuration(timeExtent.end, minimalRange); + if (this.timeService.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end) < minimalRange.asMilliseconds()) { + timeExtent.start = this.subtractDuration(timeExtent.end, minimalRange); } } } @@ -94,20 +199,43 @@ export class TimeSliderService { } public isTimeExtentValid(timeSliderConfig: TimeSliderConfiguration, timeExtent: TimeExtent): boolean { - const minDate = DayjsUtils.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maxDate = DayjsUtils.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const minDate = this.timeService.getUTCDateFromString(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maxDate = this.timeService.getUTCDateFromString(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); const updatedTimeExtent: TimeExtent = this.createValidTimeExtent(timeSliderConfig, timeExtent, false, minDate, maxDate); return timeExtent.start.getTime() === updatedTimeExtent.start.getTime() && timeExtent.end.getTime() === updatedTimeExtent.end.getTime(); } + /** + * Extracts a unit from the given date format (ISO8601) if it contains exactly one or if it contains multiple units. + * + * @remarks + * It does return a unit ('years'/'months'/...) only if the given duration contains values of this unit and nothing else; + * otherwise. + * + * @example + * 'YYYY' is a date format containing only years; The unique unit is years and therefore this method returns 'years' + * 'H:m s.SSS' is a date format containing hours, minutes, seconds and milliseconds; there are multiple units therefore this method + * returns 'undefined' + * */ + public extractUniqueUnitFromDateFormat(dateFormat: string): ManipulateType | undefined { + if (dateFormat.replace(/S/g, '').trim() === '') return 'milliseconds'; + if (dateFormat.replace(/s/g, '').trim() === '') return 'seconds'; + if (dateFormat.replace(/m/g, '').trim() === '') return 'minutes'; + if (dateFormat.replace(/[hH]/g, '').trim() === '') return 'hours'; + if (dateFormat.replace(/[dD]/g, '').trim() === '') return 'days'; + if (dateFormat.replace(/M/g, '').trim() === '') return 'months'; + if (dateFormat.replace(/Y/g, '').trim() === '') return 'years'; + return undefined; + } + /** * Creates stops for a layer source containing multiple dates which may not necessarily have constant gaps between them. */ private createStopsForLayerSource(timeSliderConfig: TimeSliderConfiguration): Array { const timeSliderLayerSource = timeSliderConfig.source as TimeSliderLayerSource; - return timeSliderLayerSource.layers.map((layer) => DayjsUtils.parseUTCDate(layer.date, timeSliderConfig.dateFormat)); + return timeSliderLayerSource.layers.map((layer) => this.timeService.getUTCDateFromString(layer.date, timeSliderConfig.dateFormat)); } /** @@ -119,18 +247,18 @@ export class TimeSliderService { * start to finish using the given duration; this can lead to gaps near the end but supports all cases. */ private createStopsForParameterSource(timeSliderConfig: TimeSliderConfiguration): Array { - const minimumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maximumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const minimumDate: Date = this.timeService.getUTCDateFromString(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maximumDate: Date = this.timeService.getUTCDateFromString(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); const initialRange: string | null = timeSliderConfig.range ?? timeSliderConfig.minimalRange ?? null; let stopRangeDuration: Duration | null = initialRange ? DayjsUtils.getDuration(initialRange) : null; if ( stopRangeDuration && - TimeExtentUtils.calculateDifferenceBetweenDates(minimumDate, maximumDate) <= stopRangeDuration.asMilliseconds() + this.timeService.calculateDifferenceBetweenDates(minimumDate, maximumDate) <= stopRangeDuration.asMilliseconds() ) { throw new InvalidTimeSliderConfiguration('min date + range > max date'); } if (!stopRangeDuration) { - const unit = TimeExtentUtils.extractSmallestUnitFromDateFormat(timeSliderConfig.dateFormat); + const unit = this.extractSmallestUnitFromDateFormat(timeSliderConfig.dateFormat); if (!unit) { throw new InvalidTimeSliderConfiguration('Datumsformat sowie minimale Range sind ungültig.'); } @@ -143,12 +271,32 @@ export class TimeSliderService { let date = minimumDate; while (date < maximumDate) { dates.push(date); - date = TimeExtentUtils.addDuration(date, stopRangeDuration); + date = TimeSliderService.addDuration(date, stopRangeDuration); } dates.push(maximumDate); return dates; } + /** + * Extracts the smallest unit from the given date format (ISO8601) or if nothing matches. + * + * @example + * 'YYYY-MM' is a date format containing years and months; The smallest unit is months (months < years) and therefore this method returns + * 'months' + * 'H:m s.SSS' is a date format containing hours, minutes, seconds and milliseconds; The smallest unit is milliseconds and therefore this + * method returns 'milliseconds' + * */ + private extractSmallestUnitFromDateFormat(dateFormat: string): ManipulateType | undefined { + if (dateFormat.includes('SSS')) return 'milliseconds'; + if (dateFormat.includes('s')) return 'seconds'; + if (dateFormat.includes('m')) return 'minutes'; + if (dateFormat.toLowerCase().includes('h')) return 'hours'; // both `h` and `H` are used for `hours` + if (dateFormat.toLowerCase().includes('d')) return 'days'; // both `d` and `D` are used for `days` + if (dateFormat.includes('M')) return 'months'; + if (dateFormat.includes('Y')) return 'years'; + return undefined; + } + /** * Validates that the date is within the given min and max date; returns the date if it is within or the corresponding min/max date * otherwise. @@ -162,4 +310,26 @@ export class TimeSliderService { } return validDate; } + + /** + * Subtracts the duration from the given date as exact as possible. + * + * @remarks + * It does more than a simple `dayjs(date).subtract(duration)`. It will subtract values of a specific unit from the date in case that + * the duration contains only values of one specific unit (e.g. 'years'). This has the advantage that it does not use + * a generic solution which would be 365 days in case of a year. + * + * @example + * subtractDuration(01.01.2001, duration(1, 'years')) === 01.01.2000 + * while the default way using `dayjs.subtract` would lead to an error: dayjs(01.01.2001).subtract(duration(1, 'years')) === 01.01.2001 - + * 365 days === 02.01.2000 + * */ + private subtractDuration(date: Date, duration: Duration): Date { + const unit = TimeSliderService.extractUniqueUnitFromDuration(duration); + if (!unit) { + return DayjsUtils.subtractDuration(date, duration); + } + const value = TimeSliderService.getDurationAsNumber(duration, unit); + return DayjsUtils.subtractDuration(date, DayjsUtils.getDurationWithUnit(value, unit)); + } } diff --git a/src/app/onboarding-guide/services/onboarding-guide.service.ts b/src/app/onboarding-guide/services/onboarding-guide.service.ts index fe2ed60d4..dc7d86d10 100644 --- a/src/app/onboarding-guide/services/onboarding-guide.service.ts +++ b/src/app/onboarding-guide/services/onboarding-guide.service.ts @@ -3,7 +3,6 @@ import {TourService} from 'ngx-ui-tour-md-menu'; import {tap} from 'rxjs'; import {LocalStorageService} from '../../shared/services/local-storage.service'; import {OnboardingGuideConfig} from '../interfaces/onboarding-guide-config.interface'; -import {StorageUtils} from '../../shared/utils/storage.utils'; export const ONBOARDING_STEPS = new InjectionToken('onboardingSteps'); @@ -57,7 +56,7 @@ export class OnboardingGuideService { if (!viewedGuides.includes(this.onboardingGuideId)) { viewedGuides.push(this.onboardingGuideId); - this.localStorageService.set('onboardingGuidesViewed', StorageUtils.stringifyJson(viewedGuides)); + this.localStorageService.set('onboardingGuidesViewed', this.localStorageService.stringifyJson(viewedGuides)); } } diff --git a/src/app/shared/factories/time-service.factory.spec.ts b/src/app/shared/factories/time-service.factory.spec.ts new file mode 100644 index 000000000..3a0f05e79 --- /dev/null +++ b/src/app/shared/factories/time-service.factory.spec.ts @@ -0,0 +1,10 @@ +import {timeServiceFactory} from './time-service.factory'; +import {DayjsService} from '../services/dayjs.service'; + +describe('TimeServiceFactory', () => { + it('returns an instance of dayjs service', () => { + const result = timeServiceFactory(); + + expect(result).toBeInstanceOf(DayjsService); + }); +}); diff --git a/src/app/shared/factories/time-service.factory.ts b/src/app/shared/factories/time-service.factory.ts new file mode 100644 index 000000000..4cdc8b5db --- /dev/null +++ b/src/app/shared/factories/time-service.factory.ts @@ -0,0 +1,5 @@ +import {DayjsService} from '../services/dayjs.service'; + +export function timeServiceFactory() { + return new DayjsService(); +} diff --git a/src/app/shared/interfaces/time-service.interface.ts b/src/app/shared/interfaces/time-service.interface.ts new file mode 100644 index 000000000..087db4190 --- /dev/null +++ b/src/app/shared/interfaces/time-service.interface.ts @@ -0,0 +1,33 @@ +export interface TimeService { + getDateFromString: (date: string, format?: string) => Date; // todo: create a type for the format + + getDateAsFormattedString: (date: Date, format: string) => string; // todo: create a type for the format + + /** + * Converts a date into UTC and returns it as (optionally formatted) string. + * @param date + * @param format + */ + getDateAsUTCString: (date: Date, format?: string) => string; // todo: create a type for the format + + getDateFromUnixTimestamp: (timestamp: number) => Date; + + /** + * Returns the difference between two dates in milliseconds. + */ + calculateDifferenceBetweenDates: (firstDate: Date, secondDate: Date) => number; + + /** + * Given a date and a unit, this method returns the partial value of the given date. + * @param date + * @param unit + */ + getPartialFromString(date: string, unit: DateUnit): number; // todo: create a type for the unit + + isDate: (value: string) => boolean; + + getUTCDateFromString: (date: string, format?: string) => Date; +} + +// base on Dayjs.UnitTypeShort; but it's basically normal iso8601 date units? -> check +export type DateUnit = 'd' | 'D' | 'M' | 'y' | 'h' | 'm' | 's' | 'ms'; diff --git a/src/app/shared/utils/storage.utils.ts b/src/app/shared/services/abstract-storage.service.ts similarity index 55% rename from src/app/shared/utils/storage.utils.ts rename to src/app/shared/services/abstract-storage.service.ts index a49687a92..e6a43a11c 100644 --- a/src/app/shared/utils/storage.utils.ts +++ b/src/app/shared/services/abstract-storage.service.ts @@ -1,12 +1,31 @@ -import {DayjsUtils} from './dayjs.utils'; +import {LocalStorageKey} from '../types/local-storage-key.type'; +import {SessionStorageKey} from '../types/session-storage-key.type'; +import {TimeService} from '../interfaces/time-service.interface'; + +export abstract class AbstractStorageService { + constructor(public readonly timeService: TimeService) {} + + public abstract set(key: T, value: string): void; + + public abstract get(key: T): string | null; + + public abstract remove(key: T): void; + + public parseJson(value: string): JsonType { + return JSON.parse(value, this.reviver); + } + + public stringifyJson(value: JsonType): string { + return JSON.stringify(value); + } -export class StorageUtils { /** * Returns a date in the correct format after parsing from stringified Object. This is used within the JSON.parse method, hence the * "any" type for the value parameter. * - * The comment below refers to the implementation in the DayjsTimeService, which has been created to have all dayjs related methods in one place. - * This comment is only really refering to this specific usecase of the isValidDate method, which is why the comment is not in the DayjsTimeService itself. + * The comment below refers to the implementation in the DayjsTimeService, which has been created to have all dayjs related methods in + * one place. This comment is only really refering to this specific usecase of the isValidDate method, which is why the comment is not in + * the DayjsTimeService itself. * * Note that we're not using the "strict" mode of "dayjs.isValid" here, because we parse a stringified date that was created by using * JSON.stringify(). This uses the ISO8601 format, which looks like YYYY-MM-DDTHH:mm:ss.SSSZ. The strict mode for dayjs() does not work @@ -15,19 +34,11 @@ export class StorageUtils { * that e.g. "12" is valid and parsed as a date. Therefore, we check whether the parsed string's ISO string representation is equal to * the original string. If it is, we return the parsed date, otherwise the original string (see GB3-1597). */ - private static reviver(key: string, value: any): any { - if (typeof value === 'string' && DayjsUtils.isValidDate(value)) { - const parsed = DayjsUtils.parseUTCDate(value); + private reviver(key: string, value: any): any { + if (typeof value === 'string' && this.timeService.isDate(value)) { + const parsed = this.timeService.getUTCDateFromString(value); return parsed.toISOString() === value ? parsed : value; } return value; } - - public static parseJson(value: string): T { - return JSON.parse(value, StorageUtils.reviver); - } - - public static stringifyJson(value: T): string { - return JSON.stringify(value); - } } diff --git a/src/app/shared/services/apis/abstract-api.service.ts b/src/app/shared/services/apis/abstract-api.service.ts index 606b39caa..755bb920a 100644 --- a/src/app/shared/services/apis/abstract-api.service.ts +++ b/src/app/shared/services/apis/abstract-api.service.ts @@ -1,7 +1,9 @@ import {HttpClient} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {Observable} from 'rxjs'; import {ConfigService} from '../config.service'; +import {TimeService} from '../../interfaces/time-service.interface'; +import {TIME_SERVICE} from '../../../app.module'; @Injectable({ providedIn: 'root', @@ -12,6 +14,7 @@ export abstract class BaseApiService { constructor( private readonly http: HttpClient, protected readonly configService: ConfigService, + @Inject(TIME_SERVICE) protected readonly timeService: TimeService, ) {} protected get(url: string): Observable { diff --git a/src/app/shared/services/apis/gb3/gb3-export.service.ts b/src/app/shared/services/apis/gb3/gb3-export.service.ts index f1ac9d20c..6993bf635 100644 --- a/src/app/shared/services/apis/gb3/gb3-export.service.ts +++ b/src/app/shared/services/apis/gb3/gb3-export.service.ts @@ -6,6 +6,8 @@ import {ConfigService} from '../../config.service'; import {map} from 'rxjs/operators'; import {ExportFormat} from '../../../enums/export-format.enum'; import {FileDownloadService} from '../../file-download-service'; +import {TIME_SERVICE} from '../../../../app.module'; +import {TimeService} from 'src/app/shared/interfaces/time-service.interface'; @Injectable({ providedIn: 'root', @@ -16,9 +18,10 @@ export class Gb3ExportService extends Gb3ApiService { constructor( @Inject(HttpClient) http: HttpClient, @Inject(ConfigService) configService: ConfigService, + @Inject(TIME_SERVICE) timeService: TimeService, private readonly fileDownloadService: FileDownloadService, ) { - super(http, configService); + super(http, configService, timeService); } public exportDrawing(exportFormat: ExportFormat, drawings: Gb3VectorLayer) { diff --git a/src/app/shared/services/apis/gb3/gb3-favourites.service.ts b/src/app/shared/services/apis/gb3/gb3-favourites.service.ts index ec1d7ebff..e305ddc33 100644 --- a/src/app/shared/services/apis/gb3/gb3-favourites.service.ts +++ b/src/app/shared/services/apis/gb3/gb3-favourites.service.ts @@ -10,7 +10,6 @@ import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {CreateFavourite, Favourite, FavouritesResponse} from '../../../interfaces/favourite.interface'; import {ApiGeojsonGeometryToGb3ConverterUtils} from '../../../utils/api-geojson-geometry-to-gb3-converter.utils'; -import {DayjsUtils} from '../../../utils/dayjs.utils'; @Injectable({ providedIn: 'root', @@ -67,8 +66,8 @@ export class Gb3FavouritesService extends Gb3ApiService { attributeFilters: content.attributeFilters, timeExtent: content.timeExtent ? { - start: DayjsUtils.parseUTCDate(content.timeExtent.start), - end: DayjsUtils.parseUTCDate(content.timeExtent.end), + start: this.timeService.getUTCDateFromString(content.timeExtent.start), + end: this.timeService.getUTCDateFromString(content.timeExtent.end), } : undefined, }; diff --git a/src/app/shared/services/apis/gb3/gb3-import.service.ts b/src/app/shared/services/apis/gb3/gb3-import.service.ts index cec4a2e85..3f1871a80 100644 --- a/src/app/shared/services/apis/gb3/gb3-import.service.ts +++ b/src/app/shared/services/apis/gb3/gb3-import.service.ts @@ -3,6 +3,8 @@ import {Inject, Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {ConfigService} from '../../config.service'; import {Gb3VectorLayer} from '../../../interfaces/gb3-vector-layer.interface'; +import {TimeService} from '../../../interfaces/time-service.interface'; +import {TIME_SERVICE} from '../../../../app.module'; @Injectable({ providedIn: 'root', @@ -10,8 +12,12 @@ import {Gb3VectorLayer} from '../../../interfaces/gb3-vector-layer.interface'; export class Gb3ImportService extends Gb3ApiService { protected readonly endpoint = 'import'; - constructor(@Inject(HttpClient) http: HttpClient, @Inject(ConfigService) configService: ConfigService) { - super(http, configService); + constructor( + @Inject(HttpClient) http: HttpClient, + @Inject(ConfigService) configService: ConfigService, + @Inject(TIME_SERVICE) timeService: TimeService, + ) { + super(http, configService, timeService); } public importDrawing(file: File | Blob) { diff --git a/src/app/shared/services/apis/gb3/gb3-print.service.ts b/src/app/shared/services/apis/gb3/gb3-print.service.ts index 922c56b97..247ae251f 100644 --- a/src/app/shared/services/apis/gb3/gb3-print.service.ts +++ b/src/app/shared/services/apis/gb3/gb3-print.service.ts @@ -22,6 +22,8 @@ import {Gb3StyledInternalDrawingRepresentation} from '../../../interfaces/intern import {PrintData} from '../../../../map/interfaces/print-data.interface'; import {Gb2WmsSettings} from '../../../../map/models/implementations/gb2-wms.model'; import {DrawingLayerSettings} from '../../../../map/models/implementations/drawing.model'; +import {TIME_SERVICE} from '../../../../app.module'; +import {TimeService} from '../../../interfaces/time-service.interface'; @Injectable({ providedIn: 'root', @@ -32,9 +34,10 @@ export class Gb3PrintService extends Gb3ApiService { constructor( @Inject(HttpClient) http: HttpClient, @Inject(ConfigService) configService: ConfigService, + @Inject(TIME_SERVICE) timeService: TimeService, private readonly basemapConfigService: BasemapConfigService, ) { - super(http, configService); + super(http, configService, timeService); } public createPrintJob(printCreation: PrintCreation): Observable { diff --git a/src/app/shared/services/apis/gb3/gb3-share-link.service.ts b/src/app/shared/services/apis/gb3/gb3-share-link.service.ts index 234d7a0ca..0b1777a8c 100644 --- a/src/app/shared/services/apis/gb3/gb3-share-link.service.ts +++ b/src/app/shared/services/apis/gb3/gb3-share-link.service.ts @@ -11,7 +11,8 @@ import {HttpClient} from '@angular/common/http'; import {BasemapConfigService} from '../../../../map/services/basemap-config.service'; import {FavouritesService} from '../../../../map/services/favourites.service'; import {MapRestoreItem} from '../../../interfaces/map-restore-item.interface'; -import {DayjsUtils} from '../../../utils/dayjs.utils'; +import {TimeService} from '../../../interfaces/time-service.interface'; +import {TIME_SERVICE} from '../../../../app.module'; @Injectable({ providedIn: 'root', @@ -22,10 +23,11 @@ export class Gb3ShareLinkService extends Gb3ApiService { constructor( @Inject(HttpClient) http: HttpClient, @Inject(ConfigService) configService: ConfigService, + @Inject(TIME_SERVICE) timeService: TimeService, private readonly basemapConfigService: BasemapConfigService, private readonly favouritesService: FavouritesService, ) { - super(http, configService); + super(http, configService, timeService); } public loadShareLink(shareLinkId: string): Observable { @@ -93,8 +95,8 @@ export class Gb3ShareLinkService extends Gb3ApiService { attributeFilters: content.attributeFilters, timeExtent: content.timeExtent ? { - start: DayjsUtils.parseUTCDate(content.timeExtent.start), - end: DayjsUtils.parseUTCDate(content.timeExtent.end), + start: this.timeService.getUTCDateFromString(content.timeExtent.start), + end: this.timeService.getUTCDateFromString(content.timeExtent.end), } : undefined, }; diff --git a/src/app/shared/services/apis/grav-cms/grav-cms.service.ts b/src/app/shared/services/apis/grav-cms/grav-cms.service.ts index f3310c53e..7fb57856a 100644 --- a/src/app/shared/services/apis/grav-cms/grav-cms.service.ts +++ b/src/app/shared/services/apis/grav-cms/grav-cms.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {BaseApiService} from '../abstract-api.service'; import {Observable} from 'rxjs'; import {DiscoverMapsItem} from '../../../interfaces/discover-maps-item.interface'; @@ -9,7 +9,8 @@ import {MainPage} from '../../../enums/main-page.enum'; import {FrequentlyUsedItem} from '../../../interfaces/frequently-used-item.interface'; import {HttpClient} from '@angular/common/http'; import {ConfigService} from '../../config.service'; -import {DayjsUtils} from '../../../utils/dayjs.utils'; +import {TimeService} from '../../../interfaces/time-service.interface'; +import {TIME_SERVICE} from '../../../../app.module'; const DATE_FORMAT = 'DD.MM.YYYY'; @@ -22,8 +23,8 @@ export class GravCmsService extends BaseApiService { private readonly pageInfosEndpoint: string = 'pageinfos.json'; private readonly frequentlyUsedItemsEndpoint: string = 'frequentlyused.json'; - constructor(httpClient: HttpClient, configService: ConfigService) { - super(httpClient, configService); + constructor(httpClient: HttpClient, configService: ConfigService, @Inject(TIME_SERVICE) timeService: TimeService) { + super(httpClient, configService, timeService); } public loadDiscoverMapsData(): Observable { const requestUrl = this.createFullEndpointUrl(this.discoverMapsEndpoint); @@ -47,8 +48,8 @@ export class GravCmsService extends BaseApiService { title: discoverMapData.title, description: discoverMapData.description, mapId: discoverMapData.id, - fromDate: DayjsUtils.getDate(discoverMapData.from_date, DATE_FORMAT), - toDate: DayjsUtils.getDate(discoverMapData.to_date, DATE_FORMAT), + fromDate: this.timeService.getDateFromString(discoverMapData.from_date, DATE_FORMAT), + toDate: this.timeService.getDateFromString(discoverMapData.to_date, DATE_FORMAT), image: { url: this.createFullImageUrl(discoverMapData.image.path), name: discoverMapData.image.name, @@ -68,8 +69,8 @@ export class GravCmsService extends BaseApiService { title: pageInfoData.title, description: pageInfoData.description, pages: this.transformPagesToMainPages(pageInfoData.pages), - fromDate: DayjsUtils.getDate(pageInfoData.from_date, DATE_FORMAT), - toDate: DayjsUtils.getDate(pageInfoData.to_date, DATE_FORMAT), + fromDate: this.timeService.getDateFromString(pageInfoData.from_date, DATE_FORMAT), + toDate: this.timeService.getDateFromString(pageInfoData.to_date, DATE_FORMAT), severity: pageInfoData.severity as PageNotificationSeverity, isMarkedAsRead: false, }; @@ -93,7 +94,7 @@ export class GravCmsService extends BaseApiService { altText: frequentlyUsedData.image_alt, } : undefined, - created: DayjsUtils.getUnixDate(+frequentlyUsedData.created), + created: this.timeService.getDateFromUnixTimestamp(Number(frequentlyUsedData.created)), }; }); } diff --git a/src/app/shared/services/dayjs.service.spec.ts b/src/app/shared/services/dayjs.service.spec.ts new file mode 100644 index 000000000..bd0289c25 --- /dev/null +++ b/src/app/shared/services/dayjs.service.spec.ts @@ -0,0 +1,70 @@ +import {TestBed} from '@angular/core/testing'; +import {TIME_SERVICE} from '../../app.module'; +import {DayjsService} from './dayjs.service'; + +describe('DayjsService', () => { + let dayjsService: DayjsService; + // todo override + beforeEach(() => { + TestBed.configureTestingModule({providers: [{provide: TIME_SERVICE, useClass: DayjsService}]}); + dayjsService = TestBed.inject(TIME_SERVICE) as DayjsService; + }); + + it('should be created', () => { + expect(dayjsService).toBeTruthy(); + }); + + describe('getDateFromString', () => { + it('returns the date object from a string', () => { + expect(dayjsService.getDateFromString('2023-10-01')).toEqual(new Date(2023, 9, 1)); + }); + + it('returns the date object from a string with a format', () => { + expect(dayjsService.getDateFromString('2023-10-01', 'YYYY-MM-DD')).toEqual(new Date(2023, 9, 1)); + }); + }); + + describe('getDateAsFormattedString', () => { + it('returns the date as a formatted string', () => { + const date = new Date(2023, 9, 1); // October 1, 2023 + expect(dayjsService.getDateAsFormattedString(date, 'YYYY-MM-DD')).toBe('2023-10-01'); + }); + }); + + describe('getDateAsUTCString', () => { + it('returns the UTC date as a formatted string', () => { + const date = new Date(Date.UTC(2023, 9, 1)); // October 1, 2023 UTC + expect(dayjsService.getDateAsUTCString(date, 'YYYY-MM-DD')).toBe('2023-10-01'); + }); + }); + + describe('getUnixDate', () => { + it('returns the date object from a Unix timestamp', () => { + const expectedDate = new Date(Date.UTC(2000, 0, 1)); + // 946684800 is the Unix timestamp for 2000-01-01T00:00:00.000Z (from https://timestampgenerator.com/946684800/+00:00) + expect(dayjsService.getDateFromUnixTimestamp(946684800).getTime()).toEqual(expectedDate.getTime()); + }); + }); + + describe('getUTCDateFromString', () => { + it('parses the UTC date from a string', () => { + expect(dayjsService.getUTCDateFromString('2023-10-01', 'YYYY-MM-DD')).toEqual(new Date(Date.UTC(2023, 9, 1))); + expect(dayjsService.getUTCDateFromString('2023-10-01')).toEqual(new Date(Date.UTC(2023, 9, 1))); + }); + }); + + describe('isDate', () => { + it('validates the date string', () => { + expect(dayjsService.isDate('2023-10-01')).toBe(true); + expect(dayjsService.isDate('invalid-date')).toBe(false); + }); + }); + + describe('calculateDifferenceBetweenDates', () => { + it('calculates the difference between two dates', () => { + const date1 = new Date(2023, 9, 1); + const date2 = new Date(2023, 9, 2); + expect(dayjsService.calculateDifferenceBetweenDates(date1, date2)).toBe(86400000); // 1 day in milliseconds + }); + }); +}); diff --git a/src/app/shared/services/dayjs.service.ts b/src/app/shared/services/dayjs.service.ts new file mode 100644 index 000000000..e2fe41979 --- /dev/null +++ b/src/app/shared/services/dayjs.service.ts @@ -0,0 +1,48 @@ +import {Injectable} from '@angular/core'; +import {DateUnit, TimeService} from '../interfaces/time-service.interface'; +import dayjs from 'dayjs'; + +@Injectable({ + providedIn: 'root', +}) +export class DayjsService implements TimeService { + public getDateFromString(date: string, format?: string): Date { + return this.createDayjsObject(date, format).toDate(); + } + + public getDateAsFormattedString(date: Date, format: string): string { + return this.createDayjsObject(date).format(format); + } + + public getDateAsUTCString(date: Date, format?: string): string { + return this.createUTCDayjsObject(date).format(format); + } + + public getPartialFromString(date: string, unit: DateUnit): number { + return this.createDayjsObject(date).get(unit); + } + + public getDateFromUnixTimestamp(timestamp: number): Date { + return dayjs.unix(timestamp).toDate(); + } + + public getUTCDateFromString(date: string, format?: string): Date { + return this.createUTCDayjsObject(date, format).toDate(); + } + + public isDate(value: string): boolean { + return this.createDayjsObject(value).isValid(); + } + + public calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number { + return Math.abs(this.createDayjsObject(secondDate).diff(this.createDayjsObject(firstDate))); + } + + private createDayjsObject(date: Date | string, format?: string): dayjs.Dayjs { + return dayjs(date, format); + } + + private createUTCDayjsObject(date: Date | string, format?: string): dayjs.Dayjs { + return dayjs.utc(date, format); + } +} diff --git a/src/app/shared/services/local-storage.service.ts b/src/app/shared/services/local-storage.service.ts index 4f9bc169f..c0bef251d 100644 --- a/src/app/shared/services/local-storage.service.ts +++ b/src/app/shared/services/local-storage.service.ts @@ -1,10 +1,17 @@ -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {LocalStorageKey} from '../types/local-storage-key.type'; +import {AbstractStorageService} from './abstract-storage.service'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../interfaces/time-service.interface'; @Injectable({ providedIn: 'root', }) -export class LocalStorageService { +export class LocalStorageService extends AbstractStorageService { + constructor(@Inject(TIME_SERVICE) timeService: TimeService) { + super(timeService); + } + public set(key: LocalStorageKey, value: string) { localStorage.setItem(key, value); } @@ -12,4 +19,8 @@ export class LocalStorageService { public get(key: LocalStorageKey): string | null { return localStorage.getItem(key); } + + public remove(key: LocalStorageKey) { + localStorage.removeItem(key); + } } diff --git a/src/app/shared/services/session-storage.service.ts b/src/app/shared/services/session-storage.service.ts index 77618e08b..c99b83507 100644 --- a/src/app/shared/services/session-storage.service.ts +++ b/src/app/shared/services/session-storage.service.ts @@ -1,10 +1,17 @@ -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {SessionStorageKey} from '../types/session-storage-key.type'; +import {AbstractStorageService} from './abstract-storage.service'; +import {TIME_SERVICE} from '../../app.module'; +import {TimeService} from '../interfaces/time-service.interface'; @Injectable({ providedIn: 'root', }) -export class SessionStorageService { +export class SessionStorageService extends AbstractStorageService { + constructor(@Inject(TIME_SERVICE) timeService: TimeService) { + super(timeService); + } + public set(key: SessionStorageKey, value: string) { sessionStorage.setItem(key, value); } diff --git a/src/app/shared/utils/dayjs.utils.spec.ts b/src/app/shared/utils/dayjs.utils.spec.ts index bcc5892c7..02bcdfb67 100644 --- a/src/app/shared/utils/dayjs.utils.spec.ts +++ b/src/app/shared/utils/dayjs.utils.spec.ts @@ -9,42 +9,6 @@ describe('DayjsUtils', () => { }); }); - describe('getDateAsString', () => { - it('returns the date as a formatted string', () => { - const date = new Date(2023, 9, 1); // October 1, 2023 - expect(DayjsUtils.getDateAsString(date, 'YYYY-MM-DD')).toBe('2023-10-01'); - }); - }); - - describe('getDate', () => { - it('returns the date object from a string', () => { - expect(DayjsUtils.getDate('2023-10-01', 'YYYY-MM-DD')).toEqual(new Date(2023, 9, 1)); - expect(DayjsUtils.getDate('2023-10-01')).toEqual(new Date(2023, 9, 1)); - }); - }); - - describe('getUTCDateAsString', () => { - it('returns the UTC date as a formatted string', () => { - const date = new Date(Date.UTC(2023, 9, 1)); // October 1, 2023 UTC - expect(DayjsUtils.getUTCDateAsString(date, 'YYYY-MM-DD')).toBe('2023-10-01'); - }); - }); - - describe('getUnixDate', () => { - it('returns the date object from a Unix timestamp', () => { - const expectedDate = new Date(Date.UTC(2000, 0, 1)); - // 946684800 is the Unix timestamp for 2000-01-01T00:00:00.000Z (from https://timestampgenerator.com/946684800/+00:00) - expect(DayjsUtils.getUnixDate(946684800).getTime()).toEqual(expectedDate.getTime()); - }); - }); - - describe('parseUTCDate', () => { - it('parses the UTC date from a string', () => { - expect(DayjsUtils.parseUTCDate('2023-10-01', 'YYYY-MM-DD')).toEqual(new Date(Date.UTC(2023, 9, 1))); - expect(DayjsUtils.parseUTCDate('2023-10-01')).toEqual(new Date(Date.UTC(2023, 9, 1))); - }); - }); - describe('getDuration', () => { it('returns the duration object from a time string', () => { expect(DayjsUtils.getDuration('P1D').asSeconds()).toBe(86400); @@ -57,13 +21,6 @@ describe('DayjsUtils', () => { }); }); - describe('isValidDate', () => { - it('validates the date string', () => { - expect(DayjsUtils.isValidDate('2023-10-01')).toBe(true); - expect(DayjsUtils.isValidDate('invalid-date')).toBe(false); - }); - }); - describe('addDuration', () => { it('adds the duration to the date', () => { const date = new Date(2023, 9, 1); @@ -79,12 +36,4 @@ describe('DayjsUtils', () => { expect(DayjsUtils.subtractDuration(date, duration)).toEqual(new Date(2023, 8, 30)); }); }); - - describe('calculateDifferenceBetweenDates', () => { - it('calculates the difference between two dates', () => { - const date1 = new Date(2023, 9, 1); - const date2 = new Date(2023, 9, 2); - expect(DayjsUtils.calculateDifferenceBetweenDates(date1, date2)).toBe(86400000); // 1 day in milliseconds - }); - }); }); diff --git a/src/app/shared/utils/dayjs.utils.ts b/src/app/shared/utils/dayjs.utils.ts index 4def7b034..e62f793f4 100644 --- a/src/app/shared/utils/dayjs.utils.ts +++ b/src/app/shared/utils/dayjs.utils.ts @@ -12,18 +12,6 @@ export class DayjsUtils { public static getPartial(date: string, unit: UnitType): number { return dayjs(date).get(unit); } - public static getDateAsString(date: Date, format: string): string { - return dayjs(date).format(format); - } - public static getDate(date: string, format?: string): Date { - return format ? dayjs(date, format).toDate() : dayjs(date).toDate(); - } - public static getUTCDateAsString(date: Date, format?: string): string { - return dayjs.utc(date).format(format); - } - public static getUnixDate(created: number): Date { - return dayjs.unix(created).toDate(); - } public static parseUTCDate(date: string, format?: string): Date { return dayjs.utc(date, format).toDate(); } @@ -33,16 +21,11 @@ export class DayjsUtils { public static getDurationWithUnit(time: number, unit?: ManipulateType): Duration { return dayjs.duration(time, unit); } - public static isValidDate(value: string): boolean { - return dayjs(value).isValid(); - } + public static addDuration(date: Date, durationToAdd: Duration): Date { return dayjs(date).add(durationToAdd).toDate(); } public static subtractDuration(date: Date, durationToSubtract: Duration): Date { return dayjs(date).subtract(durationToSubtract).toDate(); } - public static calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number { - return Math.abs(dayjs(secondDate).diff(dayjs(firstDate))); - } } diff --git a/src/app/shared/utils/storage.utils.spec.ts b/src/app/shared/utils/storage.utils.spec.ts deleted file mode 100644 index 95f00e943..000000000 --- a/src/app/shared/utils/storage.utils.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {StorageUtils} from './storage.utils'; -import {DayjsUtils} from './dayjs.utils'; - -describe('StorageUtils', () => { - describe('parseJson', () => { - it(`parses a Json with valid Dates correctly`, () => { - const stringToParse = - '{"date":"1506-01-01T00:00:00.000Z", "number": 2683132, "string": "test", "stringifiedNumberParseableAsDate": "12"}'; - - const expectedJsonObject = { - date: DayjsUtils.parseUTCDate('1506-01-01T00:00:00.000Z'), - number: 2683132, - string: 'test', - stringifiedNumberParseableAsDate: '12', - }; - expect(StorageUtils.parseJson(stringToParse)).toEqual(expectedJsonObject); - }); - it(`does not parses an invalid Date-String `, () => { - const stringToParse = '{"invalidDate":"T1506-01-01T00:00:00.000Z"}'; - - const expectedJsonObject = {invalidDate: 'T1506-01-01T00:00:00.000Z'}; - expect(StorageUtils.parseJson(stringToParse)).toEqual(expectedJsonObject); - }); - it(`does not parses a number as Date`, () => { - const stringToParse = '{"validDateAsNumber":19000101}'; - - const expectedJsonObject = {validDateAsNumber: 19000101}; - expect(StorageUtils.parseJson(stringToParse)).toEqual(expectedJsonObject); - }); - }); -}); diff --git a/src/app/shared/utils/time-extent.utils.ts b/src/app/shared/utils/time-extent.utils.ts deleted file mode 100644 index adbb3f355..000000000 --- a/src/app/shared/utils/time-extent.utils.ts +++ /dev/null @@ -1,178 +0,0 @@ -import {Duration} from 'dayjs/plugin/duration'; -import {ManipulateTypeAlias as ManipulateType} from '../types/dayjs-alias-type'; -import {TimeSliderConfiguration} from '../interfaces/topic.interface'; -import {TimeExtent} from '../../map/interfaces/time-extent.interface'; -import {DayjsUtils} from './dayjs.utils'; - -export class TimeExtentUtils { - /** - * Creates an initial time extent based on the given time slider configuration. - */ - public static createInitialTimeSliderExtent(timeSliderConfig: TimeSliderConfiguration): TimeExtent { - const minimumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maximumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); - const range: Duration | null = timeSliderConfig.range ? DayjsUtils.getDuration(timeSliderConfig.range) : null; - return { - start: minimumDate, - end: range ? TimeExtentUtils.addDuration(minimumDate, range) : maximumDate, - }; - } - - /** - * Extracts the unit from the given duration or if it contains values with multiple units. - * - * @remarks - * It does return a unit ('years'/'months'/...) only if the given duration contains values of this unit and nothing else; - * otherwise. - * - * @example - * 'P3Y' is a duration of 3 years. The duration only contains years and therefore this method returns 'years' - * 'P1Y6M' is a duration of 1 year and 6 months. It contains years (1) and months (6) which is a mix of two units. The return value will - * be . - * */ - public static extractUniqueUnitFromDuration(duration: Duration): ManipulateType | undefined { - if (duration.years() === duration.asYears()) return 'years'; - if (duration.months() === duration.asMonths()) return 'months'; - if (duration.days() === duration.asDays()) return 'days'; - if (duration.hours() === duration.asHours()) return 'hours'; - if (duration.minutes() === duration.asMinutes()) return 'minutes'; - if (duration.seconds() === duration.asSeconds()) return 'seconds'; - if (duration.milliseconds() === duration.asMilliseconds()) return 'milliseconds'; - return undefined; - } - - /** - * Extracts a unit from the given date format (ISO8601) if it contains exactly one or if it contains multiple units. - * - * @remarks - * It does return a unit ('years'/'months'/...) only if the given duration contains values of this unit and nothing else; - * otherwise. - * - * @example - * 'YYYY' is a date format containing only years; The unique unit is years and therefore this method returns 'years' - * 'H:m s.SSS' is a date format containing hours, minutes, seconds and milliseconds; there are multiple units therefore this method - * returns 'undefined' - * */ - public static extractUniqueUnitFromDateFormat(dateFormat: string): ManipulateType | undefined { - if (dateFormat.replace(/S/g, '').trim() === '') return 'milliseconds'; - if (dateFormat.replace(/s/g, '').trim() === '') return 'seconds'; - if (dateFormat.replace(/m/g, '').trim() === '') return 'minutes'; - if (dateFormat.replace(/[hH]/g, '').trim() === '') return 'hours'; - if (dateFormat.replace(/[dD]/g, '').trim() === '') return 'days'; - if (dateFormat.replace(/M/g, '').trim() === '') return 'months'; - if (dateFormat.replace(/Y/g, '').trim() === '') return 'years'; - return undefined; - } - - /** - * Extracts the smallest unit from the given date format (ISO8601) or if nothing matches. - * - * @example - * 'YYYY-MM' is a date format containing years and months; The smallest unit is months (months < years) and therefore this method returns - * 'months' - * 'H:m s.SSS' is a date format containing hours, minutes, seconds and milliseconds; The smallest unit is milliseconds and therefore this - * method returns 'milliseconds' - * */ - public static extractSmallestUnitFromDateFormat(dateFormat: string): ManipulateType | undefined { - if (dateFormat.includes('SSS')) return 'milliseconds'; - if (dateFormat.includes('s')) return 'seconds'; - if (dateFormat.includes('m')) return 'minutes'; - if (dateFormat.toLowerCase().includes('h')) return 'hours'; // both `h` and `H` are used for `hours` - if (dateFormat.toLowerCase().includes('d')) return 'days'; // both `d` and `D` are used for `days` - if (dateFormat.includes('M')) return 'months'; - if (dateFormat.includes('Y')) return 'years'; - return undefined; - } - - /** - * Adds the duration to the given date as exact as possible. - * - * @remarks - * It does more than a simple `dayjs(date).add(duration)`. It will add values of a specific unit to the date in case that - * the duration contains only values of one specific unit (e.g. 'years'). This has the advantage that it does not use - * a generic solution which would be 365 days in case of a year. - * - * @example - * addDuration(01.01.2000, duration(1, 'years')) === 01.01.2001 - * while the default way using `dayjs.add` would lead to an error: dayjs(01.01.2000).add(duration(1, 'years')) === 01.01.2000 + 365 days - * === 31.12.2000 - * */ - public static addDuration(date: Date, duration: Duration): Date { - const unit = TimeExtentUtils.extractUniqueUnitFromDuration(duration); - if (!unit) { - return DayjsUtils.addDuration(date, duration); - } - const value = TimeExtentUtils.getDurationAsNumber(duration, unit); - return DayjsUtils.addDuration(date, DayjsUtils.getDurationWithUnit(value, unit)); - } - - /** - * Subtracts the duration from the given date as exact as possible. - * - * @remarks - * It does more than a simple `dayjs(date).subtract(duration)`. It will subtract values of a specific unit from the date in case that - * the duration contains only values of one specific unit (e.g. 'years'). This has the advantage that it does not use - * a generic solution which would be 365 days in case of a year. - * - * @example - * subtractDuration(01.01.2001, duration(1, 'years')) === 01.01.2000 - * while the default way using `dayjs.subtract` would lead to an error: dayjs(01.01.2001).subtract(duration(1, 'years')) === 01.01.2001 - - * 365 days === 02.01.2000 - * */ - public static subtractDuration(date: Date, duration: Duration): Date { - const unit = TimeExtentUtils.extractUniqueUnitFromDuration(duration); - if (!unit) { - return DayjsUtils.subtractDuration(date, duration); - } - const value = TimeExtentUtils.getDurationAsNumber(duration, unit); - return DayjsUtils.subtractDuration(date, DayjsUtils.getDurationWithUnit(value, unit)); - } - - /** - * Gets the whole given duration as a number value in the desired unit. - */ - public static getDurationAsNumber(duration: Duration, unit: ManipulateType): number { - switch (unit) { - case 'ms': - case 'millisecond': - case 'milliseconds': - return duration.asMilliseconds(); - case 'second': - case 'seconds': - case 's': - return duration.asSeconds(); - case 'minute': - case 'minutes': - case 'm': - return duration.asMinutes(); - case 'hour': - case 'hours': - case 'h': - return duration.asHours(); - case 'd': - case 'D': - case 'day': - case 'days': - return duration.asDays(); - case 'M': - case 'month': - case 'months': - return duration.asMonths(); - case 'y': - case 'year': - case 'years': - return duration.asYears(); - case 'w': - case 'week': - case 'weeks': - return duration.asWeeks(); - } - } - - /** - * Returns the difference in milliseconds between the two given dates. - */ - public static calculateDifferenceBetweenDates(firstDate: Date, secondDate: Date): number { - return DayjsUtils.calculateDifferenceBetweenDates(firstDate, secondDate); - } -} diff --git a/src/app/state/auth/effects/auth-status.effects.spec.ts b/src/app/state/auth/effects/auth-status.effects.spec.ts index a2478b255..b1ef4b2ab 100644 --- a/src/app/state/auth/effects/auth-status.effects.spec.ts +++ b/src/app/state/auth/effects/auth-status.effects.spec.ts @@ -15,7 +15,7 @@ import {selectActiveMapItemConfigurations} from '../../map/selectors/active-map- import {selectMaps} from '../../map/selectors/maps.selector'; import {selectFavouriteBaseConfig} from '../../map/selectors/favourite-base-config.selector'; import {selectUserDrawingsVectorLayers} from '../../map/selectors/user-drawings-vector-layers.selector'; -import {MAP_SERVICE} from '../../../app.module'; +import {MAP_SERVICE, TIME_SERVICE} from '../../../app.module'; import {MapServiceStub} from '../../../testing/map-testing/map.service.stub'; import {LayerCatalogActions} from '../../map/actions/layer-catalog.actions'; import {Gb3ShareLinkService} from '../../../shared/services/apis/gb3/gb3-share-link.service'; @@ -29,6 +29,7 @@ import {selectItems} from '../../map/selectors/active-map-items.selector'; import {selectDrawings} from '../../map/reducers/drawing.reducer'; import {ToolService} from '../../../map/interfaces/tool.service'; import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; +import {TimeService} from '../../../shared/interfaces/time-service.interface'; const mockOAuthService = jasmine.createSpyObj({ logout: void 0, @@ -40,6 +41,7 @@ describe('AuthStatusEffects', () => { let store: MockStore; let effects: AuthStatusEffects; let storageService: SessionStorageService; + let timeService: TimeService; beforeEach(() => { actions$ = new Observable(); @@ -56,6 +58,7 @@ describe('AuthStatusEffects', () => { provideHttpClientTesting(), ], }); + timeService = TestBed.inject(TIME_SERVICE); storageService = TestBed.inject(SessionStorageService); store = TestBed.inject(MockStore); store.overrideSelector(selectActiveMapItemConfigurations, []); @@ -74,7 +77,10 @@ describe('AuthStatusEffects', () => { describe('login$', () => { it('logins using the AuthService and stores the current map state into a share link item; dispatches no further actions', (done: DoneFn) => { - const shareLinkItem: ShareLinkItem = ShareLinkItemTestUtils.createShareLinkItem(); + const shareLinkItem: ShareLinkItem = ShareLinkItemTestUtils.createShareLinkItem( + timeService.getUTCDateFromString('1000'), + timeService.getUTCDateFromString('2020'), + ); const storageServiceSpy = spyOn(storageService, 'set'); store.overrideSelector(selectCurrentShareLinkItem, shareLinkItem); @@ -93,7 +99,10 @@ describe('AuthStatusEffects', () => { describe('logout$', () => { it('logouts using the AuthService and stores the current map state into a share link item; dispatches no further actions', (done: DoneFn) => { - const shareLinkItem: ShareLinkItem = ShareLinkItemTestUtils.createShareLinkItem(); + const shareLinkItem: ShareLinkItem = ShareLinkItemTestUtils.createShareLinkItem( + timeService.getUTCDateFromString('1000'), + timeService.getUTCDateFromString('2020'), + ); const storageServiceSpy = spyOn(storageService, 'set'); store.overrideSelector(selectCurrentShareLinkItem, shareLinkItem); const isForced = true; @@ -116,7 +125,10 @@ describe('AuthStatusEffects', () => { 'dispatches AuthStatusActions.completeRestoreApplication after setting the layer catalog' + 'and loading an existing share link item from the session storage.', (done: DoneFn) => { - const shareLinkItem: ShareLinkItem = ShareLinkItemTestUtils.createShareLinkItem(); + const shareLinkItem: ShareLinkItem = ShareLinkItemTestUtils.createShareLinkItem( + timeService.getUTCDateFromString('1000'), + timeService.getUTCDateFromString('2020'), + ); const shareLinkItemString = JSON.stringify(shareLinkItem); const storageServiceGetSpy = spyOn(storageService, 'get').and.returnValue(shareLinkItemString); const storageServiceRemoveSpy = spyOn(storageService, 'remove').and.stub(); diff --git a/src/app/state/auth/effects/auth-status.effects.ts b/src/app/state/auth/effects/auth-status.effects.ts index ba2e191a7..328b394c9 100644 --- a/src/app/state/auth/effects/auth-status.effects.ts +++ b/src/app/state/auth/effects/auth-status.effects.ts @@ -19,7 +19,6 @@ import {DrawingActiveMapItem} from '../../../map/models/implementations/drawing. import {MAP_SERVICE} from '../../../app.module'; import {MapService} from '../../../map/interfaces/map.service'; import {isActiveMapItemOfType} from '../../../shared/type-guards/active-map-item-type.type-guard'; -import {StorageUtils} from '../../../shared/utils/storage.utils'; import {defaultActiveMapItemConfiguration} from '../../../shared/interfaces/active-map-item-configuration.interface'; @Injectable() @@ -30,7 +29,7 @@ export class AuthStatusEffects { ofType(AuthStatusActions.performLogin), concatLatestFrom(() => this.store.select(selectCurrentShareLinkItem)), tap(([_, shareLinkItem]) => { - this.sessionStorageService.set('shareLinkItem', StorageUtils.stringifyJson(shareLinkItem)); + this.sessionStorageService.set('shareLinkItem', this.sessionStorageService.stringifyJson(shareLinkItem)); this.authService.login(); }), ); @@ -44,7 +43,7 @@ export class AuthStatusEffects { ofType(AuthStatusActions.performLogout), concatLatestFrom(() => this.store.select(selectCurrentShareLinkItem)), tap(([{isForced}, shareLinkItem]) => { - this.sessionStorageService.set('shareLinkItem', StorageUtils.stringifyJson(shareLinkItem)); + this.sessionStorageService.set('shareLinkItem', this.sessionStorageService.stringifyJson(shareLinkItem)); this.authService.logout(isForced); }), ); @@ -61,7 +60,7 @@ export class AuthStatusEffects { this.sessionStorageService.remove('shareLinkItem'); const shareLinkItem: ShareLinkItem | undefined = shareLinkItemString - ? StorageUtils.parseJson(shareLinkItemString) + ? this.sessionStorageService.parseJson(shareLinkItemString) : undefined; return shareLinkItem diff --git a/src/app/testing/map-testing/share-link-item-test.utils.ts b/src/app/testing/map-testing/share-link-item-test.utils.ts index dbf7914aa..67e1b218f 100644 --- a/src/app/testing/map-testing/share-link-item-test.utils.ts +++ b/src/app/testing/map-testing/share-link-item-test.utils.ts @@ -1,9 +1,8 @@ import {ShareLinkItem} from '../../shared/interfaces/share-link.interface'; import {MinimalGeometriesUtils} from './minimal-geometries.utils'; -import {DayjsUtils} from '../../shared/utils/dayjs.utils'; export class ShareLinkItemTestUtils { - public static createShareLinkItem(): ShareLinkItem { + public static createShareLinkItem(timeExtentStart: Date, timeExtentEnd: Date): ShareLinkItem { const {srs, ...minimalPolygonGeometry} = MinimalGeometriesUtils.getMinimalPolygon(2056); return { basemapId: 'arelkbackgroundzh', @@ -33,7 +32,7 @@ export class ShareLinkItemTestUtils { opacity: 0.5, visible: true, isSingleLayer: false, - timeExtent: {start: DayjsUtils.parseUTCDate('1000'), end: DayjsUtils.parseUTCDate('2020')}, + timeExtent: {start: timeExtentStart, end: timeExtentEnd}, attributeFilters: [ { parameter: 'FILTER_GEBART', diff --git a/src/test.ts b/src/test.ts index bdbddfe24..05d4b9b34 100644 --- a/src/test.ts +++ b/src/test.ts @@ -3,6 +3,18 @@ import 'zone.js/testing'; import {getTestBed} from '@angular/core/testing'; import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'; +import {TIME_SERVICE} from './app/app.module'; +import {timeServiceFactory} from './app/shared/factories/time-service.factory'; // First, initialize the Angular testing environment. -getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting([ + // we provide the time service here so it is the same as in the running app and we don't have to inject it in each test + // todo: add to documentation + { + provide: TIME_SERVICE, + useFactory: timeServiceFactory, + }, + ]), +); From b73725e2da2ac0154d6ee7aeb622e4dbc0924db0 Mon Sep 17 00:00:00 2001 From: Lukas Merz Date: Tue, 24 Sep 2024 17:59:01 +0200 Subject: [PATCH 08/12] GB3-1361: Remove utils classes and refactor the whole chain of failing stuff --- .../time-slider/time-slider.component.ts | 25 +-- src/app/map/interfaces/time-slider.service.ts | 16 -- .../models/implementations/gb2-wms.model.ts | 4 +- .../esri-services/esri-map.service.ts | 5 +- .../map/services/favourites.service.spec.ts | 4 +- src/app/map/services/favourites.service.ts | 2 +- .../map/services/time-slider.service.spec.ts | 9 +- src/app/map/services/time-slider.service.ts | 204 ++++++------------ .../interfaces/time-service.interface.ts | 26 ++- src/app/shared/services/dayjs.service.spec.ts | 23 ++ src/app/shared/services/dayjs.service.ts | 112 +++++++++- src/app/shared/utils/dayjs.utils.spec.ts | 39 ---- src/app/shared/utils/dayjs.utils.ts | 19 +- .../map/effects/share-link.effects.spec.ts | 198 +++++++++-------- ...ve-map-item-configuration.selector.spec.ts | 11 +- 15 files changed, 344 insertions(+), 353 deletions(-) delete mode 100644 src/app/map/interfaces/time-slider.service.ts delete mode 100644 src/app/shared/utils/dayjs.utils.spec.ts diff --git a/src/app/map/components/time-slider/time-slider.component.ts b/src/app/map/components/time-slider/time-slider.component.ts index c60316469..f7044da58 100644 --- a/src/app/map/components/time-slider/time-slider.component.ts +++ b/src/app/map/components/time-slider/time-slider.component.ts @@ -1,18 +1,16 @@ import {Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; import {TimeExtent} from '../../interfaces/time-extent.interface'; import {TimeSliderConfiguration, TimeSliderLayerSource} from '../../../shared/interfaces/topic.interface'; -import {ManipulateTypeAlias as ManipulateType, UnitTypeAlias as UnitType} from '../../../shared/types/dayjs-alias-type'; import {TimeSliderService} from '../../services/time-slider.service'; import {MatDatepicker} from '@angular/material/datepicker'; -import {DayjsUtils} from '../../../shared/utils/dayjs.utils'; import {TIME_SERVICE} from '../../../app.module'; -import {TimeService} from '../../../shared/interfaces/time-service.interface'; +import {DateUnit, TimeService} from '../../../shared/interfaces/time-service.interface'; // There is an array (`allowedDatePickerManipulationUnits`) and a new union type (`DatePickerManipulationUnits`) for two reasons: // To be able to extract a union type subset of `ManipulateType` AND to have an array used to check if a given value is in said union type. // => more infos: https://stackoverflow.com/questions/50085494/how-to-check-if-a-given-value-is-in-a-union-type-array const allowedDatePickerManipulationUnits = ['years', 'months', 'days'] as const; // TS3.4 syntax -export type DatePickerManipulationUnits = Extract; +export type DatePickerManipulationUnits = Extract; type DatePickerStartView = 'month' | 'year' | 'multi-year'; @Component({ @@ -164,23 +162,8 @@ export class TimeSliderComponent implements OnInit, OnChanges { } } - /** - * Returns `true` if the given range is defined and is exactly one of a single time unit (year, month, ...). - * If the optional parameter `allowedTimeUnits` is given then only the units in there are allowed; all other return `false` as well. - * @param range The range (in ISO-8601 time span format) to be evaluated - * - * @example - * `P1Y1M` is a duration of one year AND one month which is more than one time unit; therefore is the result `false` - * `P2Y` is a duration of two years which is more than one of a single time unit; therefore is the result `false` - * `P1D` is a duration of one day which is exactly one of a single time unit; therefore the result is `true` - */ private isRangeExactlyOneOfSingleTimeUnit(range: string | null | undefined): boolean { - if (range) { - const rangeDuration = DayjsUtils.getDuration(range); - const unit = TimeSliderService.extractUniqueUnitFromDuration(rangeDuration); - return unit !== undefined && TimeSliderService.getDurationAsNumber(rangeDuration, unit) === 1; - } - return false; + return range ? this.timeService.isStringSingleTimeUnitRange(range) : false; } /** @@ -216,7 +199,7 @@ export class TimeSliderComponent implements OnInit, OnChanges { private isLayerSourceContinuous(layerSource: TimeSliderLayerSource, unit: DatePickerManipulationUnits): boolean { const dateAsAscendingSortedNumbers = layerSource.layers - .map((layer) => DayjsUtils.getPartial(layer.date, unit as UnitType)) + .map((layer) => this.timeService.getPartialFromString(layer.date, unit)) .sort((a, b) => a - b); // all date numbers must be part of a continuous and strictly monotonously rising series each with exactly // one step between them: `date[0] = x` => `date[n] = x + n` diff --git a/src/app/map/interfaces/time-slider.service.ts b/src/app/map/interfaces/time-slider.service.ts deleted file mode 100644 index 00401edca..000000000 --- a/src/app/map/interfaces/time-slider.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {Observable} from 'rxjs'; -import {TimeExtent} from './time-extent.interface'; -import {TimeSliderConfiguration} from '../../shared/interfaces/topic.interface'; -import {Gb2WmsActiveMapItem} from '../models/implementations/gb2-wms.model'; - -export interface TimeSliderService { - /** Assigns a time slider widget to the given container based on the active map item */ - assignTimeSliderWidget(activeMapItem: Gb2WmsActiveMapItem, container: HTMLDivElement): void; - - /** Returns an observable which fires an event in case the time extent changes for the active map item with the given ID */ - watchTimeExtent(activeMapItemId: string): Observable; - - /** Creates a new time extent from the given new time extent. The created time extent will be fully validated against - * the limitations and rules of the time slider configuration. */ - createValidTimeExtent(timeSliderConfig: TimeSliderConfiguration, newValue: TimeExtent, oldValue?: TimeExtent): TimeExtent; -} diff --git a/src/app/map/models/implementations/gb2-wms.model.ts b/src/app/map/models/implementations/gb2-wms.model.ts index 5efee3fcb..2503d34d4 100644 --- a/src/app/map/models/implementations/gb2-wms.model.ts +++ b/src/app/map/models/implementations/gb2-wms.model.ts @@ -2,7 +2,6 @@ import {FilterConfiguration, Map, MapLayer, SearchConfiguration, TimeSliderConfi import {TimeExtent} from '../../interfaces/time-extent.interface'; import {AbstractActiveMapItemSettings, ActiveMapItem} from '../active-map-item.model'; import {AddToMapVisitor} from '../../interfaces/add-to-map.visitor'; -import {TimeSliderService} from '../../services/time-slider.service'; export class Gb2WmsSettings extends AbstractActiveMapItemSettings { public readonly type = 'gb2Wms'; @@ -23,7 +22,8 @@ export class Gb2WmsSettings extends AbstractActiveMapItemSettings { this.layers = layer ? [layer] : map.layers; this.timeSliderConfiguration = map.timeSliderConfiguration; if (map.timeSliderConfiguration) { - this.timeSliderExtent = timeExtent ?? TimeSliderService.createInitialTimeSliderExtent(map.timeSliderConfiguration); + //this.timeSliderExtent = timeExtent ?? TimeSliderService.createInitialTimeSliderExtent(map.timeSliderConfiguration); // todo: solve + //this.timeSliderExtent = timeExtent ?? TimeSliderService.createInitialTimeSliderExtent(map.timeSliderConfiguration); } this.filterConfigurations = filterConfigurations ?? map.filterConfigurations; this.searchConfigurations = map.searchConfigurations; diff --git a/src/app/map/services/esri-services/esri-map.service.ts b/src/app/map/services/esri-services/esri-map.service.ts index c91d2ab61..534cff0f2 100644 --- a/src/app/map/services/esri-services/esri-map.service.ts +++ b/src/app/map/services/esri-services/esri-map.service.ts @@ -58,7 +58,6 @@ import {TransformationService} from './transformation.service'; import {ExternalWmsActiveMapItem} from '../../models/implementations/external-wms.model'; import {ExternalKmlActiveMapItem} from '../../models/implementations/external-kml.model'; import {ExternalWmsLayer} from '../../../shared/interfaces/external-layer.interface'; -import {ActiveTimeSliderLayersUtils} from '../../utils/active-time-slider-layers.utils'; import {Gb3TopicsService} from '../../../shared/services/apis/gb3/gb3-topics.service'; import {InitialMapExtentService} from '../initial-map-extent.service'; import {MapConstants} from '../../../shared/constants/map.constants'; @@ -66,6 +65,7 @@ import {HitTestSelectionUtils} from './utils/hit-test-selection.utils'; import * as intl from '@arcgis/core/intl'; import {TimeService} from '../../../shared/interfaces/time-service.interface'; import {TIME_SERVICE} from '../../../app.module'; +import {TimeSliderService} from '../time-slider.service'; import GraphicHit = __esri.GraphicHit; const DEFAULT_POINT_ZOOM_EXTENT_SCALE = 750; @@ -107,6 +107,7 @@ export class EsriMapService implements MapService, OnDestroy { private readonly esriToolService: EsriToolService, private readonly gb3TopicsService: Gb3TopicsService, private readonly initialMapExtentService: InitialMapExtentService, + private readonly timeSliderService: TimeSliderService, @Inject(TIME_SERVICE) private readonly timeService: TimeService, ) { /** @@ -666,7 +667,7 @@ export class EsriMapService implements MapService, OnDestroy { const timeSliderLayerSource = timeSliderConfig.source as TimeSliderLayerSource; const timeSliderLayerNames = timeSliderLayerSource.layers.map((layer) => layer.layerName); const visibleTimeSliderLayers = mapItem.settings.layers.filter( - (layer) => ActiveTimeSliderLayersUtils.isLayerVisible(layer, mapItem.settings.timeSliderConfiguration, timeSliderExtent) === true, + (layer) => this.timeSliderService.isLayerVisible(layer, mapItem.settings.timeSliderConfiguration, timeSliderExtent) === true, ); // include all layers that are not specified in the time slider config const esriSublayers = esriLayer.sublayers.filter((sublayer) => !timeSliderLayerNames.includes(sublayer.name)); diff --git a/src/app/map/services/favourites.service.spec.ts b/src/app/map/services/favourites.service.spec.ts index 221556973..351a64cd7 100644 --- a/src/app/map/services/favourites.service.spec.ts +++ b/src/app/map/services/favourites.service.spec.ts @@ -30,6 +30,7 @@ describe('FavouritesService', () => { let store: MockStore; let gb3FavouritesService: Gb3FavouritesService; let timeService: TimeService; + let timeSliderService: TimeSliderService; beforeEach(() => { TestBed.configureTestingModule({ @@ -38,6 +39,7 @@ describe('FavouritesService', () => { }); store = TestBed.inject(MockStore); timeService = TestBed.inject(TIME_SERVICE); + timeSliderService = TestBed.inject(TimeSliderService); store.overrideSelector(selectActiveMapItemConfigurations, []); store.overrideSelector(selectMaps, []); store.overrideSelector(selectFavouriteBaseConfig, {center: {x: 0, y: 0}, scale: 0, basemap: ''}); @@ -2367,7 +2369,7 @@ describe('FavouritesService', () => { const result = service.getActiveMapItemsForFavourite(activeMapItemConfigurations, true); // eslint-disable-next-line @typescript-eslint/dot-notation - const initialTimeExtent = TimeSliderService.createInitialTimeSliderExtent(service['availableMaps'][0].timeSliderConfiguration!); + const initialTimeExtent = timeSliderService.createInitialTimeSliderExtent(service['availableMaps'][0].timeSliderConfiguration!); const activeMapItems: ActiveMapItem[] = [ ActiveMapItemFactory.createGb2WmsMapItem( // eslint-disable-next-line @typescript-eslint/dot-notation diff --git a/src/app/map/services/favourites.service.ts b/src/app/map/services/favourites.service.ts index 6522b86eb..19369a514 100644 --- a/src/app/map/services/favourites.service.ts +++ b/src/app/map/services/favourites.service.ts @@ -254,7 +254,7 @@ export class FavouritesService implements OnDestroy { const isValid = this.validateTimeSlider(timeSliderConfiguration, timeExtent); if (!isValid) { if (ignoreErrors) { - return TimeSliderService.createInitialTimeSliderExtent(timeSliderConfiguration); + return this.timeSliderService.createInitialTimeSliderExtent(timeSliderConfiguration); } else { throw new FavouriteIsInvalid(`Die Konfiguration für den Zeitschieberegler der Karte '${title}' ist ungültig.`); } diff --git a/src/app/map/services/time-slider.service.spec.ts b/src/app/map/services/time-slider.service.spec.ts index 0ddf6cb7e..518b79035 100644 --- a/src/app/map/services/time-slider.service.spec.ts +++ b/src/app/map/services/time-slider.service.spec.ts @@ -3,7 +3,6 @@ import {TimeSliderService} from './time-slider.service'; import dayjs from 'dayjs'; import {TimeSliderConfiguration, TimeSliderParameterSource} from '../../shared/interfaces/topic.interface'; import {TimeExtent} from '../interfaces/time-extent.interface'; -import {DayjsUtils} from '../../shared/utils/dayjs.utils'; import {TIME_SERVICE} from '../../app.module'; import {TimeService} from '../../shared/interfaces/time-service.interface'; @@ -130,7 +129,7 @@ describe('TimeSliderService', () => { }); it('should create a new start date if it is smaller than the minimum date', () => { - const newValue: TimeExtent = {start: DayjsUtils.subtractDuration(minimumDate, DayjsUtils.getDuration('P1M')), end: minimumDate}; + const newValue: TimeExtent = {start: timeService.subtractRangeFromDate(minimumDate, 'P1M'), end: minimumDate}; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); expect( timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2000-01', dateFormat)), @@ -141,7 +140,7 @@ describe('TimeSliderService', () => { }); it('should create a new start date if it is bigger than the maximum date', () => { - const newValue: TimeExtent = {start: DayjsUtils.addDuration(maximumDate, DayjsUtils.getDuration('P1M')), end: minimumDate}; + const newValue: TimeExtent = {start: timeService.addRangeToDate(maximumDate, 'P1M'), end: minimumDate}; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); expect( timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2001-03', dateFormat)), @@ -400,9 +399,7 @@ describe('TimeSliderService', () => { const expectedNumberOfStops = 12; expect(stops.length).toBe(expectedNumberOfStops); expect(timeService.calculateDifferenceBetweenDates(stops[0], minimumDate)).toBe(0); - expect( - timeService.calculateDifferenceBetweenDates(stops[1], DayjsUtils.addDuration(minimumDate, DayjsUtils.getDuration(range))), - ).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[1], timeService.addRangeToDate(minimumDate, range))).toBe(0); expect(timeService.calculateDifferenceBetweenDates(stops[stops.length - 1], maximumDate)).toBe(0); }); }); diff --git a/src/app/map/services/time-slider.service.ts b/src/app/map/services/time-slider.service.ts index 5183685ef..11df1007c 100644 --- a/src/app/map/services/time-slider.service.ts +++ b/src/app/map/services/time-slider.service.ts @@ -1,120 +1,27 @@ import {Inject, Injectable} from '@angular/core'; -import {TimeSliderConfiguration, TimeSliderLayerSource} from '../../shared/interfaces/topic.interface'; -import {Duration} from 'dayjs/plugin/duration'; +import {MapLayer, TimeSliderConfiguration, TimeSliderLayerSource} from '../../shared/interfaces/topic.interface'; import {TimeExtent} from '../interfaces/time-extent.interface'; import {InvalidTimeSliderConfiguration} from '../../shared/errors/map.errors'; -import {DayjsUtils} from '../../shared/utils/dayjs.utils'; import {TIME_SERVICE} from '../../app.module'; -import {TimeService} from '../../shared/interfaces/time-service.interface'; -import {ManipulateType} from 'dayjs'; +import {DateUnit, TimeService} from '../../shared/interfaces/time-service.interface'; @Injectable({ providedIn: 'root', }) export class TimeSliderService { constructor(@Inject(TIME_SERVICE) private readonly timeService: TimeService) {} - /** * Creates an initial time extent based on the given time slider configuration. */ - public static createInitialTimeSliderExtent(timeSliderConfig: TimeSliderConfiguration): TimeExtent { - const minimumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maximumDate: Date = DayjsUtils.parseUTCDate(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); - const range: Duration | null = timeSliderConfig.range ? DayjsUtils.getDuration(timeSliderConfig.range) : null; + public createInitialTimeSliderExtent(timeSliderConfig: TimeSliderConfiguration): TimeExtent { + const minimumDate: Date = this.timeService.getUTCDateFromString(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maximumDate: Date = this.timeService.getUTCDateFromString(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); return { start: minimumDate, - end: range ? this.addDuration(minimumDate, range) : maximumDate, + end: timeSliderConfig.range ? this.addRangeToDate(minimumDate, timeSliderConfig.range) : maximumDate, }; } - /** - * Extracts the unit from the given duration or if it contains values with multiple units. - * - * @remarks - * It does return a unit ('years'/'months'/...) only if the given duration contains values of this unit and nothing else; - * otherwise. - * - * @example - * 'P3Y' is a duration of 3 years. The duration only contains years and therefore this method returns 'years' - * 'P1Y6M' is a duration of 1 year and 6 months. It contains years (1) and months (6) which is a mix of two units. The return value will - * be . - * */ - public static extractUniqueUnitFromDuration(duration: Duration): ManipulateType | undefined { - // todo: this could be a utils class still - if (duration.years() === duration.asYears()) return 'years'; - if (duration.months() === duration.asMonths()) return 'months'; - if (duration.days() === duration.asDays()) return 'days'; - if (duration.hours() === duration.asHours()) return 'hours'; - if (duration.minutes() === duration.asMinutes()) return 'minutes'; - if (duration.seconds() === duration.asSeconds()) return 'seconds'; - if (duration.milliseconds() === duration.asMilliseconds()) return 'milliseconds'; - return undefined; - } - - /** - * Gets the whole given duration as a number value in the desired unit. - */ - public static getDurationAsNumber(duration: Duration, unit: ManipulateType): number { - // todo: this one as well - switch (unit) { - case 'ms': - case 'millisecond': - case 'milliseconds': - return duration.asMilliseconds(); - case 'second': - case 'seconds': - case 's': - return duration.asSeconds(); - case 'minute': - case 'minutes': - case 'm': - return duration.asMinutes(); - case 'hour': - case 'hours': - case 'h': - return duration.asHours(); - case 'd': - case 'D': - case 'day': - case 'days': - return duration.asDays(); - case 'M': - case 'month': - case 'months': - return duration.asMonths(); - case 'y': - case 'year': - case 'years': - return duration.asYears(); - case 'w': - case 'week': - case 'weeks': - return duration.asWeeks(); - } - } - - /** - * Adds the duration to the given date as exact as possible. - * - * @remarks - * It does more than a simple `dayjs(date).add(duration)`. It will add values of a specific unit to the date in case that - * the duration contains only values of one specific unit (e.g. 'years'). This has the advantage that it does not use - * a generic solution which would be 365 days in case of a year. - * - * @example - * addDuration(01.01.2000, duration(1, 'years')) === 01.01.2001 - * while the default way using `dayjs.add` would lead to an error: dayjs(01.01.2000).add(duration(1, 'years')) === 01.01.2000 + 365 days - * === 31.12.2000 - * */ - private static addDuration(date: Date, duration: Duration): Date { - const unit = TimeSliderService.extractUniqueUnitFromDuration(duration); - if (!unit) { - return DayjsUtils.addDuration(date, duration); - } - const value = TimeSliderService.getDurationAsNumber(duration, unit); - return DayjsUtils.addDuration(date, DayjsUtils.getDurationWithUnit(value, unit)); - } - /** * Creates stops which define specific locations on the time slider where thumbs will snap to when manipulated. */ @@ -127,6 +34,29 @@ export class TimeSliderService { } } + /** + * Is the given layer visible? Returns `true` or `false` depending on the layer to be within the given time extent; or `undefined` if + * either the layer isn't part of a time slider configuration, the extent is undefined or the configuration source isn't of type `layer`. + */ + public isLayerVisible( + mapLayer: MapLayer, + timeSliderConfiguration: TimeSliderConfiguration | undefined, + timeExtent: TimeExtent | undefined, + ): boolean | undefined { + if (!timeSliderConfiguration || timeSliderConfiguration.sourceType === 'parameter' || !timeExtent) { + return undefined; + } + + const timeSliderLayerSource = timeSliderConfiguration.source as TimeSliderLayerSource; + const timeSliderLayer = timeSliderLayerSource.layers.find((layer) => layer.layerName === mapLayer.layer); + if (timeSliderLayer) { + const date = this.timeService.getUTCDateFromString(timeSliderLayer.date, timeSliderConfiguration.dateFormat); + return date >= timeExtent.start && date < timeExtent.end; + } else { + return undefined; + } + } + public createValidTimeExtent( timeSliderConfig: TimeSliderConfiguration, newValue: TimeExtent, @@ -153,8 +83,7 @@ export class TimeSliderService { The start has changed as fixed ranges technically don't have an end date => the end date has to be adjusted accordingly to enforce the fixed range between start and end date */ - const range: Duration = DayjsUtils.getDuration(timeSliderConfig.range); - timeExtent.end = TimeSliderService.addDuration(timeExtent.start, range); + timeExtent.end = this.addRangeToDate(timeExtent.start, timeSliderConfig.range); } else if (timeSliderConfig.minimalRange) { /* Minimal range @@ -176,20 +105,20 @@ export class TimeSliderService { } const startEndDiff: number = this.timeService.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end); - const minimalRange: Duration = DayjsUtils.getDuration(timeSliderConfig.minimalRange); + const minimalRangeInMs: number = this.timeService.getISORangeInMilliseconds(timeSliderConfig.minimalRange); - if (startEndDiff < minimalRange.asMilliseconds()) { + if (startEndDiff < minimalRangeInMs) { if (hasStartDateChanged) { - const newStartDate = this.subtractDuration(timeExtent.end, minimalRange); + const newStartDate = this.subtractRangeFromDate(timeExtent.end, timeSliderConfig.minimalRange); timeExtent.start = this.validateDateWithinLimits(newStartDate, minimumDate, maximumDate); - if (this.timeService.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end) < minimalRange.asMilliseconds()) { - timeExtent.end = TimeSliderService.addDuration(timeExtent.start, minimalRange); + if (this.timeService.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end) < minimalRangeInMs) { + timeExtent.end = this.addRangeToDate(timeExtent.start, timeSliderConfig.minimalRange); } } else { - const newEndDate = TimeSliderService.addDuration(timeExtent.start, minimalRange); + const newEndDate = this.addRangeToDate(timeExtent.start, timeSliderConfig.minimalRange); timeExtent.end = this.validateDateWithinLimits(newEndDate, minimumDate, maximumDate); - if (this.timeService.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end) < minimalRange.asMilliseconds()) { - timeExtent.start = this.subtractDuration(timeExtent.end, minimalRange); + if (this.timeService.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end) < minimalRangeInMs) { + timeExtent.start = this.subtractRangeFromDate(timeExtent.end, timeSliderConfig.minimalRange); } } } @@ -219,7 +148,7 @@ export class TimeSliderService { * 'H:m s.SSS' is a date format containing hours, minutes, seconds and milliseconds; there are multiple units therefore this method * returns 'undefined' * */ - public extractUniqueUnitFromDateFormat(dateFormat: string): ManipulateType | undefined { + public extractUniqueUnitFromDateFormat(dateFormat: string): DateUnit | undefined { if (dateFormat.replace(/S/g, '').trim() === '') return 'milliseconds'; if (dateFormat.replace(/s/g, '').trim() === '') return 'seconds'; if (dateFormat.replace(/m/g, '').trim() === '') return 'minutes'; @@ -230,6 +159,10 @@ export class TimeSliderService { return undefined; } + private addRangeToDate(date: Date, range: string): Date { + return this.timeService.addRangeToDate(date, range); + } + /** * Creates stops for a layer source containing multiple dates which may not necessarily have constant gaps between them. */ @@ -250,33 +183,42 @@ export class TimeSliderService { const minimumDate: Date = this.timeService.getUTCDateFromString(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); const maximumDate: Date = this.timeService.getUTCDateFromString(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); const initialRange: string | null = timeSliderConfig.range ?? timeSliderConfig.minimalRange ?? null; - let stopRangeDuration: Duration | null = initialRange ? DayjsUtils.getDuration(initialRange) : null; if ( - stopRangeDuration && - this.timeService.calculateDifferenceBetweenDates(minimumDate, maximumDate) <= stopRangeDuration.asMilliseconds() + initialRange && + this.timeService.calculateDifferenceBetweenDates(minimumDate, maximumDate) <= this.timeService.getISORangeInMilliseconds(initialRange) ) { throw new InvalidTimeSliderConfiguration('min date + range > max date'); } - if (!stopRangeDuration) { - const unit = this.extractSmallestUnitFromDateFormat(timeSliderConfig.dateFormat); + + let unit: DateUnit | undefined; + if (!initialRange) { + unit = this.extractSmallestUnitFromDateFormat(timeSliderConfig.dateFormat); if (!unit) { throw new InvalidTimeSliderConfiguration('Datumsformat sowie minimale Range sind ungültig.'); } - - // create a new duration base on the smallest unit with the lowest valid unit number (1) - stopRangeDuration = DayjsUtils.getDurationWithUnit(1, unit); } const dates: Date[] = []; let date = minimumDate; while (date < maximumDate) { dates.push(date); - date = TimeSliderService.addDuration(date, stopRangeDuration); + + if (initialRange) { + date = this.addRangeToDate(date, initialRange); + } else if (unit) { + date = this.addMinimalDuration(date, unit); + } else { + throw new InvalidTimeSliderConfiguration('Datumsformat sowie minimale Range sind ungültig.'); + } } dates.push(maximumDate); return dates; } + private addMinimalDuration(date: Date, unit: string): Date { + return this.timeService.addMinimalRangeToDate(date, unit); + } + /** * Extracts the smallest unit from the given date format (ISO8601) or if nothing matches. * @@ -286,7 +228,7 @@ export class TimeSliderService { * 'H:m s.SSS' is a date format containing hours, minutes, seconds and milliseconds; The smallest unit is milliseconds and therefore this * method returns 'milliseconds' * */ - private extractSmallestUnitFromDateFormat(dateFormat: string): ManipulateType | undefined { + private extractSmallestUnitFromDateFormat(dateFormat: string): DateUnit | undefined { if (dateFormat.includes('SSS')) return 'milliseconds'; if (dateFormat.includes('s')) return 'seconds'; if (dateFormat.includes('m')) return 'minutes'; @@ -311,25 +253,7 @@ export class TimeSliderService { return validDate; } - /** - * Subtracts the duration from the given date as exact as possible. - * - * @remarks - * It does more than a simple `dayjs(date).subtract(duration)`. It will subtract values of a specific unit from the date in case that - * the duration contains only values of one specific unit (e.g. 'years'). This has the advantage that it does not use - * a generic solution which would be 365 days in case of a year. - * - * @example - * subtractDuration(01.01.2001, duration(1, 'years')) === 01.01.2000 - * while the default way using `dayjs.subtract` would lead to an error: dayjs(01.01.2001).subtract(duration(1, 'years')) === 01.01.2001 - - * 365 days === 02.01.2000 - * */ - private subtractDuration(date: Date, duration: Duration): Date { - const unit = TimeSliderService.extractUniqueUnitFromDuration(duration); - if (!unit) { - return DayjsUtils.subtractDuration(date, duration); - } - const value = TimeSliderService.getDurationAsNumber(duration, unit); - return DayjsUtils.subtractDuration(date, DayjsUtils.getDurationWithUnit(value, unit)); + private subtractRangeFromDate(date: Date, range: string): Date { + return this.timeService.subtractRangeFromDate(date, range); } } diff --git a/src/app/shared/interfaces/time-service.interface.ts b/src/app/shared/interfaces/time-service.interface.ts index 087db4190..cb19dd84e 100644 --- a/src/app/shared/interfaces/time-service.interface.ts +++ b/src/app/shared/interfaces/time-service.interface.ts @@ -27,7 +27,29 @@ export interface TimeService { isDate: (value: string) => boolean; getUTCDateFromString: (date: string, format?: string) => Date; + + /** + * Returns `true` if the given string range is exactly one of a single time unit (year, month, ...). + * + * @example + * `P1Y1M` is a duration of one year AND one month which is more than one time unit; therefore is the result `false` + * `P2Y` is a duration of two years which is more than one of a single time unit; therefore is the result `false` + * `P1D` is a duration of one day which is exactly one of a single time unit; therefore the result is `true` + */ + isStringSingleTimeUnitRange: (range: string) => boolean; + + addRangeToDate: (date: Date, range: string) => Date; + + subtractRangeFromDate: (date: Date, range: string) => Date; + + getISORangeInMilliseconds: (range: string) => number; + + /** + * Adds a range of 1 of the given unit to the date. + * @param date + * @param unit + */ + addMinimalRangeToDate: (date: Date, unit: string) => Date; } -// base on Dayjs.UnitTypeShort; but it's basically normal iso8601 date units? -> check -export type DateUnit = 'd' | 'D' | 'M' | 'y' | 'h' | 'm' | 's' | 'ms'; +export type DateUnit = 'days' | 'months' | 'years' | 'hours' | 'minutes' | 'seconds' | 'milliseconds'; diff --git a/src/app/shared/services/dayjs.service.spec.ts b/src/app/shared/services/dayjs.service.spec.ts index bd0289c25..649500506 100644 --- a/src/app/shared/services/dayjs.service.spec.ts +++ b/src/app/shared/services/dayjs.service.spec.ts @@ -67,4 +67,27 @@ describe('DayjsService', () => { expect(dayjsService.calculateDifferenceBetweenDates(date1, date2)).toBe(86400000); // 1 day in milliseconds }); }); + + describe('addDuration', () => { + it('adds the duration to the date', () => { + const date = new Date(2023, 9, 1); + const range = 'P1D'; + expect(dayjsService.addRangeToDate(date, range)).toEqual(new Date(2023, 9, 2)); + }); + }); + + describe('subtractDuration', () => { + it('subtracts the duration from the date', () => { + const date = new Date(2023, 9, 1); + const range = 'P1D'; + expect(dayjsService.subtractRangeFromDate(date, range)).toEqual(new Date(2023, 8, 30)); + }); + }); + + describe('getPartial', () => { + it('returns the correct partial value', () => { + expect(dayjsService.getPartialFromString('2023-10-01', 'years')).toBe(2023); + expect(dayjsService.getPartialFromString('2023-10-01', 'months')).toBe(9); // month is 0-indexed + }); + }); }); diff --git a/src/app/shared/services/dayjs.service.ts b/src/app/shared/services/dayjs.service.ts index e2fe41979..fc1a4e42b 100644 --- a/src/app/shared/services/dayjs.service.ts +++ b/src/app/shared/services/dayjs.service.ts @@ -1,6 +1,7 @@ import {Injectable} from '@angular/core'; import {DateUnit, TimeService} from '../interfaces/time-service.interface'; -import dayjs from 'dayjs'; +import dayjs, {ManipulateType} from 'dayjs'; +import {Duration} from 'dayjs/plugin/duration'; @Injectable({ providedIn: 'root', @@ -38,6 +39,115 @@ export class DayjsService implements TimeService { return Math.abs(this.createDayjsObject(secondDate).diff(this.createDayjsObject(firstDate))); } + public getISORangeInMilliseconds(range: string): number { + return this.createDayjsDurationFromISOString(range).asMilliseconds(); + } + + public addMinimalRangeToDate(date: Date, unit: string): Date { + return this.createDayjsObject(date) + .add(1, unit as ManipulateType) + .toDate(); + } + + public isStringSingleTimeUnitRange(range: string): boolean { + const rangeDuration = this.createDayjsDurationFromISOString(range); + const unit = this.extractUniqueUnitFromDuration(rangeDuration); + return unit !== undefined && this.getDurationAsNumber(rangeDuration, unit) === 1; + } + + public addRangeToDate(date: Date, range: string): Date { + const duration = this.createDayjsDurationFromISOString(range); + const unit = this.extractUniqueUnitFromDuration(duration); + if (!unit) { + return this.createDayjsObject(date).add(duration).toDate(); + } + const value = this.getDurationAsNumber(duration, unit); + return this.createDayjsObject(date).add(this.createDayjsDurationFromNumber(value, unit)).toDate(); + } + + public subtractRangeFromDate(date: Date, range: string): Date { + const duration = this.createDayjsDurationFromISOString(range); + const unit = this.extractUniqueUnitFromDuration(duration); + if (!unit) { + return this.createDayjsObject(date).subtract(duration).toDate(); + } + const value = this.getDurationAsNumber(duration, unit); + return this.createDayjsObject(date).subtract(this.createDayjsDurationFromNumber(value, unit)).toDate(); + } + + private createDayjsDurationFromNumber(value: number, unit: ManipulateType): Duration { + return dayjs.duration(value, unit); + } + + private createDayjsDurationFromISOString(range: string): Duration { + return dayjs.duration(range); + } + + /** + * Extracts the unit from the given duration or if it contains values with multiple units. + * + * @remarks + * It does return a unit ('years'/'months'/...) only if the given duration contains values of this unit and nothing else; + * otherwise. + * + * @example + * 'P3Y' is a duration of 3 years. The duration only contains years and therefore this method returns 'years' + * 'P1Y6M' is a duration of 1 year and 6 months. It contains years (1) and months (6) which is a mix of two units. The return value will + * be . + * */ + private extractUniqueUnitFromDuration(duration: Duration): ManipulateType | undefined { + if (duration.years() === duration.asYears()) return 'years'; + if (duration.months() === duration.asMonths()) return 'months'; + if (duration.days() === duration.asDays()) return 'days'; + if (duration.hours() === duration.asHours()) return 'hours'; + if (duration.minutes() === duration.asMinutes()) return 'minutes'; + if (duration.seconds() === duration.asSeconds()) return 'seconds'; + if (duration.milliseconds() === duration.asMilliseconds()) return 'milliseconds'; + return undefined; + } + + /** + * Gets the whole given duration as a number value in the desired unit. + */ + private getDurationAsNumber(duration: Duration, unit: ManipulateType): number { + // todo: this one as well + switch (unit) { + case 'ms': + case 'millisecond': + case 'milliseconds': + return duration.asMilliseconds(); + case 'second': + case 'seconds': + case 's': + return duration.asSeconds(); + case 'minute': + case 'minutes': + case 'm': + return duration.asMinutes(); + case 'hour': + case 'hours': + case 'h': + return duration.asHours(); + case 'd': + case 'D': + case 'day': + case 'days': + return duration.asDays(); + case 'M': + case 'month': + case 'months': + return duration.asMonths(); + case 'y': + case 'year': + case 'years': + return duration.asYears(); + case 'w': + case 'week': + case 'weeks': + return duration.asWeeks(); + } + } + private createDayjsObject(date: Date | string, format?: string): dayjs.Dayjs { return dayjs(date, format); } diff --git a/src/app/shared/utils/dayjs.utils.spec.ts b/src/app/shared/utils/dayjs.utils.spec.ts deleted file mode 100644 index 02bcdfb67..000000000 --- a/src/app/shared/utils/dayjs.utils.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {DayjsUtils} from './dayjs.utils'; -import dayjs from 'dayjs'; - -describe('DayjsUtils', () => { - describe('getPartial', () => { - it('returns the correct partial value', () => { - expect(DayjsUtils.getPartial('2023-10-01', 'years')).toBe(2023); - expect(DayjsUtils.getPartial('2023-10-01', 'month')).toBe(9); // month is 0-indexed - }); - }); - - describe('getDuration', () => { - it('returns the duration object from a time string', () => { - expect(DayjsUtils.getDuration('P1D').asSeconds()).toBe(86400); - }); - }); - - describe('getDurationWithUnit', () => { - it('returns the duration object from a time and unit', () => { - expect(DayjsUtils.getDurationWithUnit(1, 'year').asYears()).toBe(1); - }); - }); - - describe('addDuration', () => { - it('adds the duration to the date', () => { - const date = new Date(2023, 9, 1); - const duration = dayjs.duration({days: 1}); - expect(DayjsUtils.addDuration(date, duration)).toEqual(new Date(2023, 9, 2)); - }); - }); - - describe('subtractDuration', () => { - it('subtracts the duration from the date', () => { - const date = new Date(2023, 9, 1); - const duration = dayjs.duration({days: 1}); - expect(DayjsUtils.subtractDuration(date, duration)).toEqual(new Date(2023, 8, 30)); - }); - }); -}); diff --git a/src/app/shared/utils/dayjs.utils.ts b/src/app/shared/utils/dayjs.utils.ts index e62f793f4..c89734bb3 100644 --- a/src/app/shared/utils/dayjs.utils.ts +++ b/src/app/shared/utils/dayjs.utils.ts @@ -1,6 +1,5 @@ import dayjs from 'dayjs'; -import {ManipulateTypeAlias as ManipulateType, UnitTypeAlias as UnitType} from '../types/dayjs-alias-type'; -import duration, {Duration} from 'dayjs/plugin/duration'; +import duration from 'dayjs/plugin/duration'; import utc from 'dayjs/plugin/utc'; import customParseFormat from 'dayjs/plugin/customParseFormat'; @@ -9,23 +8,7 @@ dayjs.extend(customParseFormat); dayjs.extend(utc); export class DayjsUtils { - public static getPartial(date: string, unit: UnitType): number { - return dayjs(date).get(unit); - } public static parseUTCDate(date: string, format?: string): Date { return dayjs.utc(date, format).toDate(); } - public static getDuration(time: string): Duration { - return dayjs.duration(time); - } - public static getDurationWithUnit(time: number, unit?: ManipulateType): Duration { - return dayjs.duration(time, unit); - } - - public static addDuration(date: Date, durationToAdd: Duration): Date { - return dayjs(date).add(durationToAdd).toDate(); - } - public static subtractDuration(date: Date, durationToSubtract: Duration): Date { - return dayjs(date).subtract(durationToSubtract).toDate(); - } } diff --git a/src/app/state/map/effects/share-link.effects.spec.ts b/src/app/state/map/effects/share-link.effects.spec.ts index 2b095e4ce..1af7bf1b2 100644 --- a/src/app/state/map/effects/share-link.effects.spec.ts +++ b/src/app/state/map/effects/share-link.effects.spec.ts @@ -35,7 +35,8 @@ import {ActiveMapItemFactory} from '../../../shared/factories/active-map-item.fa import {selectIsAuthenticated, selectIsAuthenticationInitialized} from '../../auth/reducers/auth-status.reducer'; import {MapRestoreItem} from '../../../shared/interfaces/map-restore-item.interface'; import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; -import {DayjsUtils} from '../../../shared/utils/dayjs.utils'; +import {TimeService} from '../../../shared/interfaces/time-service.interface'; +import {TIME_SERVICE} from '../../../app.module'; function createActiveMapItemsFromConfigs(activeMapItemConfigurations: ActiveMapItemConfiguration[]): ActiveMapItem[] { return activeMapItemConfigurations.map( @@ -54,88 +55,8 @@ describe('ShareLinkEffects', () => { let gb3ShareLinkService: Gb3ShareLinkService; let authServiceMock: jasmine.SpyObj; let favouriteServiceMock: jasmine.SpyObj; - - const shareLinkItemMock: ShareLinkItem = { - basemapId: 'arelkbackgroundzh', - center: {x: 2675158, y: 1259964}, - scale: 18000, - content: [ - { - id: 'StatGebAlterZH', - mapId: 'StatGebAlterZH', - layers: [ - { - id: 132494, - layer: 'geb-alter_wohnen', - visible: true, - }, - { - id: 132495, - layer: 'geb-alter_grau', - visible: false, - }, - { - id: 132496, - layer: 'geb-alter_2', - visible: true, - }, - ], - opacity: 0.5, - visible: true, - isSingleLayer: false, - timeExtent: {start: DayjsUtils.parseUTCDate('1000'), end: DayjsUtils.parseUTCDate('2020')}, - attributeFilters: [ - { - parameter: 'FILTER_GEBART', - name: 'Anzeigeoptionen nach Hauptnutzung', - activeFilters: [ - {name: 'Wohnen', isActive: false}, - {name: 'Andere', isActive: false}, - { - name: 'Gewerbe und Verwaltung', - isActive: false, - }, - ], - }, - ], - }, - { - id: 'Lageklassen2003ZH', - mapId: 'Lageklassen2003ZH', - layers: [ - { - id: 135765, - layer: 'lageklassen-2003-flaechen', - visible: true, - }, - { - id: 135775, - layer: 'lageklassen-2003-einzelobjekte', - visible: true, - }, - ], - opacity: 1, - visible: true, - isSingleLayer: false, - timeExtent: undefined, - attributeFilters: undefined, - }, - ], - drawings: { - type: 'Vector', - geojson: { - type: 'FeatureCollection', - features: [{type: 'Feature', geometry: {type: 'Point', coordinates: [0, 1]}, properties: {text: 'drawing', style: ''}}], - }, - } as Gb3VectorLayer, - measurements: { - type: 'Vector', - geojson: { - type: 'FeatureCollection', - features: [{type: 'Feature', geometry: {type: 'Point', coordinates: [0, 1]}, properties: {text: 'measurement', style: ''}}], - }, - } as Gb3VectorLayer, - }; + let timeService: TimeService; + let shareLinkItemMock: ShareLinkItem; beforeEach(() => { actions$ = new Observable(); @@ -155,7 +76,89 @@ describe('ShareLinkEffects', () => { }); effects = TestBed.inject(ShareLinkEffects); gb3ShareLinkService = TestBed.inject(Gb3ShareLinkService); + timeService = TestBed.inject(TIME_SERVICE); store = TestBed.inject(MockStore); + shareLinkItemMock = { + basemapId: 'arelkbackgroundzh', + center: {x: 2675158, y: 1259964}, + scale: 18000, + content: [ + { + id: 'StatGebAlterZH', + mapId: 'StatGebAlterZH', + layers: [ + { + id: 132494, + layer: 'geb-alter_wohnen', + visible: true, + }, + { + id: 132495, + layer: 'geb-alter_grau', + visible: false, + }, + { + id: 132496, + layer: 'geb-alter_2', + visible: true, + }, + ], + opacity: 0.5, + visible: true, + isSingleLayer: false, + timeExtent: {start: timeService.getUTCDateFromString('1000'), end: timeService.getUTCDateFromString('2020')}, + attributeFilters: [ + { + parameter: 'FILTER_GEBART', + name: 'Anzeigeoptionen nach Hauptnutzung', + activeFilters: [ + {name: 'Wohnen', isActive: false}, + {name: 'Andere', isActive: false}, + { + name: 'Gewerbe und Verwaltung', + isActive: false, + }, + ], + }, + ], + }, + { + id: 'Lageklassen2003ZH', + mapId: 'Lageklassen2003ZH', + layers: [ + { + id: 135765, + layer: 'lageklassen-2003-flaechen', + visible: true, + }, + { + id: 135775, + layer: 'lageklassen-2003-einzelobjekte', + visible: true, + }, + ], + opacity: 1, + visible: true, + isSingleLayer: false, + timeExtent: undefined, + attributeFilters: undefined, + }, + ], + drawings: { + type: 'Vector', + geojson: { + type: 'FeatureCollection', + features: [{type: 'Feature', geometry: {type: 'Point', coordinates: [0, 1]}, properties: {text: 'drawing', style: ''}}], + }, + } as Gb3VectorLayer, + measurements: { + type: 'Vector', + geojson: { + type: 'FeatureCollection', + features: [{type: 'Feature', geometry: {type: 'Point', coordinates: [0, 1]}, properties: {text: 'measurement', style: ''}}], + }, + } as Gb3VectorLayer, + }; }); describe('loadShareLinkItem$', () => { @@ -252,20 +255,25 @@ describe('ShareLinkEffects', () => { describe('Initialize the application based on a share link', () => { const expectedId = 'abcd-efgh-ijkl-mnop'; - const expectedItem = shareLinkItemMock; const expectedOriginalError = new ShareLinkPropertyCouldNotBeValidated("He's dead, Jim."); const expectedTopics: Topic[] = []; - const expectedCompleteItem: MapRestoreItem = { - activeMapItems: createActiveMapItemsFromConfigs(shareLinkItemMock.content), - scale: shareLinkItemMock.scale, - basemapId: shareLinkItemMock.basemapId, - x: shareLinkItemMock.center.x, - y: shareLinkItemMock.center.y, - drawings: [ - {id: 'mockDrawing1', source: UserDrawingLayer.Drawings, geometry: {type: 'Point', srs: 2056, coordinates: [0, 0]}}, - {id: 'mockDrawing2', source: UserDrawingLayer.Measurements, geometry: {type: 'Point', srs: 2056, coordinates: [0, 0]}}, - ] as Gb3StyledInternalDrawingRepresentation[], - }; + let expectedCompleteItem: MapRestoreItem; + let expectedItem: ShareLinkItem; + + beforeEach(() => { + expectedItem = shareLinkItemMock; + expectedCompleteItem = { + activeMapItems: createActiveMapItemsFromConfigs(shareLinkItemMock.content), + scale: shareLinkItemMock.scale, + basemapId: shareLinkItemMock.basemapId, + x: shareLinkItemMock.center.x, + y: shareLinkItemMock.center.y, + drawings: [ + {id: 'mockDrawing1', source: UserDrawingLayer.Drawings, geometry: {type: 'Point', srs: 2056, coordinates: [0, 0]}}, + {id: 'mockDrawing2', source: UserDrawingLayer.Measurements, geometry: {type: 'Point', srs: 2056, coordinates: [0, 0]}}, + ] as Gb3StyledInternalDrawingRepresentation[], + }; + }); describe('Action: Initialize Application Based On Id', () => { describe('waitForAuthenticationStatusToBeLoaded$', () => { diff --git a/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts b/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts index 31a672c12..5ee445544 100644 --- a/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts +++ b/src/app/state/map/selectors/active-map-item-configuration.selector.spec.ts @@ -2,7 +2,6 @@ import {selectActiveMapItemConfigurations} from './active-map-item-configuration import {ActiveMapItemConfiguration} from '../../../shared/interfaces/active-map-item-configuration.interface'; import {ActiveMapItemFactory} from '../../../shared/factories/active-map-item.factory'; import {Map} from '../../../shared/interfaces/topic.interface'; -import {DayjsUtils} from '../../../shared/utils/dayjs.utils'; describe('selectActiveMapItemConfiguration', () => { it('returns activeMapItemConfigurations from ActiveMapItmes', () => { @@ -30,10 +29,7 @@ describe('selectActiveMapItemConfiguration', () => { undefined, true, 0.71, - { - start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), - }, + undefined, [ { parameter: 'FILTER_GEBART', @@ -89,10 +85,7 @@ describe('selectActiveMapItemConfiguration', () => { ], }, ], - timeExtent: { - start: DayjsUtils.parseUTCDate('1000-01-01T00:00:00.000Z'), - end: DayjsUtils.parseUTCDate('2020-01-01T00:00:00.000Z'), - }, + timeExtent: undefined, }, ]; From 35d91a8f2c7b4fd7fe71d1bb82cc45b0870a0430 Mon Sep 17 00:00:00 2001 From: Lukas Merz Date: Wed, 25 Sep 2024 10:23:03 +0200 Subject: [PATCH 09/12] GB3-1361: Remove gb2wmsitem dependency on static utils classes --- .../models/implementations/gb2-wms.model.ts | 3 +- src/app/shared/interfaces/topic.interface.ts | 2 + .../services/apis/gb3/gb3-topics.service.ts | 68 +++++++++++++----- src/app/shared/services/dayjs.service.ts | 8 ++- .../map/actions/active-map-item.actions.ts | 1 + .../effects/active-map-item.effects.spec.ts | 69 ++++++++++++++++++- .../map/effects/active-map-item.effects.ts | 35 ++++++++++ .../reducers/active-map-item.reducer.spec.ts | 66 +++++------------- .../map/reducers/active-map-item.reducer.ts | 31 +++------ 9 files changed, 190 insertions(+), 93 deletions(-) diff --git a/src/app/map/models/implementations/gb2-wms.model.ts b/src/app/map/models/implementations/gb2-wms.model.ts index 2503d34d4..48300d079 100644 --- a/src/app/map/models/implementations/gb2-wms.model.ts +++ b/src/app/map/models/implementations/gb2-wms.model.ts @@ -22,8 +22,7 @@ export class Gb2WmsSettings extends AbstractActiveMapItemSettings { this.layers = layer ? [layer] : map.layers; this.timeSliderConfiguration = map.timeSliderConfiguration; if (map.timeSliderConfiguration) { - //this.timeSliderExtent = timeExtent ?? TimeSliderService.createInitialTimeSliderExtent(map.timeSliderConfiguration); // todo: solve - //this.timeSliderExtent = timeExtent ?? TimeSliderService.createInitialTimeSliderExtent(map.timeSliderConfiguration); + this.timeSliderExtent = timeExtent ?? map.initialTimeSliderExtent; } this.filterConfigurations = filterConfigurations ?? map.filterConfigurations; this.searchConfigurations = map.searchConfigurations; diff --git a/src/app/shared/interfaces/topic.interface.ts b/src/app/shared/interfaces/topic.interface.ts index 13fea44a9..b20340be2 100644 --- a/src/app/shared/interfaces/topic.interface.ts +++ b/src/app/shared/interfaces/topic.interface.ts @@ -2,6 +2,7 @@ import {HasVisibility} from '../../map/interfaces/has-visibility.interface'; import {HasHidingState} from './has-hiding-state.interface'; import {HasActiveState} from './has-active-state.interface'; import {HasOpacity} from '../../map/interfaces/has-opacity.interface'; +import {TimeExtent} from '../../map/interfaces/time-extent.interface'; export interface Topic { title: string; @@ -36,6 +37,7 @@ export interface Map extends HasOpacity { permissionMissing?: boolean; /** Timeslider Settings */ timeSliderConfiguration?: TimeSliderConfiguration; + initialTimeSliderExtent?: TimeExtent; /** Filters Settings */ filterConfigurations?: FilterConfiguration[]; searchConfigurations?: SearchConfiguration[]; diff --git a/src/app/shared/services/apis/gb3/gb3-topics.service.ts b/src/app/shared/services/apis/gb3/gb3-topics.service.ts index aca5ec875..c593cbc4e 100644 --- a/src/app/shared/services/apis/gb3/gb3-topics.service.ts +++ b/src/app/shared/services/apis/gb3/gb3-topics.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {forkJoin, Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {DataCataloguePage} from '../../../enums/data-catalogue-page.enum'; @@ -10,6 +10,7 @@ import { FilterValue, Map, MapLayer, + TimeSliderConfiguration, TimeSliderLayer, TimeSliderLayerSource, TimeSliderParameterSource, @@ -31,6 +32,12 @@ import {QueryTopic} from '../../../interfaces/query-topic.interface'; import {ApiGeojsonGeometryToGb3ConverterUtils} from '../../../utils/api-geojson-geometry-to-gb3-converter.utils'; import {LinkObject} from '../../../interfaces/link-object.interface'; import {GeometryWithSrs} from '../../../interfaces/geojson-types-with-srs.interface'; +import {HttpClient} from '@angular/common/http'; +import {ConfigService} from '../../config.service'; +import {TIME_SERVICE} from '../../../../app.module'; +import {TimeService} from '../../../interfaces/time-service.interface'; +import {TimeSliderService} from '../../../../map/services/time-slider.service'; +import {TimeExtent} from '../../../../map/interfaces/time-extent.interface'; const INACTIVE_STRING_FILTER_VALUE = ''; const INACTIVE_NUMBER_FILTER_VALUE = -1; @@ -44,6 +51,15 @@ export class Gb3TopicsService extends Gb3ApiService { private readonly dataDatasetTabUrl = `/${MainPage.Data}/${DataCataloguePage.Datasets}`; private readonly dataMapTabUrl = `/${MainPage.Data}/${DataCataloguePage.Maps}`; + constructor( + httpClient: HttpClient, + configService: ConfigService, + @Inject(TIME_SERVICE) timeService: TimeService, + private readonly timeSliderService: TimeSliderService, + ) { + super(httpClient, configService, timeService); + } + public loadTopics(): Observable { const requestUrl = this.createTopicsUrl(); const topicsListData = this.get(requestUrl); @@ -187,23 +203,7 @@ export class Gb3TopicsService extends Gb3ApiService { ) .reverse(), // reverse the order of the layers because the order in the GB3 interfaces (Topic, ActiveMapItem) is inverted // to the order of the WMS specifications - timeSliderConfiguration: topic.timesliderConfiguration - ? { - name: topic.timesliderConfiguration.name, - alwaysMaxRange: topic.timesliderConfiguration.alwaysMaxRange, - dateFormat: topic.timesliderConfiguration.dateFormat, - description: topic.timesliderConfiguration.description, - maximumDate: topic.timesliderConfiguration.maximumDate, - minimumDate: topic.timesliderConfiguration.minimumDate, - minimalRange: topic.timesliderConfiguration.minimalRange, - range: topic.timesliderConfiguration.range, - sourceType: topic.timesliderConfiguration.sourceType as TimeSliderSourceType, - source: this.transformTimeSliderConfigurationSource( - topic.timesliderConfiguration.source, - topic.timesliderConfiguration.sourceType, - ), - } - : undefined, + ...this.handleTimeSliderConfiguration(topic.timesliderConfiguration), filterConfigurations: topic.filterConfigurations?.map((filterConfiguration): FilterConfiguration => { return { name: filterConfiguration.name, @@ -234,6 +234,38 @@ export class Gb3TopicsService extends Gb3ApiService { return topicsResponse; } + private handleTimeSliderConfiguration( + timesliderConfiguration: TopicsListData['categories'][0]['topics'][0]['timesliderConfiguration'] | undefined, + ): + | { + initialTimeSliderExtent: undefined; + timeSliderConfiguration: undefined; + } + | {initialTimeSliderExtent: TimeExtent; timeSliderConfiguration: TimeSliderConfiguration} { + if (!timesliderConfiguration) { + return { + timeSliderConfiguration: undefined, + initialTimeSliderExtent: undefined, + }; + } + const config: TimeSliderConfiguration = { + name: timesliderConfiguration.name, + alwaysMaxRange: timesliderConfiguration.alwaysMaxRange, + dateFormat: timesliderConfiguration.dateFormat, + description: timesliderConfiguration.description, + maximumDate: timesliderConfiguration.maximumDate, + minimumDate: timesliderConfiguration.minimumDate, + minimalRange: timesliderConfiguration.minimalRange, + range: timesliderConfiguration.range, + sourceType: timesliderConfiguration.sourceType as TimeSliderSourceType, + source: this.transformTimeSliderConfigurationSource(timesliderConfiguration.source, timesliderConfiguration.sourceType), + }; + return { + timeSliderConfiguration: config, + initialTimeSliderExtent: this.timeSliderService.createInitialTimeSliderExtent(config), + }; + } + private transformTimeSliderConfigurationSource( // the following typing for `source` is used to extract a subtype of the generated interface `TopicsListData` source: TopicsListData['categories'][0]['topics'][0]['timesliderConfiguration']['source'], diff --git a/src/app/shared/services/dayjs.service.ts b/src/app/shared/services/dayjs.service.ts index fc1a4e42b..9a4ee4130 100644 --- a/src/app/shared/services/dayjs.service.ts +++ b/src/app/shared/services/dayjs.service.ts @@ -1,7 +1,13 @@ import {Injectable} from '@angular/core'; import {DateUnit, TimeService} from '../interfaces/time-service.interface'; import dayjs, {ManipulateType} from 'dayjs'; -import {Duration} from 'dayjs/plugin/duration'; +import duration, {Duration} from 'dayjs/plugin/duration'; +import utc from 'dayjs/plugin/utc'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; + +dayjs.extend(duration); +dayjs.extend(customParseFormat); +dayjs.extend(utc); @Injectable({ providedIn: 'root', diff --git a/src/app/state/map/actions/active-map-item.actions.ts b/src/app/state/map/actions/active-map-item.actions.ts index 8d02e84e5..9d0f66173 100644 --- a/src/app/state/map/actions/active-map-item.actions.ts +++ b/src/app/state/map/actions/active-map-item.actions.ts @@ -12,6 +12,7 @@ export const ActiveMapItemActions = createActionGroup({ events: { 'Add Active Map Item': props<{activeMapItem: ActiveMapItem; position: number}>(), 'Remove Active Map Item': props<{activeMapItem: ActiveMapItem}>(), + 'Replace Active Map Item': props<{activeMapItem: ActiveMapItem}>(), 'Remove All Active Map Items': emptyProps(), 'Remove Temporary Active Map Item': props<{activeMapItem: ActiveMapItem}>(), 'Remove All Temporary Active Map Items': emptyProps(), diff --git a/src/app/state/map/effects/active-map-item.effects.spec.ts b/src/app/state/map/effects/active-map-item.effects.spec.ts index 532297eb2..3926bae21 100644 --- a/src/app/state/map/effects/active-map-item.effects.spec.ts +++ b/src/app/state/map/effects/active-map-item.effects.spec.ts @@ -20,7 +20,13 @@ import {MapUiActions} from '../actions/map-ui.actions'; import {FeatureInfoActions} from '../actions/feature-info.actions'; import {TimeExtent} from '../../../map/interfaces/time-extent.interface'; import {ActiveMapItemFactory} from '../../../shared/factories/active-map-item.factory'; -import {FilterConfiguration, Map} from '../../../shared/interfaces/topic.interface'; +import { + FilterConfiguration, + Map, + MapLayer, + TimeSliderConfiguration, + TimeSliderLayerSource, +} from '../../../shared/interfaces/topic.interface'; import {MapConfigActions} from '../actions/map-config.actions'; import {FavouriteBaseConfig} from '../../../shared/interfaces/favourite.interface'; import {PointWithSrs} from '../../../shared/interfaces/geojson-types-with-srs.interface'; @@ -30,6 +36,7 @@ import {DrawingActions} from '../actions/drawing.actions'; import {LayerCatalogActions} from '../actions/layer-catalog.actions'; import {SearchActions} from '../../app/actions/search.actions'; import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; +import {Gb2WmsActiveMapItem} from '../../../map/models/implementations/gb2-wms.model'; describe('ActiveMapItemEffects', () => { let actions$: Observable; @@ -557,4 +564,64 @@ describe('ActiveMapItemEffects', () => { }); }); }); + + describe('setTimeSliderExtent$', () => { + it('dispatches nothing if item does not exist', fakeAsync(() => { + const timeExtent: TimeExtent = { + start: new Date(2023, 0, 1), + end: new Date(2023, 11, 31), + }; + const activeMapItem = createGb2WmsMapItemMock('id'); + store.overrideSelector(selectItems, []); + + let newAction; + actions$ = of(ActiveMapItemActions.setTimeSliderExtent({timeExtent, activeMapItem})); + effects.setTimeSliderExtent$.subscribe((action) => (newAction = action)); + flush(); + + expect(newAction).toBeUndefined(); + })); + + it('sets the time extent and reevaluates all layer visibilities, dispatches replaceActiveMapItem', () => { + const timeExtent: TimeExtent = { + start: new Date(2023, 0, 1), + end: new Date(2023, 11, 31), + }; + const mapMock: Partial = {id: 'id'}; + mapMock.layers = [ + {layer: 'layer01', visible: false} as MapLayer, + {layer: 'layer02', visible: false} as MapLayer, + {layer: 'layer03', visible: false} as MapLayer, + ]; + mapMock.timeSliderConfiguration = { + dateFormat: 'YYYY-MM-DD', + sourceType: 'layer', + source: { + layers: [ + {layerName: 'layer01', date: '2022-06-30'}, + {layerName: 'layer02', date: '2023-06-30'}, + {layerName: 'layer03', date: '2024-06-30'}, + ], + } as TimeSliderLayerSource, + } as TimeSliderConfiguration; + const activeMapItem = ActiveMapItemFactory.createGb2WmsMapItem(mapMock); + store.overrideSelector(selectItems, [activeMapItem]); + + actions$ = of(ActiveMapItemActions.setTimeSliderExtent({timeExtent, activeMapItem})); + + effects.setTimeSliderExtent$.subscribe((action) => { + const expectedTimeExtent = timeExtent; + const expectedLayers: Partial[] = [ + {layer: 'layer01', visible: false}, + {layer: 'layer02', visible: true}, + {layer: 'layer03', visible: false}, + ]; + + expect(action).toEqual(ActiveMapItemActions.replaceActiveMapItem({activeMapItem: activeMapItem})); + expect(action.activeMapItem).toBeInstanceOf(Gb2WmsActiveMapItem); + expect((action.activeMapItem).settings.timeSliderExtent).toEqual(expectedTimeExtent); + expect((action.activeMapItem).settings.layers).toEqual(expectedLayers); + }); + }); + }); }); diff --git a/src/app/state/map/effects/active-map-item.effects.ts b/src/app/state/map/effects/active-map-item.effects.ts index 00177942e..ecd49704c 100644 --- a/src/app/state/map/effects/active-map-item.effects.ts +++ b/src/app/state/map/effects/active-map-item.effects.ts @@ -26,6 +26,7 @@ import {DrawingActiveMapItem} from '../../../map/models/implementations/drawing. import {DrawingActions} from '../actions/drawing.actions'; import {LayerCatalogActions} from '../actions/layer-catalog.actions'; import {SearchActions} from '../../app/actions/search.actions'; +import {TimeSliderService} from '../../../map/services/time-slider.service'; @Injectable() export class ActiveMapItemEffects { @@ -354,11 +355,45 @@ export class ActiveMapItemEffects { ); }); + public setTimeSliderExtent$ = createEffect(() => { + return this.actions$.pipe( + ofType(ActiveMapItemActions.setTimeSliderExtent), + concatLatestFrom(() => this.store.select(selectItems)), + map(([{activeMapItem, timeExtent}, activeMapItems]) => { + const existingMapItem = activeMapItems + .filter(isActiveMapItemOfType(Gb2WmsActiveMapItem)) + .find((mapItem) => mapItem.id === activeMapItem.id); + + if (!existingMapItem) { + return undefined; + } + const clonedMapItem = structuredClone(existingMapItem); + + clonedMapItem.settings.timeSliderExtent = timeExtent; + clonedMapItem.settings.layers.forEach((layer) => { + const isVisible = this.timeSliderService.isLayerVisible( + layer, + existingMapItem.settings.timeSliderConfiguration, + existingMapItem.settings.timeSliderExtent, + ); + if (isVisible !== undefined) { + layer.visible = isVisible; + } + }); + + return clonedMapItem; + }), + filter((a) => a !== undefined), + map((a) => ActiveMapItemActions.replaceActiveMapItem({activeMapItem: a})), + ); + }); + constructor( private readonly actions$: Actions, @Inject(MAP_SERVICE) private readonly mapService: MapService, private readonly gb3TopicsService: Gb3TopicsService, private readonly store: Store, private readonly configService: ConfigService, + private readonly timeSliderService: TimeSliderService, ) {} } diff --git a/src/app/state/map/reducers/active-map-item.reducer.spec.ts b/src/app/state/map/reducers/active-map-item.reducer.spec.ts index aff26d192..997923be1 100644 --- a/src/app/state/map/reducers/active-map-item.reducer.spec.ts +++ b/src/app/state/map/reducers/active-map-item.reducer.spec.ts @@ -8,16 +8,9 @@ import {Gb2WmsActiveMapItem} from '../../../map/models/implementations/gb2-wms.m import {LoadingState} from '../../../shared/types/loading-state.type'; import {ViewProcessState} from '../../../shared/types/view-process-state.type'; import {isActiveMapItemOfType} from '../../../shared/type-guards/active-map-item-type.type-guard'; -import { - FilterConfiguration, - Map, - MapLayer, - TimeSliderConfiguration, - TimeSliderLayerSource, -} from '../../../shared/interfaces/topic.interface'; +import {FilterConfiguration, Map} from '../../../shared/interfaces/topic.interface'; import {ActiveMapItemFactory} from '../../../shared/factories/active-map-item.factory'; import {FavouriteBaseConfig} from '../../../shared/interfaces/favourite.interface'; -import {TimeExtent} from '../../../map/interfaces/time-extent.interface'; describe('ActiveMapItem Reducer', () => { const activeMapItemsMock: ActiveMapItem[] = [ @@ -343,48 +336,6 @@ describe('ActiveMapItem Reducer', () => { }); }); - describe('setTimeSliderExtent', () => { - it('sets the time extend and reevaluates all layer visibilities', () => { - const timeExtent: TimeExtent = { - start: new Date(2023, 0, 1), - end: new Date(2023, 11, 31), - }; - const mapMock: Partial = {id: 'id'}; - mapMock.layers = [ - {layer: 'layer01', visible: false} as MapLayer, - {layer: 'layer02', visible: false} as MapLayer, - {layer: 'layer03', visible: false} as MapLayer, - ]; - mapMock.timeSliderConfiguration = { - dateFormat: 'YYYY-MM-DD', - sourceType: 'layer', - source: { - layers: [ - {layerName: 'layer01', date: '2022-06-30'}, - {layerName: 'layer02', date: '2023-06-30'}, - {layerName: 'layer03', date: '2024-06-30'}, - ], - } as TimeSliderLayerSource, - } as TimeSliderConfiguration; - const activeMapItem = ActiveMapItemFactory.createGb2WmsMapItem(mapMock); - existingState.items = [activeMapItem]; - - const action = ActiveMapItemActions.setTimeSliderExtent({timeExtent, activeMapItem}); - const state = reducer(existingState, action); - - const expectedTimeExtent = timeExtent; - const expectedLayers: Partial[] = [ - {layer: 'layer01', visible: false}, - {layer: 'layer02', visible: true}, - {layer: 'layer03', visible: false}, - ]; - - expect(state.items[0]).toBeInstanceOf(Gb2WmsActiveMapItem); - expect((state.items[0]).settings.timeSliderExtent).toEqual(expectedTimeExtent); - expect((state.items[0]).settings.layers).toEqual(expectedLayers); - }); - }); - describe('setAttributeFilterValueState', () => { const filterConfigurationsMock: FilterConfiguration[] = [ { @@ -484,4 +435,19 @@ describe('ActiveMapItem Reducer', () => { expect(state.items.filter(isActiveMapItemOfType(Gb2WmsActiveMapItem)).every((item) => item.settings.isNoticeMarkedAsRead)).toBeTrue(); }); }); + + describe('replaceActiveMapItem', () => { + it('replaces the active map item correctly', () => { + const modifiedItem = structuredClone(activeMapItemsMock[1]); + const modifiedOpacity = activeMapItemsMock[1].opacity + 1337; + modifiedItem.opacity = modifiedOpacity; + + existingState.items = activeMapItemsMock; + + const action = ActiveMapItemActions.replaceActiveMapItem({activeMapItem: modifiedItem}); + const state = reducer(existingState, action); + + expect((state.items[1]).opacity).toEqual(modifiedOpacity); + }); + }); }); diff --git a/src/app/state/map/reducers/active-map-item.reducer.ts b/src/app/state/map/reducers/active-map-item.reducer.ts index 9a4ef43bb..52dfd7d66 100644 --- a/src/app/state/map/reducers/active-map-item.reducer.ts +++ b/src/app/state/map/reducers/active-map-item.reducer.ts @@ -4,7 +4,6 @@ import {ActiveMapItemState} from '../states/active-map-item.state'; import {produce} from 'immer'; import {isActiveMapItemOfType} from '../../../shared/type-guards/active-map-item-type.type-guard'; import {Gb2WmsActiveMapItem} from '../../../map/models/implementations/gb2-wms.model'; -import {ActiveTimeSliderLayersUtils} from '../../../map/utils/active-time-slider-layers.utils'; export const activeMapItemFeatureKey = 'activeMapItem'; @@ -137,26 +136,6 @@ export const activeMapItemFeature = createFeature({ }); }), ), - on( - ActiveMapItemActions.setTimeSliderExtent, - produce((draft, {timeExtent, activeMapItem}) => { - draft.items.filter(isActiveMapItemOfType(Gb2WmsActiveMapItem)).forEach((mapItem) => { - if (mapItem.id === activeMapItem.id) { - mapItem.settings.timeSliderExtent = timeExtent; - mapItem.settings.layers.forEach((layer) => { - const isVisible = ActiveTimeSliderLayersUtils.isLayerVisible( - layer, - mapItem.settings.timeSliderConfiguration, - mapItem.settings.timeSliderExtent, - ); - if (isVisible !== undefined) { - layer.visible = isVisible; - } - }); - } - }); - }), - ), on( ActiveMapItemActions.setAttributeFilterValueState, produce((draft, {isFilterValueActive, filterValueName, attributeFilterParameter, activeMapItem}) => { @@ -192,6 +171,16 @@ export const activeMapItemFeature = createFeature({ }); }), ), + on( + ActiveMapItemActions.replaceActiveMapItem, + produce((draft, {activeMapItem}) => { + const existing = draft.items.find((mapItem) => mapItem.id === activeMapItem.id); + if (existing) { + const index = draft.items.indexOf(existing); + draft.items.splice(index, 1, activeMapItem); + } + }), + ), ), }); From e5087ef82cfd86094f53a954b13e64286c4ae1e3 Mon Sep 17 00:00:00 2001 From: Lukas Merz Date: Wed, 25 Sep 2024 10:27:04 +0200 Subject: [PATCH 10/12] GB3-1361: Cleanup --- README.md | 11 +- .../time-slider/time-slider.component.ts | 7 +- src/app/map/pipes/date-to-string.pipe.spec.ts | 2 +- .../pipes/time-extent-to-string.pipe.spec.ts | 2 +- .../map/services/favourites.service.spec.ts | 535 +++++++++--------- src/app/map/services/favourites.service.ts | 3 +- .../map/services/time-slider.service.spec.ts | 252 +++++++-- src/app/map/services/time-slider.service.ts | 29 +- .../active-time-slider-layers.utils.spec.ts | 122 ---- .../utils/active-time-slider-layers.utils.ts | 28 - .../interfaces/time-service.interface.ts | 74 ++- src/app/shared/interfaces/topic.interface.ts | 20 +- .../services/abstract-storage.service.ts | 26 +- .../apis/gb3/gb3-favourites.service.ts | 4 +- .../apis/gb3/gb3-share-link.service.ts | 4 +- .../apis/gb3/gb3-topics.service.spec.ts | 4 + .../services/apis/gb3/gb3-topics.service.ts | 9 +- .../apis/grav-cms/grav-cms.service.ts | 10 +- src/app/shared/services/dayjs.service.spec.ts | 15 +- src/app/shared/services/dayjs.service.ts | 16 +- src/app/shared/types/date-unit.type.ts | 1 + src/app/shared/types/dayjs-alias-type.ts | 4 - src/app/shared/utils/dayjs.utils.ts | 14 - .../auth/effects/auth-status.effects.spec.ts | 12 +- .../map/actions/active-map-item.actions.ts | 2 +- .../effects/active-map-item.effects.spec.ts | 17 +- .../map/effects/active-map-item.effects.ts | 30 +- .../map/effects/share-link.effects.spec.ts | 2 +- .../reducers/active-map-item.reducer.spec.ts | 6 +- .../map/reducers/active-map-item.reducer.ts | 6 +- src/test.ts | 1 - 31 files changed, 634 insertions(+), 634 deletions(-) delete mode 100644 src/app/map/utils/active-time-slider-layers.utils.spec.ts delete mode 100644 src/app/map/utils/active-time-slider-layers.utils.ts create mode 100644 src/app/shared/types/date-unit.type.ts delete mode 100644 src/app/shared/types/dayjs-alias-type.ts delete mode 100644 src/app/shared/utils/dayjs.utils.ts diff --git a/README.md b/README.md index ded958302..3cdb3b7b9 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,7 @@ match. This is because there are times when you _might_ want to deviate from the > 9. [Application Initialization based on share link](#application-initialization-based-on-share-link) > 10. [Adding new NPM packages](#adding-new-npm-packages) > 11. [Feature flags](#feature-flags) +> 12. [Handling of date objects](#handling-of-date-objects) ### The `ActiveMapItem` class @@ -600,6 +601,12 @@ Feature flags can be used to toggle features throughout the application. They wo - The `FeatureFlagsService` and its `getFeatureFlag` method is used to access the feature flags. - For convenience, the `FeatureFlagDirective` can be used to toggle elements based on a feature flag. +### Handling of date objects + +Currently, we are using [dayjs](https://day.js.org/) to handle date objects. In order to have a high degree of abstraction and to be able to easily replace the library (i.e. using native Javascript features like `Intl`), +all date handlings are done via the `TimeService` interface, which is implemented as e.g. the `DayjsService`. Currently, the actual implementation is injected via the `timeServiceFactory`; and as a convenience, this is also +added in `test.ts` so it does not have to be provided for each and every test. + ## Git conventions ### Branching strategy @@ -661,7 +668,3 @@ It will be used later within a Teams announcement. 3. Change the type of article by choosing **Ankündigung** (left of **Veröffentlichen**) 4. Enter the release title as _Überschrift_. E.g. `Release 42` 5. Add the changelog from above as text and slightly format it. Use previous changelogs as styleguide. - -## Contributors - -### Individual contributors diff --git a/src/app/map/components/time-slider/time-slider.component.ts b/src/app/map/components/time-slider/time-slider.component.ts index f7044da58..229be1333 100644 --- a/src/app/map/components/time-slider/time-slider.component.ts +++ b/src/app/map/components/time-slider/time-slider.component.ts @@ -4,7 +4,8 @@ import {TimeSliderConfiguration, TimeSliderLayerSource} from '../../../shared/in import {TimeSliderService} from '../../services/time-slider.service'; import {MatDatepicker} from '@angular/material/datepicker'; import {TIME_SERVICE} from '../../../app.module'; -import {DateUnit, TimeService} from '../../../shared/interfaces/time-service.interface'; +import {TimeService} from '../../../shared/interfaces/time-service.interface'; +import {DateUnit} from '../../../shared/types/date-unit.type'; // There is an array (`allowedDatePickerManipulationUnits`) and a new union type (`DatePickerManipulationUnits`) for two reasons: // To be able to extract a union type subset of `ManipulateType` AND to have an array used to check if a given value is in said union type. @@ -147,7 +148,7 @@ export class TimeSliderComponent implements OnInit, OnChanges { } // format the given event date to the configured time format and back to ensure that it is a valid date within the current available dates - const date = this.timeService.getDateFromString( + const date = this.timeService.createDateFromString( this.timeService.getDateAsFormattedString(eventDate, this.timeSliderConfiguration.dateFormat), this.timeSliderConfiguration.dateFormat, ); @@ -199,7 +200,7 @@ export class TimeSliderComponent implements OnInit, OnChanges { private isLayerSourceContinuous(layerSource: TimeSliderLayerSource, unit: DatePickerManipulationUnits): boolean { const dateAsAscendingSortedNumbers = layerSource.layers - .map((layer) => this.timeService.getPartialFromString(layer.date, unit)) + .map((layer) => this.timeService.createPartialFromString(layer.date, unit)) .sort((a, b) => a - b); // all date numbers must be part of a continuous and strictly monotonously rising series each with exactly // one step between them: `date[0] = x` => `date[n] = x + n` diff --git a/src/app/map/pipes/date-to-string.pipe.spec.ts b/src/app/map/pipes/date-to-string.pipe.spec.ts index f74f4274d..2715b801f 100644 --- a/src/app/map/pipes/date-to-string.pipe.spec.ts +++ b/src/app/map/pipes/date-to-string.pipe.spec.ts @@ -13,7 +13,7 @@ describe('DateToStringPipe', () => { pipe = new DateToStringPipe(timeService); }); - it('create an instance', () => { + it('creates an instance', () => { expect(pipe).toBeTruthy(); }); diff --git a/src/app/map/pipes/time-extent-to-string.pipe.spec.ts b/src/app/map/pipes/time-extent-to-string.pipe.spec.ts index 2d20a6dc7..24319706c 100644 --- a/src/app/map/pipes/time-extent-to-string.pipe.spec.ts +++ b/src/app/map/pipes/time-extent-to-string.pipe.spec.ts @@ -13,7 +13,7 @@ describe('TimeExtentToStringPipe', () => { timeService = TestBed.inject(TIME_SERVICE); pipe = new TimeExtentToStringPipe(timeService); }); - it('create an instance', () => { + it('creates an instance', () => { expect(pipe).toBeTruthy(); }); diff --git a/src/app/map/services/favourites.service.spec.ts b/src/app/map/services/favourites.service.spec.ts index 351a64cd7..e89c149cc 100644 --- a/src/app/map/services/favourites.service.spec.ts +++ b/src/app/map/services/favourites.service.spec.ts @@ -85,246 +85,251 @@ describe('FavouritesService', () => { }); describe('getActiveMapItemsForFavourite', () => { - const availableMaps: Map[] = [ - { - id: 'FaBoFFFZH', - uuid: '27fd3dc4-a4d8-450c-837f-70dd2f5cd5fe', - printTitle: 'Fruchtfolgeflächen (FFF)', - gb2Url: null, - icon: 'https://maps.zh.ch/images/custom/themekl-fabofffzh.gif', - wmsUrl: 'https://maps.zh.ch/wms/FaBoFFFZH', - minScale: 2500, - organisation: 'ALN Bodenschutz', - notice: null, - title: 'Fruchtfolgeflächen (FFF)', - keywords: ['Fruchtfolgeflächen', '(FFF)', 'bvv', 'boden', 'TBAK2', 'fsla', 'fabo', 'vp', 'fap'], - opacity: 1, - layers: [ - { - id: 157886, - layer: 'fff', - title: 'Fruchtfolgeflächen', - queryable: true, - uuid: '0aa893ad-c264-ce46-bf1f-6fa785998b8c', - groupTitle: 'Fruchtfolgeflächen', - minScale: 1, - maxScale: 50000, - wmsSort: 2, - tocSort: 200, - visible: true, - isHidden: false, - }, - { - id: 157885, - layer: 'perimeter-fff', - title: 'Fruchtfolgeflächen', - queryable: true, - uuid: '0aa893ad-c264-ce46-bf1f-6fa785998b8c', - groupTitle: 'Fruchtfolgeflächen', - minScale: 50000, - maxScale: 500000, - wmsSort: 1, - tocSort: 100, - visible: true, - isHidden: false, - }, - ], - }, - { - id: 'AVfarbigZH', - uuid: '26d7c027-38f2-42cb-a17a-99f17a2e383e', - printTitle: 'Amtliche Vermessung in Farbe', - gb2Url: null, - icon: 'https://maps.zh.ch/images/custom/themekl-avfarbigzh.gif', - wmsUrl: 'https://maps.zh.ch/wms/AVfarbigZH', - minScale: 100, - organisation: 'ARE Geoinformation', - notice: null, - title: 'Amtliche Vermessung in Farbe', - keywords: ['Amtliche', 'Vermessung', 'in', 'Farbe', 'pk', 'Amtlichen Vermessung', 'AV'], - opacity: 1, - layers: [ - { - id: 151493, - layer: 'liegensch-einzelobj', - title: 'Einzelobjekte (Flächen) innerhalb Liegenschaften', - queryable: true, - uuid: null, - groupTitle: null, - minScale: 99, - maxScale: 2500, - wmsSort: 49, - tocSort: 4900, - visible: true, - isHidden: false, - }, - { - id: 151492, - layer: 'TBBP', - title: 'Hoheitsgrenzpunkte', - queryable: true, - uuid: '1466f09e-702d-7b38-053c-7b85f8e82549', - groupTitle: 'Hoheitsgrenzen', - minScale: 99, - maxScale: 1000, - wmsSort: 47, - tocSort: 9099, - visible: true, - isHidden: false, - }, - { - id: 151491, - layer: 'av-fixpunkte-nummern', - title: 'Fixpunkte Nummern', - queryable: false, - uuid: '75fe4385-de51-3588-40e2-be8575166f2a', - groupTitle: 'Fixpunkte', - minScale: 99, - maxScale: 1000, - wmsSort: 46, - tocSort: 9089, - visible: true, - isHidden: false, - }, - ], - searchConfigurations: [ - { - index: 'gvz', - title: 'GVZ-Nr.', - }, - ], - }, - { - id: 'StatGebAlterZH', - uuid: '246fe226-ead7-4f91-b735-d294994913e0', - printTitle: 'Gebäudealter', - gb2Url: null, - icon: 'https://maps.zh.ch/images/custom/themekl-statgebalterzh.gif', - wmsUrl: 'https://maps.zh.ch/wms/StatGebAlterZH', - minScale: null, - organisation: 'Statistisches Amt', - notice: null, - title: 'Gebäudealter', - keywords: ['Gebäudealter', 'stat', 'obs', 'fap', 'denkk', 'fsla'], - opacity: 1, - layers: [ - { - id: 160331, - layer: 'geb-alter_2', - title: 'Gebäude mit Baujahr x und älter', - queryable: false, - uuid: null, - groupTitle: 'Gebäudealter', - minScale: 100001, - maxScale: 15000001, - wmsSort: 11, - tocSort: 1100, - visible: true, - isHidden: false, - }, - { - id: 160330, - layer: 'geb-alter_grau', - title: 'Baujahr', - queryable: false, - uuid: null, - groupTitle: 'Gebäudealter - Polygone', - minScale: 1, - maxScale: 100000, - wmsSort: 10, - tocSort: 1000, - visible: false, - isHidden: false, - }, - { - id: 160329, - layer: 'geb-alter_wohnen', - title: 'Baujahr', - queryable: true, - uuid: null, - groupTitle: 'Gebäudealter - Polygone', - minScale: 1, - maxScale: 100000, - wmsSort: 9, - tocSort: 900, - visible: true, - isHidden: false, - }, - ], - timeSliderConfiguration: { - name: 'Aktueller Gebäudebestand nach Baujahr', - alwaysMaxRange: false, - dateFormat: 'YYYY', - description: 'Gebäude bis 2020', - maximumDate: '2020', - minimumDate: '1000', - minimalRange: 'P1Y', - sourceType: 'parameter', - source: { - startRangeParameter: 'FILTER_VON', - endRangeParameter: 'FILTER_BIS', - layerIdentifiers: ['geb-alter_wohnen', 'geb-alter_grau', 'geb-alter_2'], - }, - }, - filterConfigurations: [ - { - name: 'Anzeigeoptionen nach Hauptnutzung', - parameter: 'FILTER_GEBART', - filterValues: [ - { - isActive: true, - values: ['Gebäude Wohnen'], - name: 'Wohnen', - }, - { - isActive: false, - values: ['Gebäude Wohnen'], - name: 'Gewerbe und Verwaltung', - }, - { - isActive: false, - values: ['Gebäude Wohnen'], - name: 'Andere', - }, - ], - }, - ], - }, - { - id: 'Lidar2021BefliegungZH', - uuid: '1dac9be1-1412-45dd-a1dd-c151c737272b', - printTitle: 'LiDAR-Befliegung 2021 ZH', - gb2Url: null, - icon: 'https://maps.zh.ch/images/custom/themekl-lidar2021befliegungzh.gif', - wmsUrl: 'https://maps.zh.ch/wms/Lidar2021BefliegungZH', - minScale: null, - organisation: 'ARE Geoinformation', - notice: - 'Die kantonale LiDAR-Befliegungen sind die Basis für die Berechnung der Höhemodelle und decken den ganzen Kanton ab. Diese Karte zeigt die abgedeckte Fläche jeder aufgenommenen Überfliegung, die während dem Projekt stattgefunden hat. Die Überlappung der Flächen wird benötigt, um die einzelnen Streifen zusammenführen zu können und eine höhere Genauigkeit zu erhalten.', - title: 'LiDAR-Befliegung 2021 ZH', - keywords: ['LiDAR-Befliegung', '2021', 'ZH'], - opacity: 1, - layers: [ - { - id: 159533, - layer: 'lidarbefliegung', - title: 'LiDAR-Befliegung', - queryable: true, - uuid: '10b88da3-3715-44f5-9480-eb754955a892', - groupTitle: 'Inventar', - minScale: 1, - maxScale: 1000000, - wmsSort: 0, - tocSort: 0, - visible: true, - isHidden: false, - }, - ], - }, - ]; + let availableMaps: Map[]; beforeEach(() => { // eslint-disable-next-line @typescript-eslint/dot-notation + availableMaps = [ + { + id: 'FaBoFFFZH', + uuid: '27fd3dc4-a4d8-450c-837f-70dd2f5cd5fe', + printTitle: 'Fruchtfolgeflächen (FFF)', + gb2Url: null, + icon: 'https://maps.zh.ch/images/custom/themekl-fabofffzh.gif', + wmsUrl: 'https://maps.zh.ch/wms/FaBoFFFZH', + minScale: 2500, + organisation: 'ALN Bodenschutz', + notice: null, + title: 'Fruchtfolgeflächen (FFF)', + keywords: ['Fruchtfolgeflächen', '(FFF)', 'bvv', 'boden', 'TBAK2', 'fsla', 'fabo', 'vp', 'fap'], + opacity: 1, + layers: [ + { + id: 157886, + layer: 'fff', + title: 'Fruchtfolgeflächen', + queryable: true, + uuid: '0aa893ad-c264-ce46-bf1f-6fa785998b8c', + groupTitle: 'Fruchtfolgeflächen', + minScale: 1, + maxScale: 50000, + wmsSort: 2, + tocSort: 200, + visible: true, + isHidden: false, + }, + { + id: 157885, + layer: 'perimeter-fff', + title: 'Fruchtfolgeflächen', + queryable: true, + uuid: '0aa893ad-c264-ce46-bf1f-6fa785998b8c', + groupTitle: 'Fruchtfolgeflächen', + minScale: 50000, + maxScale: 500000, + wmsSort: 1, + tocSort: 100, + visible: true, + isHidden: false, + }, + ], + }, + { + id: 'AVfarbigZH', + uuid: '26d7c027-38f2-42cb-a17a-99f17a2e383e', + printTitle: 'Amtliche Vermessung in Farbe', + gb2Url: null, + icon: 'https://maps.zh.ch/images/custom/themekl-avfarbigzh.gif', + wmsUrl: 'https://maps.zh.ch/wms/AVfarbigZH', + minScale: 100, + organisation: 'ARE Geoinformation', + notice: null, + title: 'Amtliche Vermessung in Farbe', + keywords: ['Amtliche', 'Vermessung', 'in', 'Farbe', 'pk', 'Amtlichen Vermessung', 'AV'], + opacity: 1, + layers: [ + { + id: 151493, + layer: 'liegensch-einzelobj', + title: 'Einzelobjekte (Flächen) innerhalb Liegenschaften', + queryable: true, + uuid: null, + groupTitle: null, + minScale: 99, + maxScale: 2500, + wmsSort: 49, + tocSort: 4900, + visible: true, + isHidden: false, + }, + { + id: 151492, + layer: 'TBBP', + title: 'Hoheitsgrenzpunkte', + queryable: true, + uuid: '1466f09e-702d-7b38-053c-7b85f8e82549', + groupTitle: 'Hoheitsgrenzen', + minScale: 99, + maxScale: 1000, + wmsSort: 47, + tocSort: 9099, + visible: true, + isHidden: false, + }, + { + id: 151491, + layer: 'av-fixpunkte-nummern', + title: 'Fixpunkte Nummern', + queryable: false, + uuid: '75fe4385-de51-3588-40e2-be8575166f2a', + groupTitle: 'Fixpunkte', + minScale: 99, + maxScale: 1000, + wmsSort: 46, + tocSort: 9089, + visible: true, + isHidden: false, + }, + ], + searchConfigurations: [ + { + index: 'gvz', + title: 'GVZ-Nr.', + }, + ], + }, + { + id: 'StatGebAlterZH', + uuid: '246fe226-ead7-4f91-b735-d294994913e0', + printTitle: 'Gebäudealter', + gb2Url: null, + icon: 'https://maps.zh.ch/images/custom/themekl-statgebalterzh.gif', + wmsUrl: 'https://maps.zh.ch/wms/StatGebAlterZH', + minScale: null, + organisation: 'Statistisches Amt', + notice: null, + title: 'Gebäudealter', + keywords: ['Gebäudealter', 'stat', 'obs', 'fap', 'denkk', 'fsla'], + opacity: 1, + layers: [ + { + id: 160331, + layer: 'geb-alter_2', + title: 'Gebäude mit Baujahr x und älter', + queryable: false, + uuid: null, + groupTitle: 'Gebäudealter', + minScale: 100001, + maxScale: 15000001, + wmsSort: 11, + tocSort: 1100, + visible: true, + isHidden: false, + }, + { + id: 160330, + layer: 'geb-alter_grau', + title: 'Baujahr', + queryable: false, + uuid: null, + groupTitle: 'Gebäudealter - Polygone', + minScale: 1, + maxScale: 100000, + wmsSort: 10, + tocSort: 1000, + visible: false, + isHidden: false, + }, + { + id: 160329, + layer: 'geb-alter_wohnen', + title: 'Baujahr', + queryable: true, + uuid: null, + groupTitle: 'Gebäudealter - Polygone', + minScale: 1, + maxScale: 100000, + wmsSort: 9, + tocSort: 900, + visible: true, + isHidden: false, + }, + ], + timeSliderConfiguration: { + name: 'Aktueller Gebäudebestand nach Baujahr', + alwaysMaxRange: false, + dateFormat: 'YYYY', + description: 'Gebäude bis 2020', + maximumDate: '2020', + minimumDate: '1000', + minimalRange: 'P1Y', + sourceType: 'parameter', + source: { + startRangeParameter: 'FILTER_VON', + endRangeParameter: 'FILTER_BIS', + layerIdentifiers: ['geb-alter_wohnen', 'geb-alter_grau', 'geb-alter_2'], + }, + }, + initialTimeSliderExtent: { + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), + }, + filterConfigurations: [ + { + name: 'Anzeigeoptionen nach Hauptnutzung', + parameter: 'FILTER_GEBART', + filterValues: [ + { + isActive: true, + values: ['Gebäude Wohnen'], + name: 'Wohnen', + }, + { + isActive: false, + values: ['Gebäude Wohnen'], + name: 'Gewerbe und Verwaltung', + }, + { + isActive: false, + values: ['Gebäude Wohnen'], + name: 'Andere', + }, + ], + }, + ], + }, + { + id: 'Lidar2021BefliegungZH', + uuid: '1dac9be1-1412-45dd-a1dd-c151c737272b', + printTitle: 'LiDAR-Befliegung 2021 ZH', + gb2Url: null, + icon: 'https://maps.zh.ch/images/custom/themekl-lidar2021befliegungzh.gif', + wmsUrl: 'https://maps.zh.ch/wms/Lidar2021BefliegungZH', + minScale: null, + organisation: 'ARE Geoinformation', + notice: + 'Die kantonale LiDAR-Befliegungen sind die Basis für die Berechnung der Höhemodelle und decken den ganzen Kanton ab. Diese Karte zeigt die abgedeckte Fläche jeder aufgenommenen Überfliegung, die während dem Projekt stattgefunden hat. Die Überlappung der Flächen wird benötigt, um die einzelnen Streifen zusammenführen zu können und eine höhere Genauigkeit zu erhalten.', + title: 'LiDAR-Befliegung 2021 ZH', + keywords: ['LiDAR-Befliegung', '2021', 'ZH'], + opacity: 1, + layers: [ + { + id: 159533, + layer: 'lidarbefliegung', + title: 'LiDAR-Befliegung', + queryable: true, + uuid: '10b88da3-3715-44f5-9480-eb754955a892', + groupTitle: 'Inventar', + minScale: 1, + maxScale: 1000000, + wmsSort: 0, + tocSort: 0, + visible: true, + isHidden: false, + }, + ], + }, + ]; service['availableMaps'] = availableMaps; }); @@ -390,8 +395,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, { @@ -535,8 +540,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -653,8 +658,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -714,8 +719,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -804,8 +809,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -863,8 +868,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1042,8 +1047,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1201,8 +1206,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('1000-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1359,8 +1364,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('0999-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2020-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('0999-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), }, }, ]; @@ -1515,8 +1520,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('1450-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('1455-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('1450-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('1455-01-01T00:00:00.000Z'), }, }, ]; @@ -1671,8 +1676,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('1750-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('1455-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('1750-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('1455-01-01T00:00:00.000Z'), }, }, ]; @@ -1827,8 +1832,8 @@ describe('FavouritesService', () => { }, ], timeExtent: { - start: timeService.getUTCDateFromString('1250-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2000-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('1250-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2000-01-01T00:00:00.000Z'), }, }, ]; @@ -2094,8 +2099,8 @@ describe('FavouritesService', () => { isSingleLayer: false, attributeFilters: undefined, timeExtent: { - start: timeService.getUTCDateFromString('2016-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2017-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('2016-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2017-01-01T00:00:00.000Z'), }, }, ]; @@ -2361,8 +2366,8 @@ describe('FavouritesService', () => { isSingleLayer: false, attributeFilters: undefined, timeExtent: { - start: timeService.getUTCDateFromString('2016-01-01T00:00:00.000Z'), - end: timeService.getUTCDateFromString('2017-01-01T00:00:00.000Z'), + start: timeService.createUTCDateFromString('2016-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2017-01-01T00:00:00.000Z'), }, }, ]; diff --git a/src/app/map/services/favourites.service.ts b/src/app/map/services/favourites.service.ts index 19369a514..120d226b9 100644 --- a/src/app/map/services/favourites.service.ts +++ b/src/app/map/services/favourites.service.ts @@ -354,7 +354,8 @@ export class FavouritesService implements OnDestroy { case 'layer': { const selectedYearExists = (timeSliderConfiguration.source as TimeSliderLayerSource).layers.some( (layer) => - this.timeService.getUTCDateFromString(layer.date, timeSliderConfiguration.dateFormat).getTime() === timeExtent.start.getTime(), + this.timeService.createUTCDateFromString(layer.date, timeSliderConfiguration.dateFormat).getTime() === + timeExtent.start.getTime(), ); return selectedYearExists && isTimeExtentValid; } diff --git a/src/app/map/services/time-slider.service.spec.ts b/src/app/map/services/time-slider.service.spec.ts index 518b79035..d160bc216 100644 --- a/src/app/map/services/time-slider.service.spec.ts +++ b/src/app/map/services/time-slider.service.spec.ts @@ -1,7 +1,6 @@ import {TestBed} from '@angular/core/testing'; import {TimeSliderService} from './time-slider.service'; -import dayjs from 'dayjs'; -import {TimeSliderConfiguration, TimeSliderParameterSource} from '../../shared/interfaces/topic.interface'; +import {MapLayer, TimeSliderConfiguration, TimeSliderLayerSource, TimeSliderParameterSource} from '../../shared/interfaces/topic.interface'; import {TimeExtent} from '../interfaces/time-extent.interface'; import {TIME_SERVICE} from '../../app.module'; import {TimeService} from '../../shared/interfaces/time-service.interface'; @@ -31,8 +30,8 @@ describe('TimeSliderService', () => { let timeSliderConfig: TimeSliderConfiguration; beforeEach(() => { - minimumDate = timeService.getDateAsFormattedString(timeService.getDateFromString('2000-01', dateFormat), dateFormat); - maximumDate = timeService.getDateAsFormattedString(timeService.getDateFromString('2001-03', dateFormat), dateFormat); + minimumDate = timeService.getDateAsFormattedString(timeService.createDateFromString('2000-01', dateFormat), dateFormat); + maximumDate = timeService.getDateAsFormattedString(timeService.createDateFromString('2001-03', dateFormat), dateFormat); timeSliderConfig = { name: 'mockTimeSlider', dateFormat: dateFormat, @@ -52,19 +51,23 @@ describe('TimeSliderService', () => { it('should create always the same time extent using min-/max values', () => { const newValue: TimeExtent = { - start: timeService.getDateFromString('2000-02', dateFormat), - end: timeService.getDateFromString('2000-03', dateFormat), + start: timeService.createDateFromString('2000-02', dateFormat), + end: timeService.createDateFromString('2000-03', dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent( timeSliderConfig, newValue, true, - timeService.getDateFromString(minimumDate), - timeService.getDateFromString(maximumDate), + timeService.createDateFromString(minimumDate), + timeService.createDateFromString(maximumDate), + ); + expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.createDateFromString(minimumDate))).toBe( + 0, + ); + expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.createDateFromString(maximumDate))).toBe( + 0, ); - expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString(minimumDate))).toBe(0); - expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString(maximumDate))).toBe(0); }); }); @@ -80,8 +83,8 @@ describe('TimeSliderService', () => { let timeSliderConfig: TimeSliderConfiguration; beforeEach(() => { - minimumDate = timeService.getDateFromString('2000-01', dateFormat); - maximumDate = timeService.getDateFromString('2001-03', dateFormat); + minimumDate = timeService.createDateFromString('2000-01', dateFormat); + maximumDate = timeService.createDateFromString('2001-03', dateFormat); minimumDateString = timeService.getDateAsFormattedString(minimumDate, dateFormat); maximumDateString = timeService.getDateAsFormattedString(maximumDate, dateFormat); timeSliderConfig = { @@ -103,15 +106,15 @@ describe('TimeSliderService', () => { it('should not create a new time extent if it is already valid', () => { const newValue: TimeExtent = { - start: timeService.getDateFromString('2000-02', dateFormat), - end: timeService.getDateFromString('2000-03', dateFormat), + start: timeService.createDateFromString('2000-02', dateFormat), + end: timeService.createDateFromString('2000-03', dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent( timeSliderConfig, newValue, true, - timeService.getDateFromString(minimumDateString), - timeService.getDateFromString(maximumDateString), + timeService.createDateFromString(minimumDateString), + timeService.createDateFromString(maximumDateString), ); expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, newValue.start)).toBe(0); expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, newValue.end)).toBe(0); @@ -121,10 +124,10 @@ describe('TimeSliderService', () => { const newValue: TimeExtent = {start: maximumDate, end: minimumDate}; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2001-03', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.createDateFromString('2001-03', dateFormat)), ).toBe(0); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2001-04', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.createDateFromString('2001-04', dateFormat)), ).toBe(0); }); @@ -132,10 +135,10 @@ describe('TimeSliderService', () => { const newValue: TimeExtent = {start: timeService.subtractRangeFromDate(minimumDate, 'P1M'), end: minimumDate}; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2000-01', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.createDateFromString('2000-01', dateFormat)), ).toBe(0); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2000-02', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.createDateFromString('2000-02', dateFormat)), ).toBe(0); }); @@ -143,10 +146,10 @@ describe('TimeSliderService', () => { const newValue: TimeExtent = {start: timeService.addRangeToDate(maximumDate, 'P1M'), end: minimumDate}; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2001-03', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.createDateFromString('2001-03', dateFormat)), ).toBe(0); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2001-04', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.createDateFromString('2001-04', dateFormat)), ).toBe(0); }); }); @@ -163,8 +166,8 @@ describe('TimeSliderService', () => { let maximumDateString: string; beforeEach(() => { - minimumDate = timeService.getDateFromString('2000-01', dateFormat); - maximumDate = timeService.getDateFromString('2001-03', dateFormat); + minimumDate = timeService.createDateFromString('2000-01', dateFormat); + maximumDate = timeService.createDateFromString('2001-03', dateFormat); maximumDateString = timeService.getDateAsFormattedString(maximumDate, dateFormat); timeSliderConfig = { name: 'mockTimeSlider', @@ -185,8 +188,8 @@ describe('TimeSliderService', () => { it('should not create a new time extent if it is already valid', () => { const newValue: TimeExtent = { - start: dayjs('2000-02', dateFormat).toDate(), - end: dayjs('2000-05', dateFormat).toDate(), + start: timeService.createDateFromString('2000-02', dateFormat), + end: timeService.createDateFromString('2000-05', dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, newValue.start)).toBe(0); @@ -195,8 +198,8 @@ describe('TimeSliderService', () => { it('should create a new start/end date if it is over/under the limits', () => { const newValue: TimeExtent = { - start: dayjs('1999-12', dateFormat).toDate(), - end: dayjs('2001-04', dateFormat).toDate(), + start: timeService.createDateFromString('1999-12', dateFormat), + end: timeService.createDateFromString('2001-04', dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, minimumDate)).toBe(0); @@ -205,51 +208,51 @@ describe('TimeSliderService', () => { it('should adjust the start date if the new start date is too close to the original start date', () => { const newValue: TimeExtent = { - start: dayjs('2000-03', dateFormat).toDate(), - end: dayjs('2000-04', dateFormat).toDate(), + start: timeService.createDateFromString('2000-03', dateFormat), + end: timeService.createDateFromString('2000-04', dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2000-02', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.createDateFromString('2000-02', dateFormat)), ).toBe(0); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2000-04', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.createDateFromString('2000-04', dateFormat)), ).toBe(0); }); it('should adjust the end date if the new end date is too close to the original end date', () => { const newValue: TimeExtent = { - start: dayjs('2000-02', dateFormat).toDate(), - end: dayjs('2000-03', dateFormat).toDate(), + start: timeService.createDateFromString('2000-02', dateFormat), + end: timeService.createDateFromString('2000-03', dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, false, minimumDate, maximumDate); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2000-02', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.createDateFromString('2000-02', dateFormat)), ).toBe(0); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2000-04', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.createDateFromString('2000-04', dateFormat)), ).toBe(0); }); it('should create a new start date if if is too close to the end date and the end date is the maximum possible date', () => { const newValue: TimeExtent = { - start: dayjs('2001-02', dateFormat).toDate(), - end: dayjs('2001-03', dateFormat).toDate(), + start: timeService.createDateFromString('2001-02', dateFormat), + end: timeService.createDateFromString('2001-03', dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2001-01', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.createDateFromString('2001-01', dateFormat)), ).toBe(0); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2001-03', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.createDateFromString('2001-03', dateFormat)), ).toBe(0); }); }); it('should use the correct range in case of years', () => { const dateFormat = 'YYYY'; - const minimumDate = timeService.getDateFromString('2000-01', dateFormat); - const maximumDate = timeService.getDateFromString('2001-03', dateFormat); + const minimumDate = timeService.createDateFromString('2000-01', dateFormat); + const maximumDate = timeService.createDateFromString('2001-03', dateFormat); const minimumDateString = timeService.getDateAsFormattedString(minimumDate, dateFormat); const maximumDateString = timeService.getDateAsFormattedString(maximumDate, dateFormat); const alwaysMaxRange = false; @@ -272,18 +275,18 @@ describe('TimeSliderService', () => { }, }; const newValue: TimeExtent = { - start: timeService.getDateFromString(minimumDateString, dateFormat), - end: timeService.getDateFromString(minimumDateString, dateFormat), + start: timeService.createDateFromString(minimumDateString, dateFormat), + end: timeService.createDateFromString(minimumDateString, dateFormat), }; const calculatedTimeExtent = service.createValidTimeExtent(timeSliderConfig, newValue, true, minimumDate, maximumDate); expect( - timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.getDateFromString('2000', dateFormat)), + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.start, timeService.createDateFromString('2000', dateFormat)), ).toBe(0); expect(timeService.getDateAsFormattedString(calculatedTimeExtent.start, timeSliderConfig.dateFormat)).toBe('2000'); - expect(timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.getDateFromString('2001', dateFormat))).toBe( - 0, - ); + expect( + timeService.calculateDifferenceBetweenDates(calculatedTimeExtent.end, timeService.createDateFromString('2001', dateFormat)), + ).toBe(0); expect(timeService.getDateAsFormattedString(calculatedTimeExtent.end, timeSliderConfig.dateFormat)).toBe('2001'); }); }); @@ -314,17 +317,17 @@ describe('TimeSliderService', () => { it('should create the correct stops', () => { const stops = service.createStops(timeSliderConfig); expect(stops.length).toBe(3); - expect(timeService.calculateDifferenceBetweenDates(stops[0], timeService.getUTCDateFromString(firstStop, dateFormat))).toBe(0); - expect(timeService.calculateDifferenceBetweenDates(stops[1], timeService.getUTCDateFromString(secondStop, dateFormat))).toBe(0); - expect(timeService.calculateDifferenceBetweenDates(stops[2], timeService.getUTCDateFromString(thirdStop, dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[0], timeService.createUTCDateFromString(firstStop, dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[1], timeService.createUTCDateFromString(secondStop, dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[2], timeService.createUTCDateFromString(thirdStop, dateFormat))).toBe(0); }); }); describe('using a parameter source', () => { describe('with a single time unit and a range', () => { it('should create the correct stops', () => { const dateFormat = 'YYYY-MM'; - const minimumDate = timeService.getDateFromString('2000-01', dateFormat); - const maximumDate = timeService.getDateFromString('2001-03', dateFormat); + const minimumDate = timeService.createDateFromString('2000-01', dateFormat); + const maximumDate = timeService.createDateFromString('2001-03', dateFormat); const minimumDateString = timeService.getDateAsFormattedString(minimumDate, dateFormat); const maximumDateString = timeService.getDateAsFormattedString(maximumDate, dateFormat); const alwaysMaxRange = false; @@ -348,10 +351,13 @@ describe('TimeSliderService', () => { }; const stops = service.createStops(timeSliderConfig); expect(stops.length).toBe(15); - expect(timeService.calculateDifferenceBetweenDates(stops[0], timeService.getUTCDateFromString('2000-01', dateFormat))).toBe(0); - expect(timeService.calculateDifferenceBetweenDates(stops[1], timeService.getUTCDateFromString('2000-02', dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[0], timeService.createUTCDateFromString('2000-01', dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[1], timeService.createUTCDateFromString('2000-02', dateFormat))).toBe(0); expect( - timeService.calculateDifferenceBetweenDates(stops[stops.length - 1], timeService.getUTCDateFromString('2001-03', dateFormat)), + timeService.calculateDifferenceBetweenDates( + stops[stops.length - 1], + timeService.createUTCDateFromString('2001-03', dateFormat), + ), ).toBe(0); }); }); @@ -366,8 +372,8 @@ describe('TimeSliderService', () => { }; beforeEach(() => { - minimumDate = timeService.getUTCDateFromString('2000-01', dateFormat); - maximumDate = timeService.getUTCDateFromString('2001-03', dateFormat); + minimumDate = timeService.createUTCDateFromString('2000-01', dateFormat); + maximumDate = timeService.createUTCDateFromString('2001-03', dateFormat); }); describe('and a range', () => { @@ -430,14 +436,138 @@ describe('TimeSliderService', () => { const expectedNumberOfStops = 15; expect(stops.length).toBe(expectedNumberOfStops); - expect(timeService.calculateDifferenceBetweenDates(stops[0], timeService.getUTCDateFromString('2000-01', dateFormat))).toBe(0); - expect(timeService.calculateDifferenceBetweenDates(stops[1], timeService.getUTCDateFromString('2000-02', dateFormat))).toBe(0); + expect(timeService.calculateDifferenceBetweenDates(stops[0], timeService.createUTCDateFromString('2000-01', dateFormat))).toBe( + 0, + ); + expect(timeService.calculateDifferenceBetweenDates(stops[1], timeService.createUTCDateFromString('2000-02', dateFormat))).toBe( + 0, + ); expect( - timeService.calculateDifferenceBetweenDates(stops[stops.length - 1], timeService.getUTCDateFromString('2001-03', dateFormat)), + timeService.calculateDifferenceBetweenDates( + stops[stops.length - 1], + timeService.createUTCDateFromString('2001-03', dateFormat), + ), ).toBe(0); }); }); }); }); }); + + describe('isLayerVisible', () => { + it('returns `true` if a given layer is within the time extent', () => { + const mapLayer = {layer: 'layerName', visible: false} as MapLayer; + const timeSliderConfiguration = { + dateFormat: 'YYYY-MM-DD', + sourceType: 'layer', + source: { + layers: [{layerName: 'layerName', date: '2023-06-30'}], + } as TimeSliderLayerSource, + } as TimeSliderConfiguration; + const timeExtent: TimeExtent = { + start: new Date(2023, 0, 1), + end: new Date(2023, 11, 31), + }; + + const expected = true; + const actual = service.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); + + expect(actual).toBe(expected); + }); + + it('returns `false` if a given layer is outside the time extent', () => { + const mapLayer = {layer: 'layerName', visible: false} as MapLayer; + const timeSliderConfiguration = { + dateFormat: 'YYYY-MM-DD', + sourceType: 'layer', + source: { + layers: [{layerName: 'layerName', date: '2024-01-01'}], + } as TimeSliderLayerSource, + } as TimeSliderConfiguration; + const timeExtent: TimeExtent = { + start: new Date(2023, 0, 1), + end: new Date(2023, 11, 31), + }; + + const expected = false; + const actual = service.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); + + expect(actual).toBe(expected); + }); + + it('returns `undefined` if there is no matching layer', () => { + const mapLayer = {layer: 'layerName', visible: false} as MapLayer; + const timeSliderConfiguration = { + dateFormat: 'YYYY-MM-DD', + sourceType: 'layer', + source: { + layers: [{layerName: 'otherLayerName', date: '2023-06-15'}], + } as TimeSliderLayerSource, + } as TimeSliderConfiguration; + const timeExtent: TimeExtent = { + start: new Date(2023, 0, 1), + end: new Date(2023, 11, 31), + }; + + const expected = undefined; + const actual = service.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); + + expect(actual).toBe(expected); + }); + + it('returns `undefined` if it is not a layer based time slider configuration', () => { + const mapLayer = {layer: 'layerName', visible: false} as MapLayer; + const timeSliderConfiguration = { + dateFormat: 'YYYY-MM-DD', + sourceType: 'parameter', + source: { + startRangeParameter: 'VON', + endRangeParameter: 'BIS', + layerIdentifiers: ['layerName'], + } as TimeSliderParameterSource, + } as TimeSliderConfiguration; + const timeExtent: TimeExtent = { + start: new Date(2023, 0, 1), + end: new Date(2023, 11, 31), + }; + + const expected = undefined; + const actual = service.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); + + expect(actual).toBe(expected); + }); + + it('returns `undefined` if the time slider configuration is undefined', () => { + const mapLayer = {layer: 'layerName', visible: false} as MapLayer; + const timeSliderConfiguration = undefined; + const timeExtent: TimeExtent = { + start: new Date(2023, 0, 1), + end: new Date(2023, 11, 31), + }; + + const expected = undefined; + const actual = service.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); + + expect(actual).toBe(expected); + }); + + it('returns `undefined` if the time extent is undefined', () => { + const mapLayer = {layer: 'layerName', visible: false} as MapLayer; + const timeSliderConfiguration = { + dateFormat: 'YYYY-MM-DD', + sourceType: 'parameter', + source: { + startRangeParameter: 'VON', + endRangeParameter: 'BIS', + layerIdentifiers: ['layerName'], + } as TimeSliderParameterSource, + } as TimeSliderConfiguration; + const timeExtent = undefined; + + const expected = undefined; + const actual = service.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); + + expect(actual).toBe(expected); + }); + }); }); diff --git a/src/app/map/services/time-slider.service.ts b/src/app/map/services/time-slider.service.ts index 11df1007c..0d355f5bb 100644 --- a/src/app/map/services/time-slider.service.ts +++ b/src/app/map/services/time-slider.service.ts @@ -3,7 +3,8 @@ import {MapLayer, TimeSliderConfiguration, TimeSliderLayerSource} from '../../sh import {TimeExtent} from '../interfaces/time-extent.interface'; import {InvalidTimeSliderConfiguration} from '../../shared/errors/map.errors'; import {TIME_SERVICE} from '../../app.module'; -import {DateUnit, TimeService} from '../../shared/interfaces/time-service.interface'; +import {TimeService} from '../../shared/interfaces/time-service.interface'; +import {DateUnit} from '../../shared/types/date-unit.type'; @Injectable({ providedIn: 'root', @@ -14,8 +15,8 @@ export class TimeSliderService { * Creates an initial time extent based on the given time slider configuration. */ public createInitialTimeSliderExtent(timeSliderConfig: TimeSliderConfiguration): TimeExtent { - const minimumDate: Date = this.timeService.getUTCDateFromString(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maximumDate: Date = this.timeService.getUTCDateFromString(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const minimumDate: Date = this.timeService.createUTCDateFromString(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maximumDate: Date = this.timeService.createUTCDateFromString(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); return { start: minimumDate, end: timeSliderConfig.range ? this.addRangeToDate(minimumDate, timeSliderConfig.range) : maximumDate, @@ -50,7 +51,7 @@ export class TimeSliderService { const timeSliderLayerSource = timeSliderConfiguration.source as TimeSliderLayerSource; const timeSliderLayer = timeSliderLayerSource.layers.find((layer) => layer.layerName === mapLayer.layer); if (timeSliderLayer) { - const date = this.timeService.getUTCDateFromString(timeSliderLayer.date, timeSliderConfiguration.dateFormat); + const date = this.timeService.createUTCDateFromString(timeSliderLayer.date, timeSliderConfiguration.dateFormat); return date >= timeExtent.start && date < timeExtent.end; } else { return undefined; @@ -128,8 +129,8 @@ export class TimeSliderService { } public isTimeExtentValid(timeSliderConfig: TimeSliderConfiguration, timeExtent: TimeExtent): boolean { - const minDate = this.timeService.getUTCDateFromString(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maxDate = this.timeService.getUTCDateFromString(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const minDate = this.timeService.createUTCDateFromString(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maxDate = this.timeService.createUTCDateFromString(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); const updatedTimeExtent: TimeExtent = this.createValidTimeExtent(timeSliderConfig, timeExtent, false, minDate, maxDate); @@ -140,7 +141,7 @@ export class TimeSliderService { * Extracts a unit from the given date format (ISO8601) if it contains exactly one or if it contains multiple units. * * @remarks - * It does return a unit ('years'/'months'/...) only if the given duration contains values of this unit and nothing else; + * It does return a unit ('years'/'months'/...) only if the given range contains values of this unit and nothing else; * otherwise. * * @example @@ -168,20 +169,20 @@ export class TimeSliderService { */ private createStopsForLayerSource(timeSliderConfig: TimeSliderConfiguration): Array { const timeSliderLayerSource = timeSliderConfig.source as TimeSliderLayerSource; - return timeSliderLayerSource.layers.map((layer) => this.timeService.getUTCDateFromString(layer.date, timeSliderConfig.dateFormat)); + return timeSliderLayerSource.layers.map((layer) => this.timeService.createUTCDateFromString(layer.date, timeSliderConfig.dateFormat)); } /** * Creates stops for a parameter source. * * @remarks - * This is done by using a strict interval (e.g. one year) if the default range duration only contains + * This is done by using a strict interval (e.g. one year) if the default range only contains * a single type of unit (e.g. 'years'). Otherwise a more generic approach is used by creating date stops from - * start to finish using the given duration; this can lead to gaps near the end but supports all cases. + * start to finish using the given range; this can lead to gaps near the end but supports all cases. */ private createStopsForParameterSource(timeSliderConfig: TimeSliderConfiguration): Array { - const minimumDate: Date = this.timeService.getUTCDateFromString(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); - const maximumDate: Date = this.timeService.getUTCDateFromString(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); + const minimumDate: Date = this.timeService.createUTCDateFromString(timeSliderConfig.minimumDate, timeSliderConfig.dateFormat); + const maximumDate: Date = this.timeService.createUTCDateFromString(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); const initialRange: string | null = timeSliderConfig.range ?? timeSliderConfig.minimalRange ?? null; if ( initialRange && @@ -206,7 +207,7 @@ export class TimeSliderService { if (initialRange) { date = this.addRangeToDate(date, initialRange); } else if (unit) { - date = this.addMinimalDuration(date, unit); + date = this.addMinimalRange(date, unit); } else { throw new InvalidTimeSliderConfiguration('Datumsformat sowie minimale Range sind ungültig.'); } @@ -215,7 +216,7 @@ export class TimeSliderService { return dates; } - private addMinimalDuration(date: Date, unit: string): Date { + private addMinimalRange(date: Date, unit: string): Date { return this.timeService.addMinimalRangeToDate(date, unit); } diff --git a/src/app/map/utils/active-time-slider-layers.utils.spec.ts b/src/app/map/utils/active-time-slider-layers.utils.spec.ts deleted file mode 100644 index ae842ca4d..000000000 --- a/src/app/map/utils/active-time-slider-layers.utils.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -import {MapLayer, TimeSliderConfiguration, TimeSliderLayerSource, TimeSliderParameterSource} from '../../shared/interfaces/topic.interface'; -import {TimeExtent} from '../interfaces/time-extent.interface'; -import {ActiveTimeSliderLayersUtils} from './active-time-slider-layers.utils'; - -describe('ActiveTimeSliderLayersUtils', () => { - describe('isLayerVisible', () => { - it('returns `true` if a given layer is within the time extent', () => { - const mapLayer = {layer: 'layerName', visible: false} as MapLayer; - const timeSliderConfiguration = { - dateFormat: 'YYYY-MM-DD', - sourceType: 'layer', - source: { - layers: [{layerName: 'layerName', date: '2023-06-30'}], - } as TimeSliderLayerSource, - } as TimeSliderConfiguration; - const timeExtent: TimeExtent = { - start: new Date(2023, 0, 1), - end: new Date(2023, 11, 31), - }; - - const expected = true; - const actual = ActiveTimeSliderLayersUtils.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); - - expect(actual).toBe(expected); - }); - - it('returns `false` if a given layer is outside the time extent', () => { - const mapLayer = {layer: 'layerName', visible: false} as MapLayer; - const timeSliderConfiguration = { - dateFormat: 'YYYY-MM-DD', - sourceType: 'layer', - source: { - layers: [{layerName: 'layerName', date: '2024-01-01'}], - } as TimeSliderLayerSource, - } as TimeSliderConfiguration; - const timeExtent: TimeExtent = { - start: new Date(2023, 0, 1), - end: new Date(2023, 11, 31), - }; - - const expected = false; - const actual = ActiveTimeSliderLayersUtils.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); - - expect(actual).toBe(expected); - }); - - it('returns `undefined` if there is no matching layer', () => { - const mapLayer = {layer: 'layerName', visible: false} as MapLayer; - const timeSliderConfiguration = { - dateFormat: 'YYYY-MM-DD', - sourceType: 'layer', - source: { - layers: [{layerName: 'otherLayerName', date: '2023-06-15'}], - } as TimeSliderLayerSource, - } as TimeSliderConfiguration; - const timeExtent: TimeExtent = { - start: new Date(2023, 0, 1), - end: new Date(2023, 11, 31), - }; - - const expected = undefined; - const actual = ActiveTimeSliderLayersUtils.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); - - expect(actual).toBe(expected); - }); - - it('returns `undefined` if it is not a layer based time slider configuration', () => { - const mapLayer = {layer: 'layerName', visible: false} as MapLayer; - const timeSliderConfiguration = { - dateFormat: 'YYYY-MM-DD', - sourceType: 'parameter', - source: { - startRangeParameter: 'VON', - endRangeParameter: 'BIS', - layerIdentifiers: ['layerName'], - } as TimeSliderParameterSource, - } as TimeSliderConfiguration; - const timeExtent: TimeExtent = { - start: new Date(2023, 0, 1), - end: new Date(2023, 11, 31), - }; - - const expected = undefined; - const actual = ActiveTimeSliderLayersUtils.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); - - expect(actual).toBe(expected); - }); - - it('returns `undefined` if the time slider configuration is undefined', () => { - const mapLayer = {layer: 'layerName', visible: false} as MapLayer; - const timeSliderConfiguration = undefined; - const timeExtent: TimeExtent = { - start: new Date(2023, 0, 1), - end: new Date(2023, 11, 31), - }; - - const expected = undefined; - const actual = ActiveTimeSliderLayersUtils.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); - - expect(actual).toBe(expected); - }); - - it('returns `undefined` if the time extent is undefined', () => { - const mapLayer = {layer: 'layerName', visible: false} as MapLayer; - const timeSliderConfiguration = { - dateFormat: 'YYYY-MM-DD', - sourceType: 'parameter', - source: { - startRangeParameter: 'VON', - endRangeParameter: 'BIS', - layerIdentifiers: ['layerName'], - } as TimeSliderParameterSource, - } as TimeSliderConfiguration; - const timeExtent = undefined; - - const expected = undefined; - const actual = ActiveTimeSliderLayersUtils.isLayerVisible(mapLayer, timeSliderConfiguration, timeExtent); - - expect(actual).toBe(expected); - }); - }); -}); diff --git a/src/app/map/utils/active-time-slider-layers.utils.ts b/src/app/map/utils/active-time-slider-layers.utils.ts deleted file mode 100644 index 7df820f4a..000000000 --- a/src/app/map/utils/active-time-slider-layers.utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {MapLayer, TimeSliderConfiguration, TimeSliderLayerSource} from '../../shared/interfaces/topic.interface'; -import {TimeExtent} from '../interfaces/time-extent.interface'; -import {DayjsUtils} from '../../shared/utils/dayjs.utils'; - -export class ActiveTimeSliderLayersUtils { - /** - * Is the given layer visible? Returns `true` or `false` depending on the layer to be within the given time extent; or `undefined` if - * either the layer isn't part of a time slider configuration, the extent is undefined or the configuration source isn't of type `layer`. - */ - public static isLayerVisible( - mapLayer: MapLayer, - timeSliderConfiguration: TimeSliderConfiguration | undefined, - timeExtent: TimeExtent | undefined, - ): boolean | undefined { - if (!timeSliderConfiguration || timeSliderConfiguration.sourceType === 'parameter' || !timeExtent) { - return undefined; - } - - const timeSliderLayerSource = timeSliderConfiguration.source as TimeSliderLayerSource; - const timeSliderLayer = timeSliderLayerSource.layers.find((layer) => layer.layerName === mapLayer.layer); - if (timeSliderLayer) { - const date = DayjsUtils.parseUTCDate(timeSliderLayer.date, timeSliderConfiguration.dateFormat); - return date >= timeExtent.start && date < timeExtent.end; - } else { - return undefined; - } - } -} diff --git a/src/app/shared/interfaces/time-service.interface.ts b/src/app/shared/interfaces/time-service.interface.ts index cb19dd84e..653688f72 100644 --- a/src/app/shared/interfaces/time-service.interface.ts +++ b/src/app/shared/interfaces/time-service.interface.ts @@ -1,49 +1,69 @@ -export interface TimeService { - getDateFromString: (date: string, format?: string) => Date; // todo: create a type for the format - - getDateAsFormattedString: (date: Date, format: string) => string; // todo: create a type for the format +import {DateUnit} from '../types/date-unit.type'; +export interface TimeService { + /** + * Given a date and a unit, this method returns the partial value of the given date. + */ + createPartialFromString: (date: string, unit: DateUnit) => number; + /** + * Creates a date object from a string; with an optional format in ISO 8601. + */ + createDateFromString: (date: string, format?: string) => Date; + /** + * Creates a date object from a Unix timestamp. + */ + createDateFromUnixTimestamp: (timestamp: number) => Date; + /* + * Creates a date object from a string in UTC; with an optional format in ISO 8601. + */ + createUTCDateFromString: (date: string, format?: string) => Date; + /** + * Returns the date as a formatted string according to the given format in ISO 8601. + */ + getDateAsFormattedString: (date: Date, format: string) => string; /** * Converts a date into UTC and returns it as (optionally formatted) string. - * @param date - * @param format */ - getDateAsUTCString: (date: Date, format?: string) => string; // todo: create a type for the format - - getDateFromUnixTimestamp: (timestamp: number) => Date; - + getDateAsUTCString: (date: Date, format?: string) => string; /** * Returns the difference between two dates in milliseconds. */ calculateDifferenceBetweenDates: (firstDate: Date, secondDate: Date) => number; - /** - * Given a date and a unit, this method returns the partial value of the given date. - * @param date - * @param unit + * Checks whether the given string is a valid date. */ - getPartialFromString(date: string, unit: DateUnit): number; // todo: create a type for the unit - isDate: (value: string) => boolean; - - getUTCDateFromString: (date: string, format?: string) => Date; - /** * Returns `true` if the given string range is exactly one of a single time unit (year, month, ...). * * @example - * `P1Y1M` is a duration of one year AND one month which is more than one time unit; therefore is the result `false` - * `P2Y` is a duration of two years which is more than one of a single time unit; therefore is the result `false` - * `P1D` is a duration of one day which is exactly one of a single time unit; therefore the result is `true` + * `P1Y1M` is a range of one year AND one month which is more than one time unit; therefore is the result `false` + * `P2Y` is a range of two years which is more than one of a single time unit; therefore is the result `false` + * `P1D` is a range of one day which is exactly one of a single time unit; therefore the result is `true` */ isStringSingleTimeUnitRange: (range: string) => boolean; - + /** + * Adds the range to the given date as exact as possible. + * + * @remarks + * It will add values of a specific unit to the date in case that + * the range contains only values of one specific unit (e.g. 'years'). This has the advantage that it does not use + * a generic solution which would be 365 days in case of a year. + * */ addRangeToDate: (date: Date, range: string) => Date; - + /** + * Subtracts the range from the given date as exact as possible. + * + * @remarks + * It will subtract values of a specific unit from the date in case that + * the range contains only values of one specific unit (e.g. 'years'). This has the advantage that it does not use + * a generic solution which would be 365 days in case of a year. + * */ subtractRangeFromDate: (date: Date, range: string) => Date; - + /** + * Returns an ISO 8601 range in milliseconds. + */ getISORangeInMilliseconds: (range: string) => number; - /** * Adds a range of 1 of the given unit to the date. * @param date @@ -51,5 +71,3 @@ export interface TimeService { */ addMinimalRangeToDate: (date: Date, unit: string) => Date; } - -export type DateUnit = 'days' | 'months' | 'years' | 'hours' | 'minutes' | 'seconds' | 'milliseconds'; diff --git a/src/app/shared/interfaces/topic.interface.ts b/src/app/shared/interfaces/topic.interface.ts index b20340be2..917e9c15e 100644 --- a/src/app/shared/interfaces/topic.interface.ts +++ b/src/app/shared/interfaces/topic.interface.ts @@ -9,7 +9,20 @@ export interface Topic { maps: Map[]; } -export interface Map extends HasOpacity { +/** + * TimeSliderSettings are either or - they have either a config AND an extent, or neither. + */ +export type TimeSliderSettings = + | { + timeSliderConfiguration: TimeSliderConfiguration; + initialTimeSliderExtent: TimeExtent; + } + | { + timeSliderConfiguration: undefined; + initialTimeSliderExtent: undefined; + }; + +interface BasicMap extends HasOpacity { /** Map identifier */ id: string; /** Map title */ @@ -35,9 +48,6 @@ export interface Map extends HasOpacity { minScale: number | null; /** True if unaccessible with current permissions. Not available in production environment. */ permissionMissing?: boolean; - /** Timeslider Settings */ - timeSliderConfiguration?: TimeSliderConfiguration; - initialTimeSliderExtent?: TimeExtent; /** Filters Settings */ filterConfigurations?: FilterConfiguration[]; searchConfigurations?: SearchConfiguration[]; @@ -45,6 +55,8 @@ export interface Map extends HasOpacity { notice: string | null; } +export type Map = BasicMap & TimeSliderSettings; + export interface MapLayer extends HasVisibility, HasHidingState { /** Layer ID */ id: number; diff --git a/src/app/shared/services/abstract-storage.service.ts b/src/app/shared/services/abstract-storage.service.ts index e6a43a11c..1a6a107e2 100644 --- a/src/app/shared/services/abstract-storage.service.ts +++ b/src/app/shared/services/abstract-storage.service.ts @@ -3,7 +3,7 @@ import {SessionStorageKey} from '../types/session-storage-key.type'; import {TimeService} from '../interfaces/time-service.interface'; export abstract class AbstractStorageService { - constructor(public readonly timeService: TimeService) {} + protected constructor(private readonly timeService: TimeService) {} public abstract set(key: T, value: string): void; @@ -12,7 +12,7 @@ export abstract class AbstractStorageService(value: string): JsonType { - return JSON.parse(value, this.reviver); + return JSON.parse(value, this.reviver.bind(this)); } public stringifyJson(value: JsonType): string { @@ -20,23 +20,19 @@ export abstract class AbstractStorageService { alwaysMaxRange: false, range: undefined, }, + initialTimeSliderExtent: { + start: new Date('1850'), + end: new Date('2020'), + }, filterConfigurations: [ { name: 'Anzeigeoptionen nach Hauptnutzung', diff --git a/src/app/shared/services/apis/gb3/gb3-topics.service.ts b/src/app/shared/services/apis/gb3/gb3-topics.service.ts index c593cbc4e..5eb5a52b7 100644 --- a/src/app/shared/services/apis/gb3/gb3-topics.service.ts +++ b/src/app/shared/services/apis/gb3/gb3-topics.service.ts @@ -14,6 +14,7 @@ import { TimeSliderLayer, TimeSliderLayerSource, TimeSliderParameterSource, + TimeSliderSettings, TimeSliderSourceType, TopicsResponse, WmsFilterValue, @@ -37,7 +38,6 @@ import {ConfigService} from '../../config.service'; import {TIME_SERVICE} from '../../../../app.module'; import {TimeService} from '../../../interfaces/time-service.interface'; import {TimeSliderService} from '../../../../map/services/time-slider.service'; -import {TimeExtent} from '../../../../map/interfaces/time-extent.interface'; const INACTIVE_STRING_FILTER_VALUE = ''; const INACTIVE_NUMBER_FILTER_VALUE = -1; @@ -236,12 +236,7 @@ export class Gb3TopicsService extends Gb3ApiService { private handleTimeSliderConfiguration( timesliderConfiguration: TopicsListData['categories'][0]['topics'][0]['timesliderConfiguration'] | undefined, - ): - | { - initialTimeSliderExtent: undefined; - timeSliderConfiguration: undefined; - } - | {initialTimeSliderExtent: TimeExtent; timeSliderConfiguration: TimeSliderConfiguration} { + ): TimeSliderSettings { if (!timesliderConfiguration) { return { timeSliderConfiguration: undefined, diff --git a/src/app/shared/services/apis/grav-cms/grav-cms.service.ts b/src/app/shared/services/apis/grav-cms/grav-cms.service.ts index 7fb57856a..93bada27a 100644 --- a/src/app/shared/services/apis/grav-cms/grav-cms.service.ts +++ b/src/app/shared/services/apis/grav-cms/grav-cms.service.ts @@ -48,8 +48,8 @@ export class GravCmsService extends BaseApiService { title: discoverMapData.title, description: discoverMapData.description, mapId: discoverMapData.id, - fromDate: this.timeService.getDateFromString(discoverMapData.from_date, DATE_FORMAT), - toDate: this.timeService.getDateFromString(discoverMapData.to_date, DATE_FORMAT), + fromDate: this.timeService.createDateFromString(discoverMapData.from_date, DATE_FORMAT), + toDate: this.timeService.createDateFromString(discoverMapData.to_date, DATE_FORMAT), image: { url: this.createFullImageUrl(discoverMapData.image.path), name: discoverMapData.image.name, @@ -69,8 +69,8 @@ export class GravCmsService extends BaseApiService { title: pageInfoData.title, description: pageInfoData.description, pages: this.transformPagesToMainPages(pageInfoData.pages), - fromDate: this.timeService.getDateFromString(pageInfoData.from_date, DATE_FORMAT), - toDate: this.timeService.getDateFromString(pageInfoData.to_date, DATE_FORMAT), + fromDate: this.timeService.createDateFromString(pageInfoData.from_date, DATE_FORMAT), + toDate: this.timeService.createDateFromString(pageInfoData.to_date, DATE_FORMAT), severity: pageInfoData.severity as PageNotificationSeverity, isMarkedAsRead: false, }; @@ -94,7 +94,7 @@ export class GravCmsService extends BaseApiService { altText: frequentlyUsedData.image_alt, } : undefined, - created: this.timeService.getDateFromUnixTimestamp(Number(frequentlyUsedData.created)), + created: this.timeService.createDateFromUnixTimestamp(Number(frequentlyUsedData.created)), }; }); } diff --git a/src/app/shared/services/dayjs.service.spec.ts b/src/app/shared/services/dayjs.service.spec.ts index 649500506..b3a10a8cc 100644 --- a/src/app/shared/services/dayjs.service.spec.ts +++ b/src/app/shared/services/dayjs.service.spec.ts @@ -4,7 +4,6 @@ import {DayjsService} from './dayjs.service'; describe('DayjsService', () => { let dayjsService: DayjsService; - // todo override beforeEach(() => { TestBed.configureTestingModule({providers: [{provide: TIME_SERVICE, useClass: DayjsService}]}); dayjsService = TestBed.inject(TIME_SERVICE) as DayjsService; @@ -16,11 +15,11 @@ describe('DayjsService', () => { describe('getDateFromString', () => { it('returns the date object from a string', () => { - expect(dayjsService.getDateFromString('2023-10-01')).toEqual(new Date(2023, 9, 1)); + expect(dayjsService.createDateFromString('2023-10-01')).toEqual(new Date(2023, 9, 1)); }); it('returns the date object from a string with a format', () => { - expect(dayjsService.getDateFromString('2023-10-01', 'YYYY-MM-DD')).toEqual(new Date(2023, 9, 1)); + expect(dayjsService.createDateFromString('2023-10-01', 'YYYY-MM-DD')).toEqual(new Date(2023, 9, 1)); }); }); @@ -42,14 +41,14 @@ describe('DayjsService', () => { it('returns the date object from a Unix timestamp', () => { const expectedDate = new Date(Date.UTC(2000, 0, 1)); // 946684800 is the Unix timestamp for 2000-01-01T00:00:00.000Z (from https://timestampgenerator.com/946684800/+00:00) - expect(dayjsService.getDateFromUnixTimestamp(946684800).getTime()).toEqual(expectedDate.getTime()); + expect(dayjsService.createDateFromUnixTimestamp(946684800).getTime()).toEqual(expectedDate.getTime()); }); }); describe('getUTCDateFromString', () => { it('parses the UTC date from a string', () => { - expect(dayjsService.getUTCDateFromString('2023-10-01', 'YYYY-MM-DD')).toEqual(new Date(Date.UTC(2023, 9, 1))); - expect(dayjsService.getUTCDateFromString('2023-10-01')).toEqual(new Date(Date.UTC(2023, 9, 1))); + expect(dayjsService.createUTCDateFromString('2023-10-01', 'YYYY-MM-DD')).toEqual(new Date(Date.UTC(2023, 9, 1))); + expect(dayjsService.createUTCDateFromString('2023-10-01')).toEqual(new Date(Date.UTC(2023, 9, 1))); }); }); @@ -86,8 +85,8 @@ describe('DayjsService', () => { describe('getPartial', () => { it('returns the correct partial value', () => { - expect(dayjsService.getPartialFromString('2023-10-01', 'years')).toBe(2023); - expect(dayjsService.getPartialFromString('2023-10-01', 'months')).toBe(9); // month is 0-indexed + expect(dayjsService.createPartialFromString('2023-10-01', 'years')).toBe(2023); + expect(dayjsService.createPartialFromString('2023-10-01', 'months')).toBe(9); // month is 0-indexed }); }); }); diff --git a/src/app/shared/services/dayjs.service.ts b/src/app/shared/services/dayjs.service.ts index 9a4ee4130..a6ff9f0cc 100644 --- a/src/app/shared/services/dayjs.service.ts +++ b/src/app/shared/services/dayjs.service.ts @@ -1,11 +1,12 @@ import {Injectable} from '@angular/core'; -import {DateUnit, TimeService} from '../interfaces/time-service.interface'; +import {TimeService} from '../interfaces/time-service.interface'; import dayjs, {ManipulateType} from 'dayjs'; -import duration, {Duration} from 'dayjs/plugin/duration'; +import durationPlugin, {Duration} from 'dayjs/plugin/duration'; import utc from 'dayjs/plugin/utc'; import customParseFormat from 'dayjs/plugin/customParseFormat'; +import {DateUnit} from '../types/date-unit.type'; -dayjs.extend(duration); +dayjs.extend(durationPlugin); dayjs.extend(customParseFormat); dayjs.extend(utc); @@ -13,7 +14,7 @@ dayjs.extend(utc); providedIn: 'root', }) export class DayjsService implements TimeService { - public getDateFromString(date: string, format?: string): Date { + public createDateFromString(date: string, format?: string): Date { return this.createDayjsObject(date, format).toDate(); } @@ -25,15 +26,15 @@ export class DayjsService implements TimeService { return this.createUTCDayjsObject(date).format(format); } - public getPartialFromString(date: string, unit: DateUnit): number { + public createPartialFromString(date: string, unit: DateUnit): number { return this.createDayjsObject(date).get(unit); } - public getDateFromUnixTimestamp(timestamp: number): Date { + public createDateFromUnixTimestamp(timestamp: number): Date { return dayjs.unix(timestamp).toDate(); } - public getUTCDateFromString(date: string, format?: string): Date { + public createUTCDateFromString(date: string, format?: string): Date { return this.createUTCDayjsObject(date, format).toDate(); } @@ -116,7 +117,6 @@ export class DayjsService implements TimeService { * Gets the whole given duration as a number value in the desired unit. */ private getDurationAsNumber(duration: Duration, unit: ManipulateType): number { - // todo: this one as well switch (unit) { case 'ms': case 'millisecond': diff --git a/src/app/shared/types/date-unit.type.ts b/src/app/shared/types/date-unit.type.ts new file mode 100644 index 000000000..be35cb5f9 --- /dev/null +++ b/src/app/shared/types/date-unit.type.ts @@ -0,0 +1 @@ +export type DateUnit = 'days' | 'months' | 'years' | 'hours' | 'minutes' | 'seconds' | 'milliseconds'; diff --git a/src/app/shared/types/dayjs-alias-type.ts b/src/app/shared/types/dayjs-alias-type.ts deleted file mode 100644 index b9b2b7fe3..000000000 --- a/src/app/shared/types/dayjs-alias-type.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {ManipulateType, UnitType} from 'dayjs'; - -export type ManipulateTypeAlias = ManipulateType; -export type UnitTypeAlias = UnitType; diff --git a/src/app/shared/utils/dayjs.utils.ts b/src/app/shared/utils/dayjs.utils.ts deleted file mode 100644 index c89734bb3..000000000 --- a/src/app/shared/utils/dayjs.utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import dayjs from 'dayjs'; -import duration from 'dayjs/plugin/duration'; -import utc from 'dayjs/plugin/utc'; -import customParseFormat from 'dayjs/plugin/customParseFormat'; - -dayjs.extend(duration); -dayjs.extend(customParseFormat); -dayjs.extend(utc); - -export class DayjsUtils { - public static parseUTCDate(date: string, format?: string): Date { - return dayjs.utc(date, format).toDate(); - } -} diff --git a/src/app/state/auth/effects/auth-status.effects.spec.ts b/src/app/state/auth/effects/auth-status.effects.spec.ts index b1ef4b2ab..3eeb10775 100644 --- a/src/app/state/auth/effects/auth-status.effects.spec.ts +++ b/src/app/state/auth/effects/auth-status.effects.spec.ts @@ -78,8 +78,8 @@ describe('AuthStatusEffects', () => { describe('login$', () => { it('logins using the AuthService and stores the current map state into a share link item; dispatches no further actions', (done: DoneFn) => { const shareLinkItem: ShareLinkItem = ShareLinkItemTestUtils.createShareLinkItem( - timeService.getUTCDateFromString('1000'), - timeService.getUTCDateFromString('2020'), + timeService.createUTCDateFromString('1000'), + timeService.createUTCDateFromString('2020'), ); const storageServiceSpy = spyOn(storageService, 'set'); store.overrideSelector(selectCurrentShareLinkItem, shareLinkItem); @@ -100,8 +100,8 @@ describe('AuthStatusEffects', () => { describe('logout$', () => { it('logouts using the AuthService and stores the current map state into a share link item; dispatches no further actions', (done: DoneFn) => { const shareLinkItem: ShareLinkItem = ShareLinkItemTestUtils.createShareLinkItem( - timeService.getUTCDateFromString('1000'), - timeService.getUTCDateFromString('2020'), + timeService.createUTCDateFromString('1000'), + timeService.createUTCDateFromString('2020'), ); const storageServiceSpy = spyOn(storageService, 'set'); store.overrideSelector(selectCurrentShareLinkItem, shareLinkItem); @@ -126,8 +126,8 @@ describe('AuthStatusEffects', () => { 'and loading an existing share link item from the session storage.', (done: DoneFn) => { const shareLinkItem: ShareLinkItem = ShareLinkItemTestUtils.createShareLinkItem( - timeService.getUTCDateFromString('1000'), - timeService.getUTCDateFromString('2020'), + timeService.createUTCDateFromString('1000'), + timeService.createUTCDateFromString('2020'), ); const shareLinkItemString = JSON.stringify(shareLinkItem); const storageServiceGetSpy = spyOn(storageService, 'get').and.returnValue(shareLinkItemString); diff --git a/src/app/state/map/actions/active-map-item.actions.ts b/src/app/state/map/actions/active-map-item.actions.ts index 9d0f66173..9b12f1b23 100644 --- a/src/app/state/map/actions/active-map-item.actions.ts +++ b/src/app/state/map/actions/active-map-item.actions.ts @@ -12,7 +12,7 @@ export const ActiveMapItemActions = createActionGroup({ events: { 'Add Active Map Item': props<{activeMapItem: ActiveMapItem; position: number}>(), 'Remove Active Map Item': props<{activeMapItem: ActiveMapItem}>(), - 'Replace Active Map Item': props<{activeMapItem: ActiveMapItem}>(), + 'Replace Active Map Item': props<{modifiedActiveMapItem: ActiveMapItem}>(), 'Remove All Active Map Items': emptyProps(), 'Remove Temporary Active Map Item': props<{activeMapItem: ActiveMapItem}>(), 'Remove All Temporary Active Map Items': emptyProps(), diff --git a/src/app/state/map/effects/active-map-item.effects.spec.ts b/src/app/state/map/effects/active-map-item.effects.spec.ts index 3926bae21..dc94bea48 100644 --- a/src/app/state/map/effects/active-map-item.effects.spec.ts +++ b/src/app/state/map/effects/active-map-item.effects.spec.ts @@ -582,7 +582,7 @@ describe('ActiveMapItemEffects', () => { expect(newAction).toBeUndefined(); })); - it('sets the time extent and reevaluates all layer visibilities, dispatches replaceActiveMapItem', () => { + it('sets the time extent and reevaluates all layer visibilities, dispatches replaceActiveMapItem', (done: DoneFn) => { const timeExtent: TimeExtent = { start: new Date(2023, 0, 1), end: new Date(2023, 11, 31), @@ -604,12 +604,13 @@ describe('ActiveMapItemEffects', () => { ], } as TimeSliderLayerSource, } as TimeSliderConfiguration; + mapMock.initialTimeSliderExtent = timeExtent; const activeMapItem = ActiveMapItemFactory.createGb2WmsMapItem(mapMock); store.overrideSelector(selectItems, [activeMapItem]); actions$ = of(ActiveMapItemActions.setTimeSliderExtent({timeExtent, activeMapItem})); - - effects.setTimeSliderExtent$.subscribe((action) => { + const expectedAction = ActiveMapItemActions.replaceActiveMapItem({modifiedActiveMapItem: activeMapItem}); + effects.setTimeSliderExtent$.subscribe(({modifiedActiveMapItem, type}) => { const expectedTimeExtent = timeExtent; const expectedLayers: Partial[] = [ {layer: 'layer01', visible: false}, @@ -617,10 +618,12 @@ describe('ActiveMapItemEffects', () => { {layer: 'layer03', visible: false}, ]; - expect(action).toEqual(ActiveMapItemActions.replaceActiveMapItem({activeMapItem: activeMapItem})); - expect(action.activeMapItem).toBeInstanceOf(Gb2WmsActiveMapItem); - expect((action.activeMapItem).settings.timeSliderExtent).toEqual(expectedTimeExtent); - expect((action.activeMapItem).settings.layers).toEqual(expectedLayers); + expect(type).toEqual(expectedAction.type); + expect(modifiedActiveMapItem).toBeInstanceOf(Gb2WmsActiveMapItem); + expect((modifiedActiveMapItem).settings.timeSliderExtent).toEqual(expectedTimeExtent); + expect((modifiedActiveMapItem).settings.layers).toEqual(expectedLayers); + + done(); }); }); }); diff --git a/src/app/state/map/effects/active-map-item.effects.ts b/src/app/state/map/effects/active-map-item.effects.ts index ecd49704c..248b4ad22 100644 --- a/src/app/state/map/effects/active-map-item.effects.ts +++ b/src/app/state/map/effects/active-map-item.effects.ts @@ -27,6 +27,7 @@ import {DrawingActions} from '../actions/drawing.actions'; import {LayerCatalogActions} from '../actions/layer-catalog.actions'; import {SearchActions} from '../../app/actions/search.actions'; import {TimeSliderService} from '../../../map/services/time-slider.service'; +import {produce} from 'immer'; @Injectable() export class ActiveMapItemEffects { @@ -367,24 +368,23 @@ export class ActiveMapItemEffects { if (!existingMapItem) { return undefined; } - const clonedMapItem = structuredClone(existingMapItem); - clonedMapItem.settings.timeSliderExtent = timeExtent; - clonedMapItem.settings.layers.forEach((layer) => { - const isVisible = this.timeSliderService.isLayerVisible( - layer, - existingMapItem.settings.timeSliderConfiguration, - existingMapItem.settings.timeSliderExtent, - ); - if (isVisible !== undefined) { - layer.visible = isVisible; - } + return produce(existingMapItem, (draft) => { + draft.settings.timeSliderExtent = timeExtent; + draft.settings.layers.forEach((layer) => { + const isVisible = this.timeSliderService.isLayerVisible( + layer, + existingMapItem.settings.timeSliderConfiguration, + existingMapItem.settings.timeSliderExtent, + ); + if (isVisible !== undefined) { + layer.visible = isVisible; + } + }); }); - - return clonedMapItem; }), - filter((a) => a !== undefined), - map((a) => ActiveMapItemActions.replaceActiveMapItem({activeMapItem: a})), + filter((modifiedActiveMapItem) => modifiedActiveMapItem !== undefined), + map((modifiedActiveMapItem) => ActiveMapItemActions.replaceActiveMapItem({modifiedActiveMapItem})), ); }); diff --git a/src/app/state/map/effects/share-link.effects.spec.ts b/src/app/state/map/effects/share-link.effects.spec.ts index 1af7bf1b2..9e9fcc2e2 100644 --- a/src/app/state/map/effects/share-link.effects.spec.ts +++ b/src/app/state/map/effects/share-link.effects.spec.ts @@ -106,7 +106,7 @@ describe('ShareLinkEffects', () => { opacity: 0.5, visible: true, isSingleLayer: false, - timeExtent: {start: timeService.getUTCDateFromString('1000'), end: timeService.getUTCDateFromString('2020')}, + timeExtent: {start: timeService.createUTCDateFromString('1000'), end: timeService.createUTCDateFromString('2020')}, attributeFilters: [ { parameter: 'FILTER_GEBART', diff --git a/src/app/state/map/reducers/active-map-item.reducer.spec.ts b/src/app/state/map/reducers/active-map-item.reducer.spec.ts index 997923be1..4a5251364 100644 --- a/src/app/state/map/reducers/active-map-item.reducer.spec.ts +++ b/src/app/state/map/reducers/active-map-item.reducer.spec.ts @@ -438,13 +438,13 @@ describe('ActiveMapItem Reducer', () => { describe('replaceActiveMapItem', () => { it('replaces the active map item correctly', () => { - const modifiedItem = structuredClone(activeMapItemsMock[1]); + const modifiedActiveMapItem = structuredClone(activeMapItemsMock[1]); const modifiedOpacity = activeMapItemsMock[1].opacity + 1337; - modifiedItem.opacity = modifiedOpacity; + modifiedActiveMapItem.opacity = modifiedOpacity; existingState.items = activeMapItemsMock; - const action = ActiveMapItemActions.replaceActiveMapItem({activeMapItem: modifiedItem}); + const action = ActiveMapItemActions.replaceActiveMapItem({modifiedActiveMapItem}); const state = reducer(existingState, action); expect((state.items[1]).opacity).toEqual(modifiedOpacity); diff --git a/src/app/state/map/reducers/active-map-item.reducer.ts b/src/app/state/map/reducers/active-map-item.reducer.ts index 52dfd7d66..7628abde3 100644 --- a/src/app/state/map/reducers/active-map-item.reducer.ts +++ b/src/app/state/map/reducers/active-map-item.reducer.ts @@ -173,11 +173,11 @@ export const activeMapItemFeature = createFeature({ ), on( ActiveMapItemActions.replaceActiveMapItem, - produce((draft, {activeMapItem}) => { - const existing = draft.items.find((mapItem) => mapItem.id === activeMapItem.id); + produce((draft, {modifiedActiveMapItem}) => { + const existing = draft.items.find((mapItem) => mapItem.id === modifiedActiveMapItem.id); if (existing) { const index = draft.items.indexOf(existing); - draft.items.splice(index, 1, activeMapItem); + draft.items.splice(index, 1, modifiedActiveMapItem); } }), ), diff --git a/src/test.ts b/src/test.ts index 05d4b9b34..56306628d 100644 --- a/src/test.ts +++ b/src/test.ts @@ -11,7 +11,6 @@ getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting([ // we provide the time service here so it is the same as in the running app and we don't have to inject it in each test - // todo: add to documentation { provide: TIME_SERVICE, useFactory: timeServiceFactory, From 9f5286d87869ac43c4dff61fa4e5c89c77b4ced8 Mon Sep 17 00:00:00 2001 From: Lukas Merz Date: Wed, 25 Sep 2024 15:21:39 +0200 Subject: [PATCH 11/12] GB3-1361: Fix tests --- .../map/services/favourites.service.spec.ts | 38 +++++++++++++++++++ .../selectors/search-results.selector.spec.ts | 10 +++++ 2 files changed, 48 insertions(+) diff --git a/src/app/map/services/favourites.service.spec.ts b/src/app/map/services/favourites.service.spec.ts index e89c149cc..2c02bda4a 100644 --- a/src/app/map/services/favourites.service.spec.ts +++ b/src/app/map/services/favourites.service.spec.ts @@ -103,6 +103,8 @@ describe('FavouritesService', () => { title: 'Fruchtfolgeflächen (FFF)', keywords: ['Fruchtfolgeflächen', '(FFF)', 'bvv', 'boden', 'TBAK2', 'fsla', 'fabo', 'vp', 'fap'], opacity: 1, + timeSliderConfiguration: undefined, + initialTimeSliderExtent: undefined, layers: [ { id: 157886, @@ -147,6 +149,8 @@ describe('FavouritesService', () => { title: 'Amtliche Vermessung in Farbe', keywords: ['Amtliche', 'Vermessung', 'in', 'Farbe', 'pk', 'Amtlichen Vermessung', 'AV'], opacity: 1, + timeSliderConfiguration: undefined, + initialTimeSliderExtent: undefined, layers: [ { id: 151493, @@ -312,6 +316,8 @@ describe('FavouritesService', () => { title: 'LiDAR-Befliegung 2021 ZH', keywords: ['LiDAR-Befliegung', '2021', 'ZH'], opacity: 1, + timeSliderConfiguration: undefined, + initialTimeSliderExtent: undefined, layers: [ { id: 159533, @@ -956,6 +962,10 @@ describe('FavouritesService', () => { layerIdentifiers: ['geb-alter_wohnen', 'geb-alter_grau', 'geb-alter_2'], }, }, + initialTimeSliderExtent: { + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), + }, filterConfigurations: [ { name: 'Anzeigeoptionen nach Hauptnutzung', @@ -1135,6 +1145,10 @@ describe('FavouritesService', () => { layerIdentifiers: ['geb-alter_wohnen', 'geb-alter_grau', 'geb-alter_2'], }, }, + initialTimeSliderExtent: { + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), + }, filterConfigurations: [ { name: 'Anzeigeoptionen nach Hauptnutzung', @@ -1294,6 +1308,10 @@ describe('FavouritesService', () => { layerIdentifiers: ['geb-alter_wohnen', 'geb-alter_grau', 'geb-alter_2'], }, }, + initialTimeSliderExtent: { + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), + }, filterConfigurations: [ { name: 'Anzeigeoptionen nach Hauptnutzung', @@ -1450,6 +1468,10 @@ describe('FavouritesService', () => { layerIdentifiers: ['geb-alter_wohnen', 'geb-alter_grau', 'geb-alter_2'], }, }, + initialTimeSliderExtent: { + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), + }, filterConfigurations: [ { name: 'Anzeigeoptionen nach Hauptnutzung', @@ -1606,6 +1628,10 @@ describe('FavouritesService', () => { layerIdentifiers: ['geb-alter_wohnen', 'geb-alter_grau', 'geb-alter_2'], }, }, + initialTimeSliderExtent: { + start: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + }, filterConfigurations: [ { name: 'Anzeigeoptionen nach Hauptnutzung', @@ -1762,6 +1788,10 @@ describe('FavouritesService', () => { layerIdentifiers: ['geb-alter_wohnen', 'geb-alter_grau', 'geb-alter_2'], }, }, + initialTimeSliderExtent: { + start: timeService.createUTCDateFromString('1000-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2020-01-01T00:00:00.000Z'), + }, filterConfigurations: [ { name: 'Anzeigeoptionen nach Hauptnutzung', @@ -2035,6 +2065,10 @@ describe('FavouritesService', () => { ], }, }, + initialTimeSliderExtent: { + start: timeService.createUTCDateFromString('2016-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2017-01-01T00:00:00.000Z'), + }, filterConfigurations: undefined, }, ]; @@ -2302,6 +2336,10 @@ describe('FavouritesService', () => { ], }, }, + initialTimeSliderExtent: { + start: timeService.createUTCDateFromString('2014-01-01T00:00:00.000Z'), + end: timeService.createUTCDateFromString('2021-01-01T00:00:00.000Z'), + }, filterConfigurations: undefined, }, ]; diff --git a/src/app/state/app/selectors/search-results.selector.spec.ts b/src/app/state/app/selectors/search-results.selector.spec.ts index f71961744..86d741d40 100644 --- a/src/app/state/app/selectors/search-results.selector.spec.ts +++ b/src/app/state/app/selectors/search-results.selector.spec.ts @@ -210,6 +210,8 @@ describe('search-result selector', () => { layers: [], printTitle: '', opacity: 1, + timeSliderConfiguration: undefined, + initialTimeSliderExtent: undefined, }, { // matching by title @@ -226,6 +228,8 @@ describe('search-result selector', () => { layers: [], printTitle: '', opacity: 1, + timeSliderConfiguration: undefined, + initialTimeSliderExtent: undefined, }, { // not matching @@ -242,6 +246,8 @@ describe('search-result selector', () => { layers: [], printTitle: '', opacity: 1, + timeSliderConfiguration: undefined, + initialTimeSliderExtent: undefined, }, { // matching by title and keywords @@ -258,6 +264,8 @@ describe('search-result selector', () => { layers: [], printTitle: '', opacity: 1, + timeSliderConfiguration: undefined, + initialTimeSliderExtent: undefined, }, { // partially matching by title @@ -274,6 +282,8 @@ describe('search-result selector', () => { layers: [], printTitle: '', opacity: 1, + timeSliderConfiguration: undefined, + initialTimeSliderExtent: undefined, }, ]; } From 68fd8eee90dfaa27808c865e76d8ed9bd31205bf Mon Sep 17 00:00:00 2001 From: Lukas Merz Date: Wed, 2 Oct 2024 11:07:01 +0200 Subject: [PATCH 12/12] GB3-1361: Fix PR issues --- .../time-slider/time-slider.component.ts | 6 ++--- src/app/map/services/time-slider.service.ts | 22 ++++++------------- .../interfaces/time-service.interface.ts | 2 +- .../services/abstract-storage.service.ts | 2 +- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/app/map/components/time-slider/time-slider.component.ts b/src/app/map/components/time-slider/time-slider.component.ts index 229be1333..cff9dd0f9 100644 --- a/src/app/map/components/time-slider/time-slider.component.ts +++ b/src/app/map/components/time-slider/time-slider.component.ts @@ -11,7 +11,7 @@ import {DateUnit} from '../../../shared/types/date-unit.type'; // To be able to extract a union type subset of `ManipulateType` AND to have an array used to check if a given value is in said union type. // => more infos: https://stackoverflow.com/questions/50085494/how-to-check-if-a-given-value-is-in-a-union-type-array const allowedDatePickerManipulationUnits = ['years', 'months', 'days'] as const; // TS3.4 syntax -export type DatePickerManipulationUnits = Extract; +type DatePickerManipulationUnits = Extract; type DatePickerStartView = 'month' | 'year' | 'multi-year'; @Component({ @@ -58,7 +58,7 @@ export class TimeSliderComponent implements OnInit, OnChanges { this.timeExtentDisplay = {start: this.initialTimeExtent.start, end: this.initialTimeExtent.end}; this.firstSliderPosition = this.findPositionOfDate(this.timeExtent.start) ?? 0; this.secondSliderPosition = this.timeSliderConfiguration.range ? undefined : this.findPositionOfDate(this.timeExtent.end); - this.hasSimpleCurrentValue = this.isRangeExactlyOneOfSingleTimeUnit(this.timeSliderConfiguration.range); + this.hasSimpleCurrentValue = this.isStringSingleTimeUnitRange(this.timeSliderConfiguration.range); // date picker this.hasDatePicker = this.isRangeContinuousWithinAllowedTimeUnits(this.timeSliderConfiguration); @@ -163,7 +163,7 @@ export class TimeSliderComponent implements OnInit, OnChanges { } } - private isRangeExactlyOneOfSingleTimeUnit(range: string | null | undefined): boolean { + private isStringSingleTimeUnitRange(range: string | null | undefined): boolean { return range ? this.timeService.isStringSingleTimeUnitRange(range) : false; } diff --git a/src/app/map/services/time-slider.service.ts b/src/app/map/services/time-slider.service.ts index 0d355f5bb..2c54270df 100644 --- a/src/app/map/services/time-slider.service.ts +++ b/src/app/map/services/time-slider.service.ts @@ -19,7 +19,7 @@ export class TimeSliderService { const maximumDate: Date = this.timeService.createUTCDateFromString(timeSliderConfig.maximumDate, timeSliderConfig.dateFormat); return { start: minimumDate, - end: timeSliderConfig.range ? this.addRangeToDate(minimumDate, timeSliderConfig.range) : maximumDate, + end: timeSliderConfig.range ? this.timeService.addRangeToDate(minimumDate, timeSliderConfig.range) : maximumDate, }; } @@ -84,7 +84,7 @@ export class TimeSliderService { The start has changed as fixed ranges technically don't have an end date => the end date has to be adjusted accordingly to enforce the fixed range between start and end date */ - timeExtent.end = this.addRangeToDate(timeExtent.start, timeSliderConfig.range); + timeExtent.end = this.timeService.addRangeToDate(timeExtent.start, timeSliderConfig.range); } else if (timeSliderConfig.minimalRange) { /* Minimal range @@ -110,16 +110,16 @@ export class TimeSliderService { if (startEndDiff < minimalRangeInMs) { if (hasStartDateChanged) { - const newStartDate = this.subtractRangeFromDate(timeExtent.end, timeSliderConfig.minimalRange); + const newStartDate = this.timeService.subtractRangeFromDate(timeExtent.end, timeSliderConfig.minimalRange); timeExtent.start = this.validateDateWithinLimits(newStartDate, minimumDate, maximumDate); if (this.timeService.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end) < minimalRangeInMs) { - timeExtent.end = this.addRangeToDate(timeExtent.start, timeSliderConfig.minimalRange); + timeExtent.end = this.timeService.addRangeToDate(timeExtent.start, timeSliderConfig.minimalRange); } } else { - const newEndDate = this.addRangeToDate(timeExtent.start, timeSliderConfig.minimalRange); + const newEndDate = this.timeService.addRangeToDate(timeExtent.start, timeSliderConfig.minimalRange); timeExtent.end = this.validateDateWithinLimits(newEndDate, minimumDate, maximumDate); if (this.timeService.calculateDifferenceBetweenDates(timeExtent.start, timeExtent.end) < minimalRangeInMs) { - timeExtent.start = this.subtractRangeFromDate(timeExtent.end, timeSliderConfig.minimalRange); + timeExtent.start = this.timeService.subtractRangeFromDate(timeExtent.end, timeSliderConfig.minimalRange); } } } @@ -160,10 +160,6 @@ export class TimeSliderService { return undefined; } - private addRangeToDate(date: Date, range: string): Date { - return this.timeService.addRangeToDate(date, range); - } - /** * Creates stops for a layer source containing multiple dates which may not necessarily have constant gaps between them. */ @@ -205,7 +201,7 @@ export class TimeSliderService { dates.push(date); if (initialRange) { - date = this.addRangeToDate(date, initialRange); + date = this.timeService.addRangeToDate(date, initialRange); } else if (unit) { date = this.addMinimalRange(date, unit); } else { @@ -253,8 +249,4 @@ export class TimeSliderService { } return validDate; } - - private subtractRangeFromDate(date: Date, range: string): Date { - return this.timeService.subtractRangeFromDate(date, range); - } } diff --git a/src/app/shared/interfaces/time-service.interface.ts b/src/app/shared/interfaces/time-service.interface.ts index 653688f72..8b96a2c65 100644 --- a/src/app/shared/interfaces/time-service.interface.ts +++ b/src/app/shared/interfaces/time-service.interface.ts @@ -13,7 +13,7 @@ export interface TimeService { * Creates a date object from a Unix timestamp. */ createDateFromUnixTimestamp: (timestamp: number) => Date; - /* + /** * Creates a date object from a string in UTC; with an optional format in ISO 8601. */ createUTCDateFromString: (date: string, format?: string) => Date; diff --git a/src/app/shared/services/abstract-storage.service.ts b/src/app/shared/services/abstract-storage.service.ts index 1a6a107e2..b837237d2 100644 --- a/src/app/shared/services/abstract-storage.service.ts +++ b/src/app/shared/services/abstract-storage.service.ts @@ -20,7 +20,7 @@ export abstract class AbstractStorageService