From 4241bd2cb9226f629ef2315335865a52d3fffa0a Mon Sep 17 00:00:00 2001 From: Arno Kaimbacher Date: Tue, 28 Sep 2021 16:26:53 +0200 Subject: [PATCH] - add "moment": "^2.29.1" and "chartjs-adapter-moment": "^1.0.0", - add core module for time.service.ts - add interfaces for timespan.ts an dataset.ts --- notes.txt | 5 + package-lock.json | 17 + package.json | 2 + src/app/app.module.ts | 4 +- src/app/services/app-router.service.ts | 2 +- src/app/services/dataset-api.service.ts | 225 ++++++++++++- src/app/services/dataset.service.ts | 34 ++ .../diagram-view/diagram-view.component.html | 3 +- src/common/core/core.module.ts | 19 ++ src/common/core/time/time.service.ts | 52 +++ .../geomon-timeseries-chart.component.html | 4 +- .../geomon-timeseries-chart.component.ts | 312 +++++++++++++++--- src/common/graphjs/graphjs.module.ts | 4 +- src/shared/models/dataset.ts | 50 +++ src/shared/models/timespan.ts | 30 ++ 15 files changed, 706 insertions(+), 57 deletions(-) create mode 100644 src/common/core/core.module.ts create mode 100644 src/common/core/time/time.service.ts create mode 100644 src/shared/models/timespan.ts diff --git a/notes.txt b/notes.txt index de294d9..1bd4407 100644 --- a/notes.txt +++ b/notes.txt @@ -222,3 +222,8 @@ CREATE src/app/app-router.service.ts (138 bytes) =============================== chart.js ====================================== npm install --save chart.js + +npm install --save moment + +npm install moment chartjs-adapter-moment --save +import 'chartjs-adapter-moment'; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 85457eb..069a745 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,8 +27,10 @@ "babel-plugin-transform-typescript-metadata": "^0.3.2", "bulma": "^0.9.3", "chart.js": "^3.5.1", + "chartjs-adapter-moment": "^1.0.0", "core-js": "^3.16.0", "leaflet": "^1.7.1", + "moment": "^2.29.1", "rxjs": "^7.3.0", "zone.js": "^0.11.4" }, @@ -5012,6 +5014,15 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz", "integrity": "sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ==" }, + "node_modules/chartjs-adapter-moment": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.0.tgz", + "integrity": "sha512-PqlerEvQcc5hZLQ/NQWgBxgVQ4TRdvkW3c/t+SUEQSj78ia3hgLkf2VZ2yGJtltNbEEFyYGm+cA6XXevodYvWA==", + "peerDependencies": { + "chart.js": "^3.0.0", + "moment": "^2.10.2" + } + }, "node_modules/chokidar": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", @@ -22990,6 +23001,12 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz", "integrity": "sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ==" }, + "chartjs-adapter-moment": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.0.tgz", + "integrity": "sha512-PqlerEvQcc5hZLQ/NQWgBxgVQ4TRdvkW3c/t+SUEQSj78ia3hgLkf2VZ2yGJtltNbEEFyYGm+cA6XXevodYvWA==", + "requires": {} + }, "chokidar": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", diff --git a/package.json b/package.json index 875a39a..dd8f077 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,10 @@ "babel-plugin-transform-typescript-metadata": "^0.3.2", "bulma": "^0.9.3", "chart.js": "^3.5.1", + "chartjs-adapter-moment": "^1.0.0", "core-js": "^3.16.0", "leaflet": "^1.7.1", + "moment": "^2.29.1", "rxjs": "^7.3.0", "zone.js": "^0.11.4" } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8b2482d..9ee98f9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -10,6 +10,7 @@ import { MapViewComponent } from "./views/map-view/map-view.component"; import { ComponentsModule } from '../../src/common/components/components.module'; import { GraphjsModule } from '../../src/common/graphjs/graphjs.module'; +// import { CoreModule } from '../../src/common/core/core.module'; import { HttpClientModule, HttpClient } from '@angular/common/http'; //for http requests import { MarkerService } from './services/marker.service'; @@ -32,6 +33,7 @@ import { DatasetByStationSelectorComponent } from './components/dataset-by-stati import { MatListModule } from '@angular/material/list'; import {MatBadgeModule} from '@angular/material/badge'; +import { TimeService } from '../common/core/time/time.service'; import { InternalIdHandler } from '../common/components/services/internal-id-handler.service'; @NgModule({ @@ -43,7 +45,7 @@ import { InternalIdHandler } from '../common/components/services/internal-id-han // imports: Other modules whose exported classes are needed by component templates declared in this NgModule. imports: [BrowserModule, HttpClientModule, AppRoutingModule, ComponentsModule, GraphjsModule, BrowserAnimationsModule, MatDialogModule, MatListModule, MatBadgeModule], providers: [ - MarkerService, PopupService, HttpService, DatasetApiService, StationService, MessageService, MapService,DatasetService, InternalIdHandler + MarkerService, PopupService, HttpService, DatasetApiService, StationService, MessageService, MapService,DatasetService, InternalIdHandler, TimeService // { // provide: DatasetApiInterface, diff --git a/src/app/services/app-router.service.ts b/src/app/services/app-router.service.ts index dd8e619..a9a03bd 100644 --- a/src/app/services/app-router.service.ts +++ b/src/app/services/app-router.service.ts @@ -13,7 +13,7 @@ export class AppRouterService { } public toDiagram() { - this.router.navigate(['diagram']) + this.router.navigate(['diagram']); } } diff --git a/src/app/services/dataset-api.service.ts b/src/app/services/dataset-api.service.ts index b2a6926..2e8cae3 100644 --- a/src/app/services/dataset-api.service.ts +++ b/src/app/services/dataset-api.service.ts @@ -3,7 +3,7 @@ // https://github.com/52North/helgoland-toolbox/blob/495f75cadcc3e7232206db1cd5dd8bbf3a172c4b/libs/core/src/lib/api-communication/connectors/dataset-api-v3-connector/api-v3-interface.ts#L321 import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Observable, Observer, forkJoin } from 'rxjs'; import { Phenomenon } from '../../shared/models/phenomenon'; import { Station } from '../../shared/models/station'; @@ -15,12 +15,64 @@ import { GeomonPlatform } from '../../shared/models/platform'; import { Dataset, GeomonTimeseries } from '../../shared/models/dataset'; import { deserialize } from 'class-transformer'; -import { map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { InternalIdHandler } from '../../common/components/services/internal-id-handler.service'; + +import moment from "moment"; +import { Timespan } from '../../shared/models/timespan'; +import { GeomonDataset } from '../../shared/models/dataset'; + +import { GeomonTimeseriesData, Data, HelgolandDataFilter } from '../../shared/models/dataset'; // @Injectable({ // providedIn: 'root' // }) + + +export interface ParameterFilter { + service?: string; + category?: string; + offering?: string; + phenomenon?: string; + procedure?: string; + feature?: string; + valueTypes?: string; + platformTypes?: string; + expanded?: boolean; + lang?: string; + [key: string]: any; +} + + +export interface ApiV3DatasetDataFilter { + timespan?: string; + generalize?: boolean; + format?: string; + unixTime?: boolean; + expanded?: boolean; +} + +type TimeValueTuple = [number, number]; + +class UriParameterCoder { + + public encodeKey(key: string): string { + return encodeURIComponent(key); + } + + public encodeValue(value: string): string { + return encodeURIComponent(value); + } + + public decodeKey(key: string): string { + return key; + } + + public decodeValue(value: string): string { + return value; + } +} + @Injectable() export class DatasetApiService { @@ -60,7 +112,7 @@ export class DatasetApiService { return this.getStations(apiUrl, params, options).pipe(map(res => res.map(f => this.createGeomonPlatform(f)))); } - + public getPlatform( id: string, @@ -70,20 +122,157 @@ export class DatasetApiService { ): Observable { // const url = this.createRequestUrl(apiUrl, 'platforms', id); // return this.requestApi(url, params, options); - return this.getFeature(id, apiUrl,params, options).pipe(map(res => this.createGeomonPlatform(res))); + return this.getFeature(id, apiUrl, params, options).pipe(map(res => this.createGeomonPlatform(res))); } public getDataset(id: string, apiUrl: string, params?: any, options?: HttpRequestOptions): Observable { const url = this.createRequestUrl(apiUrl, 'datasets', id); return this.requestApi(url, params, options) - .pipe( - map((res) => this.prepareDataset(res, apiUrl)) + .pipe( + map((res) => this.prepareDataset(res, apiUrl)) + ); + } + + getDatasetData(dataset: GeomonDataset, timespan: Timespan, filter: HelgolandDataFilter): Observable { + + const maxTimeExtent = moment.duration(1, 'year').asMilliseconds();//31536000000 + const params: ApiV3DatasetDataFilter = { format: 'flot' }; + if (filter.expanded !== undefined) { params.expanded = filter.expanded }; + if (filter.generalize !== undefined) { params.generalize = filter.generalize }; + // if greater than one year + if ((timespan.to - timespan.from) > maxTimeExtent) { + const requests: Array> = []; + let start = moment(timespan.from).startOf('year'); + let end = moment(timespan.from).endOf('year'); + while (start.isBefore(moment(timespan.to))) { + const chunkSpan = new Timespan(start.unix() * 1000, end.unix() * 1000); + params.timespan = this.createRequestTimespan(chunkSpan); + requests.push( + this.getApiData(dataset.id, dataset.url, params) + .pipe(map(res => { + return this.createTimeseriesData(res); + // return res; + })) + ); + start = end.add(1, 'millisecond'); + end = moment(start).endOf('year'); + } + return forkJoin(requests).pipe(map((e) => { + const mergedResult = e.reduce((previous, current) => { + const next: GeomonTimeseriesData = new GeomonTimeseriesData(previous.values.concat(current.values)); + if (previous.valueBeforeTimespan) { + next.valueBeforeTimespan = previous.valueBeforeTimespan; + } + if (current.valueAfterTimespan) { + next.valueAfterTimespan = current.valueAfterTimespan; + } + for (const key in previous.referenceValues) { + if (previous.referenceValues.hasOwnProperty(key)) { + next.referenceValues[key] = { + values: previous.referenceValues[key].values.concat(current.referenceValues[key].values) + }; + if (previous.referenceValues[key].valueBeforeTimespan) { + next.referenceValues[key].valueBeforeTimespan = previous.referenceValues[key].valueBeforeTimespan; + } + if (current.referenceValues[key].valueAfterTimespan) { + next.referenceValues[key].valueAfterTimespan = current.referenceValues[key].valueAfterTimespan; + } + } + } + return next; + }); + if (mergedResult.values && mergedResult.values.length > 0) { + // cut first + const fromIdx = mergedResult.values.findIndex(el => el[0] >= timespan.from); + mergedResult.values = mergedResult.values.slice(fromIdx); + // cut last + const toIdx = mergedResult.values.findIndex(el => el[0] >= timespan.to); + if (toIdx >= 0) { mergedResult.values = mergedResult.values.slice(0, toIdx + 1); } + } + return mergedResult; + })); + } + else { + params.timespan = this.createRequestTimespan(timespan); + if (filter.expanded !== undefined) { params.expanded = filter.expanded }; + return this.getApiData(dataset.id, dataset.url, params) + .pipe(map(res => this.createTimeseriesData(res))); + } + + + + } + + protected createTimeseriesData(res: Data): GeomonTimeseriesData { + const data = new GeomonTimeseriesData(res.values); + data.referenceValues = res.referenceValues ? res.referenceValues : {}; + if (res.valueBeforeTimespan) { + data.valueBeforeTimespan = res.valueBeforeTimespan; + } + if (res.valueAfterTimespan) { + data.valueAfterTimespan = res.valueAfterTimespan; + } + return data; + } + + public getApiData(id: string, apiUrl: string, params?: ApiV3DatasetDataFilter): Observable> { + const url = this.createRequestUrl(apiUrl, 'datasets', `${id}/observations`); + return this.requestApi>(url, this.prepareParams(params)).pipe( + map(res => { + // if (params.expanded) { res = res[id]; } + return res; + }) ); } - //#region Helper method + protected prepareParams(params: any): HttpParams { + let httpParams = new HttpParams({ encoder: new UriParameterCoder() }); + if (params) { + Object.getOwnPropertyNames(params).forEach((key) => { + if (params[key] instanceof Array) { + httpParams = httpParams.set(key, params[key].join(',')); + } else { + httpParams = httpParams.set(key, params[key]); + } + }); + } + return httpParams; + } - private prepareDataset(datasetObj:GeomonTimeseries, apiUrl: string) { + + // getDatasetData(dataset: GeomonDataset, timespan: Timespan, filter: HelgolandDataFilter): Observable { + // const dataFilter = this.createDataFilter(filter); + // dataFilter.format = 'flot'; + // return this.api.getTsData(dataset.id, dataset.url, timespan, dataFilter).pipe(map(res => { + // const data = new HelgolandTimeseriesData(res.values); + // data.referenceValues = res.referenceValues ? res.referenceValues : {}; + // if (res.valueBeforeTimespan) { data.valueBeforeTimespan = res.valueBeforeTimespan; } + // if (res.valueAfterTimespan) { data.valueAfterTimespan = res.valueAfterTimespan; } + // return data; + // })); + // } + + + // public getTsData( + // id: string, + // apiUrl: string, + // timespan: Timespan, + // params: DataParameterFilter = {}, + // options: HttpRequestOptions + // ): Observable> { + // const url = this.createRequestUrl(apiUrl, 'timeseries', id) + '/getData'; + // params.timespan = this.createRequestTimespan(timespan); + // return this.requestApi>(url, params, options).pipe( + // map((res: any) => { + // if (params.expanded) { res = res[id]; } + // return res; + // })); + // } + + + //#region Helper method + + private prepareDataset(datasetObj: GeomonTimeseries, apiUrl: string) { let dataset = deserialize(GeomonTimeseries, JSON.stringify(datasetObj)); dataset.url = apiUrl; this.internalDatasetId.generateInternalId(dataset); @@ -97,12 +286,12 @@ export class DatasetApiService { protected createGeomonPlatform(feature: Station): GeomonPlatform { const datasetIds = []; for (const key in feature.properties.datasets) { - if (feature.properties.datasets.hasOwnProperty(key)) { - datasetIds.push(key); - } + if (feature.properties.datasets.hasOwnProperty(key)) { + datasetIds.push(key); + } } return new GeomonPlatform(feature.id, feature.properties.label, datasetIds, feature.geometry); - } + } protected createRequestUrl(apiUrl: string, endpoint: string, id?: string) { // TODO Check whether apiUrl ends with slash @@ -111,6 +300,12 @@ export class DatasetApiService { return requestUrl; } + private requestApiTextedPost(url: string, params: ParameterFilter = {}, options: HttpRequestOptions = {}): Observable { + return this.httpService.client().post(url, params, { + responseType: 'json' + }); + } + protected requestApi(url: string, params: HttpParams = new HttpParams(), options: HttpRequestOptions = {} ): Observable { return this.httpService.client(options).get(url, @@ -121,6 +316,12 @@ export class DatasetApiService { ); } + protected createRequestTimespan(timespan: Timespan): string { + return encodeURI(moment(timespan.from).format() + '/' + moment(timespan.to).format()); + } + + + protected createBasicAuthHeader(token: string): HttpHeaders { const headers = new HttpHeaders(); if (token) { return headers.set('Authorization', token); } diff --git a/src/app/services/dataset.service.ts b/src/app/services/dataset.service.ts index 2b6baab..0dfa21f 100644 --- a/src/app/services/dataset.service.ts +++ b/src/app/services/dataset.service.ts @@ -1,14 +1,40 @@ import { Injectable } from '@angular/core'; import { EventEmitter } from '@angular/core'; +import { Timespan } from '../../shared/models/timespan'; +import * as moment from "moment"; + +const TIMESERIES_OPTIONS_CACHE_PARAM = 'timeseriesOptions'; +const TIMESERIES_IDS_CACHE_PARAM = 'timeseriesIds'; +const TIME_CACHE_PARAM = 'timeseriesTime'; +// https://github.com/52North/helgoland-toolbox/blob/fe6af1b9df0e5d78eeec236e4690aeb7dc92119b/apps/helgoland/src/app/services/timeseries-service.service.ts#L22 + +import { TimeService } from '../../common/core/time/time.service'; @Injectable() export class DatasetService { public datasetIds: string[] = []; public datasetService: Map = new Map(); + + private _timespan: Timespan; public datasetIdsChanged: EventEmitter = new EventEmitter(); + constructor(private timeService: TimeService) { + this.initTimespan(); + } + + + public get timespan(): Timespan { + return this._timespan; + } + + public set timespan(v: Timespan) { + this._timespan = v; + // this.timeSrvc.saveTimespan(TIME_CACHE_PARAM, this._timespan); + } + + /** * Adds the dataset to the selection * @@ -67,4 +93,12 @@ export class DatasetService { this.datasetService.set(internalId, options); // this.saveState(); } + + private initTimespan() { + if (!this._timespan) { + this._timespan = new Timespan(1323239694000, 1323844494000); + //this.timeService.createByDurationWithEnd(moment.duration(1, 'days'), new Date(2011, 9), 'day'); + } + } + } \ No newline at end of file diff --git a/src/app/views/diagram-view/diagram-view.component.html b/src/app/views/diagram-view/diagram-view.component.html index 4c1af0f..6eceb77 100644 --- a/src/app/views/diagram-view/diagram-view.component.html +++ b/src/app/views/diagram-view/diagram-view.component.html @@ -1,3 +1,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/common/core/core.module.ts b/src/common/core/core.module.ts new file mode 100644 index 0000000..b5b5929 --- /dev/null +++ b/src/common/core/core.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + + +import { DatasetApiService } from '../../app/services/dataset-api.service'; + + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [ + ], + exports: [ + ], + providers: [ + ] +}) +export class CoreModule { } \ No newline at end of file diff --git a/src/common/core/time/time.service.ts b/src/common/core/time/time.service.ts new file mode 100644 index 0000000..26ddad5 --- /dev/null +++ b/src/common/core/time/time.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; + +import { Timespan, TimeInterval, BufferedTime } from '../../../shared/models/timespan'; +import moment from 'moment'; +// import 'moment-duration-format'; + +@Injectable() +export class TimeService { + + constructor() { } + + public getBufferedTimespan(timespan: Timespan, factor: number, maxBufferInMs?: number): Timespan { + const durationMillis = this.getDuration(timespan).asMilliseconds(); + let buffer = durationMillis * factor; + if (maxBufferInMs && buffer > maxBufferInMs) { + buffer = maxBufferInMs; + } + const from = timespan.from - buffer; + const to = timespan.to + buffer; + return new Timespan(from, to); + } + + + private getDuration(timespan: Timespan): moment.Duration { + const from = moment(timespan.from); + const to = moment(timespan.to); + return moment.duration(to.diff(from)); + } + + public createByDurationWithEnd(d: moment.Duration, end: number | Date, endOf?: moment.unitOfTime.StartOf): Timespan { + const mEnd = moment(end); + if (endOf) { + mEnd.endOf(endOf); + } + const mStart = moment(mEnd).subtract(d); + return new Timespan(mStart.toDate(), mEnd.toDate()); + } + + public createTimespanOfInterval(timeInterval: TimeInterval): Timespan { + + if(timeInterval instanceof BufferedTime) { + const d = moment.duration(timeInterval.bufferInterval / 2); + const from = moment(timeInterval.timestamp).subtract(d).unix() * 1000; + const to = moment(timeInterval.timestamp).add(d).unix() * 1000; + return new Timespan(from, to); + } else { + return timeInterval as Timespan; + } + } + + +} \ No newline at end of file diff --git a/src/common/graphjs/geomon-timeseries-chart/geomon-timeseries-chart.component.html b/src/common/graphjs/geomon-timeseries-chart/geomon-timeseries-chart.component.html index 07935db..b1d28d4 100644 --- a/src/common/graphjs/geomon-timeseries-chart/geomon-timeseries-chart.component.html +++ b/src/common/graphjs/geomon-timeseries-chart/geomon-timeseries-chart.component.html @@ -1,3 +1,3 @@ -
- +
+
\ No newline at end of file diff --git a/src/common/graphjs/geomon-timeseries-chart/geomon-timeseries-chart.component.ts b/src/common/graphjs/geomon-timeseries-chart/geomon-timeseries-chart.component.ts index 1d78751..681d4ce 100644 --- a/src/common/graphjs/geomon-timeseries-chart/geomon-timeseries-chart.component.ts +++ b/src/common/graphjs/geomon-timeseries-chart/geomon-timeseries-chart.component.ts @@ -1,61 +1,285 @@ -import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; +import { Input, Component, AfterViewInit, ViewChild, ElementRef, SimpleChanges } from '@angular/core'; // import * as d3 from 'd3'; -import { Chart, registerables } from 'chart.js'; +import { Chart, LogarithmicScale, registerables } from 'chart.js'; +import { GeomonTimeseries, DataConst, GeomonTimeseriesData } from '../../../shared/models/dataset'; +import { DatasetApiService } from '../../../app/services/dataset-api.service'; +import { DatasetService } from '../../../app/services/dataset.service'; + +import { Timespan } from '../../../shared/models/timespan'; +import { TimeService } from '../../core/time/time.service'; +import moment from 'moment'; +// import 'moment-duration-format'; +import { TimeValueTuple, Data } from '../../../shared/models/dataset'; +import 'chartjs-adapter-moment'; +import { MAT_SELECTION_LIST_VALUE_ACCESSOR } from '@angular/material/list'; @Component({ selector: 'geomon-timeseries-chart', templateUrl: './geomon-timeseries-chart.component.html', styleUrls: ['./geomon-timeseries-chart.component.scss'] }) -export class GeomonTimeseriesChartComponent implements OnInit { +export class GeomonTimeseriesChartComponent implements AfterViewInit { - @ViewChild('geomon_timeseries', { static: true }) - public d3Elem: ElementRef; + // @ViewChild('geomon_timeseries', { static: true }) + // public chartElem: ElementRef; + + + @ViewChild('geomon_timeseries') public chartElem: ElementRef; + + lineChart: Chart; + /** + * The corresponding dataset options. + */ + @Input() + public datasetOptions: Map; + + /** + * List of presented dataset ids. + */ + @Input() + public datasetIds: string[] = []; + + + /** + * The time interval in which the data should presented. + */ + @Input() + public timeInterval: Timespan; + + protected timespan: Timespan; + + // data types + protected datasetMap: Map = new Map(); + protected listOfUoms: string[] = []; + + // private loadingData: Set = new Set(); + + private width: number; + private canvas: HTMLCanvasElement; + private margin = { + top: 10, + right: 10, + bottom: 40, + left: 10 + }; + + constructor( + protected datasetApiService: DatasetApiService, + protected timeService: TimeService, + public datasetService: DatasetService, + ) { } + + ngAfterViewInit(): void { // this.createSvg(); + + this.canvas = document.getElementById("line-chart") as HTMLCanvasElement; - constructor() { } - ngOnInit(): void { - // this.createSvg(); // this.drawBars(this.data); Chart.register(...registerables); - this.drawBars(); + this.initChart(); + + for (let i = 0; i < this.datasetIds.length; i++) { + let datasetId = this.datasetIds[i]; + let dataset = this.datasetOptions.get(datasetId); + this.addDataset(dataset.id, 'https://geomon.geologie.ac.at/52n-sos-webapp/api/'); + } + + // let firstDatasetId = this.datasetIds[0]; + // let dataset = this.datasetOptions.get(firstDatasetId); + // this.addDataset(dataset.id, 'https://geomon.geologie.ac.at/52n-sos-webapp/api/'); + + } - private drawBars(): void { - let lineChart = document.getElementById("line-chart") as HTMLCanvasElement; - new Chart(lineChart, { + public ngOnChanges(changes: SimpleChanges): void { + if (changes.timeInterval && this.timeInterval) { + this.timespan = this.timeService.createTimespanOfInterval(this.timeInterval); + // this.timeIntervalChanges(); + } + // if (changes.reloadForDatasets && this.reloadForDatasets && this.reloadDataForDatasets.length > 0) { + // this.reloadDataForDatasets(this.reloadForDatasets); + // } + } + + public getDataset(internalId: string) { + return this.datasetMap.get(internalId); + } + + protected addDataset(id: string, url: string): void { + // this.servicesConnector.getDataset({ id, url }, { locale: this.translateService.currentLang, type: DatasetType.Timeseries }).subscribe( + // res => this.loadAddedDataset(res), + // error => this.errorHandler.handleDatasetLoadError(error) + // ); + this.datasetApiService.getDataset(id, url).subscribe({ + next: (res: GeomonTimeseries) => this.loadAddedDataset(res), + error: (err: any) => console.error('Observer got an error: ' + err), + complete: () => console.log('HTTP request completed.') + // error => this.errorHandler.handleDatasetLoadError(error) + }); + } + + private loadAddedDataset(dataset: GeomonTimeseries): void { + this.datasetMap.set(dataset.internalId, dataset); + this.loadDatasetData(dataset, false); + } + + // load data of dataset + private loadDatasetData(dataset: GeomonTimeseries, fprce: boolean): void { + // const datasetOptions = this.datasetOptions.get(dataset.internalId); + + // https://github.com/52North/helgoland-toolbox/blob/9ff9a42b34cd3deb181d56d76d48eba7c101554e/libs/core/src/lib/api-communication/connectors/dataset-api-v3-connector/dataset-api-v3-connector.ts#L270 + + const buffer = this.timeService.getBufferedTimespan(this.timespan, 5, moment.duration(1, 'day').asMilliseconds()); + + this.datasetApiService.getDatasetData(dataset, buffer, {}) + .subscribe({ + next: (result: GeomonTimeseriesData) => { + this.prepareData(dataset, result); + // this.onCompleteLoadingData(dataset); + let test = result; + }, + error: (error) => { + // this.errorHandler.handleDataLoadError(error, dataset); + // this.onCompleteLoadingData(dataset); + } + }); + } + + /** + * Function to prepare each dataset for the graph and adding it to an array of datasets. + * @param dataset {IDataset} Object of the whole dataset + */ + private prepareData(dataset: GeomonTimeseries, rawdata: GeomonTimeseriesData): void { + + if (rawdata instanceof GeomonTimeseriesData) { + // add surrounding entries to the set + if (rawdata.valueBeforeTimespan) { rawdata.values.unshift(rawdata.valueBeforeTimespan); } + if (rawdata.valueAfterTimespan) { rawdata.values.push(rawdata.valueAfterTimespan); } + + const data = this.generalizeData(rawdata, this.width, this.timespan); + + // this.datasetMap.get(dataset.internalId).data = data; + this.addData(this.lineChart, dataset, rawdata); + } + } + + public generalizeData(data: GeomonTimeseriesData, imageWidth: number, timespan: Timespan): Data { + if (data.values.length > imageWidth && data.values.length > 0) { + const duration = timespan.to - timespan.from; + const dataduration = data.values[data.values.length - 1][0] - data.values[0][0]; + const factor = duration / dataduration; + const realWidth = imageWidth / factor; + const modulo = 1 / (data.values.length / realWidth); + const generalizedData = { + values: data.values.filter((v, i) => i % Math.round(modulo) === 0), + referenceValues: data.referenceValues + }; + console.log(`reduce from ${data.values.length} to ${generalizedData.values.length}`); + return generalizedData; + } + return data; + } + + private addData(chart: Chart, dataset: GeomonTimeseries, data: GeomonTimeseriesData): void { + + let labels = data.values.map(function (label) { + let date = moment(label[0]).format("YYYY-MM-DD HH:mm"); + return date; + }); + + let values = data.values.map(function (value) { + return value[1]; + }); + + chart.data.labels = labels; + // chart.data.datasets.forEach((dataset) => { + // dataset.data.push(data); + // }); + let letters = '0123456789ABCDEF'.split(''); + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + var newDataset = { + label: dataset.label, + // backgroundColor: 'rgba(99, 255, 132, 0.2)', + backgroundColor: color, + // borderColor: 'rgba(99, 255, 132, 1)', + borderWidth: 1, + data: values, + } + // You add the newly created dataset to the list of `data` + chart.data.datasets.push(newDataset); + chart.update(); + this.width = this.calculateWidth(); + } + + private initChart(): void { + + this.lineChart = new Chart(this.canvas, { type: 'line', data: { - labels: [1500, 1600, 1700, 1750, 1800, 1850, 1900, 1950, 1999, 2050], - datasets: [{ - data: [86, 114, 106, 106, 107, 111, 133, 221, 783, 2478], - label: "Africa", - borderColor: "#3e95cd", - fill: false - }, { - data: [282, 350, 411, 502, 635, 809, 947, 1402, 3700, 5267], - label: "Asia", - borderColor: "#8e5ea2", - fill: false - }, { - data: [168, 170, 178, 190, 203, 276, 408, 547, 675, 734], - label: "Europe", - borderColor: "#3cba9f", - fill: false - }, { - data: [40, 20, 10, 16, 24, 38, 74, 167, 508, 784], - label: "Latin America", - borderColor: "#e8c3b9", - fill: false - }, { - data: [6, 3, 2, 2, 7, 26, 82, 172, 312, 433], - label: "North America", - borderColor: "#c45850", - fill: false - } + // labels: [1500, 1600, 1700, 1750, 1800, 1850, 1900, 1950, 1999, 2050], + labels: [], + datasets: [ + // { + // data: [86, 114, 106, 106, 107, 111, 133, 221, 783, 2478], + // label: "Africa", + // borderColor: "#3e95cd", + // fill: false + // }, { + // data: [282, 350, 411, 502, 635, 809, 947, 1402, 3700, 5267], + // label: "Asia", + // borderColor: "#8e5ea2", + // fill: false + // }, { + // data: [168, 170, 178, 190, 203, 276, 408, 547, 675, 734], + // label: "Europe", + // borderColor: "#3cba9f", + // fill: false + // }, { + // data: [40, 20, 10, 16, 24, 38, 74, 167, 508, 784], + // label: "Latin America", + // borderColor: "#e8c3b9", + // fill: false + // }, { + // data: [6, 3, 2, 2, 7, 26, 82, 172, 312, 433], + // label: "North America", + // borderColor: "#c45850", + // fill: false + // } ] }, + options: { + scales: { + y: { + suggestedMin: 0, // minimum will be 0, unless there is a lower value. + // OR // + beginAtZero: true // minimum value will be 0. + }, + + x: { + type: 'time', + + time: { + unit: 'minute', + displayFormats: { + minute: "DD/MM HH:mm", + hour: "DD/MM HH:mm", + day: "dd/MM", + week: "dd/MM", + month: "MMMM yyyy", + quarter: 'MMMM yyyy', + year: "yyyy", + } + }, + // ticks: { + // labelOffset: 10 + // } + } + }, + } // options: { // title: { // display: true, @@ -63,6 +287,7 @@ export class GeomonTimeseriesChartComponent implements OnInit { // } // } }); + this.width = this.calculateWidth() - 20; // add buffer to the left to garantee visualization of last date (tick x-axis) } /** @@ -80,4 +305,13 @@ export class GeomonTimeseriesChartComponent implements OnInit { .toString(16) .substring(1); } + + /** + * Function that returns the width of the graph diagram. + */ + private calculateWidth(): number { + return this.canvas.width - this.margin.left - this.margin.right; + } + + } \ No newline at end of file diff --git a/src/common/graphjs/graphjs.module.ts b/src/common/graphjs/graphjs.module.ts index 1845c97..2eb314b 100644 --- a/src/common/graphjs/graphjs.module.ts +++ b/src/common/graphjs/graphjs.module.ts @@ -4,7 +4,9 @@ import { NgModule } from '@angular/core'; import { GeomonTimeseriesChartComponent } from './geomon-timeseries-chart/geomon-timeseries-chart.component'; // import { ZoomControlComponent } from './zoom-control/zoom.component'; +import { DatasetApiService } from '../../app/services/dataset-api.service'; +import { TimeService } from '../core/time/time.service'; @NgModule({ @@ -19,7 +21,7 @@ import { GeomonTimeseriesChartComponent } from './geomon-timeseries-chart/geomon ], providers: [ - + DatasetApiService , TimeService ] }) export class GraphjsModule { } \ No newline at end of file diff --git a/src/shared/models/dataset.ts b/src/shared/models/dataset.ts index 500ffca..b11600b 100644 --- a/src/shared/models/dataset.ts +++ b/src/shared/models/dataset.ts @@ -93,4 +93,54 @@ export interface RenderingHints { properties: { color: string; }; +} + +export interface DataConst extends GeomonTimeseries { + data?: Data; +} + + + +// export class TimeseriesData { +// public id: string; +// public url: string; +// public data: FirstLastValue[]; +// } + +export interface IDataEntry { } + +export interface Data { + values: T[]; + referenceValues: ReferenceValues; + valueBeforeTimespan?: T; + valueAfterTimespan?: T; +} + +export class ReferenceValues { + [key: string]: { + values: T[]; + valueBeforeTimespan?: T; + valueAfterTimespan?: T; + }; +} + + + + +export type TimeValueTuple = [number, number]; +export interface GeomonData { } +export class GeomonTimeseriesData implements GeomonData { + + referenceValues: ReferenceValues = {}; + valueBeforeTimespan: TimeValueTuple; + valueAfterTimespan: TimeValueTuple; + + constructor( + public values: TimeValueTuple[], + ) { } +} + +export interface HelgolandDataFilter { + expanded?: boolean; + generalize?: boolean; } \ No newline at end of file diff --git a/src/shared/models/timespan.ts b/src/shared/models/timespan.ts new file mode 100644 index 0000000..3ac7159 --- /dev/null +++ b/src/shared/models/timespan.ts @@ -0,0 +1,30 @@ +export abstract class TimeInterval { + +} + +export class Timespan extends TimeInterval{ + + public from: number; + + public to: number; + + constructor(from: number | Date, to?: number | Date) { + super(); + this.from = from instanceof Date ? from.valueOf() : from; + this.to = to ? (to instanceof Date ? to.valueOf() : to) : this.from; + } +} + +export class BufferedTime extends TimeInterval { + public timestamp: Date; + public bufferInterval: number; + + constructor( + timestamp: Date, + bufferInterval: number + ) { + super(); + this.timestamp = timestamp; + this.bufferInterval = bufferInterval; + } +} \ No newline at end of file