From d469b73d0a1b65985d5f7b9458657a71a5ac9320 Mon Sep 17 00:00:00 2001 From: "mathias.chouet" <mathias.chouet@irstea.fr> Date: Thu, 12 Sep 2019 16:51:06 +0200 Subject: [PATCH] Fix #287 add jet type results --- src/app/app.module.ts | 2 + .../fixedvar-results.component.ts | 8 +- .../jet-results/jet-results.component.html | 2 +- .../jet-results/jet-results.component.ts | 45 ++- .../jet-trajectory-graph.component.html | 22 ++ .../jet-trajectory-graph.component.scss | 34 +++ .../jet-trajectory-graph.component.ts | 268 ++++++++++++++++++ src/app/services/formulaire.service.ts | 1 - src/locale/messages.en.json | 3 + src/locale/messages.fr.json | 3 + 10 files changed, 381 insertions(+), 7 deletions(-) create mode 100644 src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.html create mode 100644 src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.scss create mode 100644 src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 794becbda..7b1ed1fce 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -91,6 +91,7 @@ import { ModulesDiagramComponent } from "./components/modules-diagram/modules-di import { MacrorugoCompoundResultsTableComponent } from "./components/macrorugo-compound-results/macrorugo-compound-results-table.component"; import { MacrorugoCompoundResultsComponent } from "./components/macrorugo-compound-results/macrorugo-compound-results.component"; import { JetResultsComponent } from "./components/jet-results/jet-results.component"; +import { JetTrajectoryGraphComponent } from "./components/jet-trajectory-graph/jet-trajectory-graph.component"; import { DialogConfirmEmptySessionComponent } from "./components/dialog-confirm-empty-session/dialog-confirm-empty-session.component"; import { DialogConfirmCloseCalcComponent } from "./components/dialog-confirm-close-calc/dialog-confirm-close-calc.component"; @@ -195,6 +196,7 @@ const appRoutes: Routes = [ JalhydModelValidationMaxDirective, JalhydModelValidationStepDirective, JetResultsComponent, + JetTrajectoryGraphComponent, LogComponent, LogEntryComponent, ModulesDiagramComponent, diff --git a/src/app/components/fixedvar-results/fixedvar-results.component.ts b/src/app/components/fixedvar-results/fixedvar-results.component.ts index 394450131..211c44c80 100644 --- a/src/app/components/fixedvar-results/fixedvar-results.component.ts +++ b/src/app/components/fixedvar-results/fixedvar-results.component.ts @@ -22,13 +22,13 @@ export class FixedVarResultsComponent extends ResultsComponent implements DoChec /** * résultats non mis en forme */ - private _fixedResults: FixedResults; - private _varResults: VarResults; + protected _fixedResults: FixedResults; + protected _varResults: VarResults; /** * true si les résultats doiventt être remis à jour */ - private _doUpdate = false; + protected _doUpdate = false; @ViewChild(FixedResultsComponent, { static: false }) private fixedResultsComponent: FixedResultsComponent; @@ -116,7 +116,7 @@ export class FixedVarResultsComponent extends ResultsComponent implements DoChec * met à jour l'affichage des résultats * @returns true si les résultats ont pu être mis à jour */ - private updateResults() { + protected updateResults() { const fixedUpdated = this._fixedResults !== undefined && this.fixedResultsComponent !== undefined; if (fixedUpdated) { this.fixedResultsComponent.results = this._fixedResults; diff --git a/src/app/components/jet-results/jet-results.component.html b/src/app/components/jet-results/jet-results.component.html index ca214f920..5b726da97 100644 --- a/src/app/components/jet-results/jet-results.component.html +++ b/src/app/components/jet-results/jet-results.component.html @@ -4,7 +4,7 @@ <results-graph *ngIf="showVarResults"></results-graph> - <trajectory-graph></trajectory-graph> + <jet-trajectory-graph *ngIf="hasResults"></jet-trajectory-graph> <div> <!-- table des résultats fixés --> diff --git a/src/app/components/jet-results/jet-results.component.ts b/src/app/components/jet-results/jet-results.component.ts index 4bbf4a2d0..8703b78e7 100644 --- a/src/app/components/jet-results/jet-results.component.ts +++ b/src/app/components/jet-results/jet-results.component.ts @@ -1,6 +1,7 @@ -import { Component } from "@angular/core"; +import { Component, ViewChild } from "@angular/core"; import { FixedVarResultsComponent } from "../fixedvar-results/fixedvar-results.component"; +import { JetTrajectoryGraphComponent } from "../jet-trajectory-graph/jet-trajectory-graph.component"; @Component({ selector: "jet-results", @@ -11,4 +12,46 @@ import { FixedVarResultsComponent } from "../fixedvar-results/fixedvar-results.c }) export class JetResultsComponent extends FixedVarResultsComponent { + /** graphique de trajectoire */ + @ViewChild(JetTrajectoryGraphComponent, { static: false }) + private jetTrajectoryGraphComponent: JetTrajectoryGraphComponent; + + public get hasResults(): boolean { + return ( + (this._fixedResults !== undefined && this._fixedResults.hasResults) + || + (this._varResults !== undefined && this._varResults.hasResults) + ); + } + + public updateView() { + if (this.jetTrajectoryGraphComponent) { + this.jetTrajectoryGraphComponent.results = undefined; + } + super.updateView(); + } + + /** + * met à jour l'affichage des résultats + * @returns true si les résultats ont pu être mis à jour + */ + protected updateResults() { + const superUpdated = super.updateResults(); + + let trajectoryGraphUpdated: boolean; + trajectoryGraphUpdated = this.jetTrajectoryGraphComponent !== undefined; + + if (trajectoryGraphUpdated) { + // draw chart whether params are variating or not, + // hence different Results object for each case + if (this._varResults && this._varResults.hasResults) { + this.jetTrajectoryGraphComponent.results = this._varResults; + } else { + this.jetTrajectoryGraphComponent.results = this._fixedResults; + } + this.jetTrajectoryGraphComponent.updateView(); + } + + return superUpdated && trajectoryGraphUpdated; + } } diff --git a/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.html b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.html new file mode 100644 index 000000000..6ef01d504 --- /dev/null +++ b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.html @@ -0,0 +1,22 @@ +<div class="graph-results-container" #graphProfile fxLayout="row wrap" fxLayoutAlign="center center"> + <div fxFlex="1 1 100%"> + <div class="graph-profile-buttons"> + <button mat-icon-button (click)="resetZoom()" [disabled]="! zoomWasChanged" [title]="uitextResetZoomTitle"> + <mat-icon color="primary">replay</mat-icon> + </button> + <button mat-icon-button (click)="exportAsImage(graphProfile)" [title]="uitextExportImageTitle"> + <mat-icon color="primary">image</mat-icon> + </button> + <button mat-icon-button *ngIf="! isFullscreen" (click)="setFullscreen(graphProfile)" [title]="uitextEnterFSTitle"> + <mat-icon color="primary" class="scaled12">fullscreen</mat-icon> + </button> + <button mat-icon-button *ngIf="isFullscreen" (click)="exitFullscreen()" [title]="uitextExitFSTitle"> + <mat-icon color="primary" class="scaled12">fullscreen_exit</mat-icon> + </button> + </div> + + <div *ngIf="! displayChart" class="fake-chart"></div><!-- trick to avoid blinking effect due to forceRebuild --> + <chart *ngIf="displayChart" type="scatter" [data]="graph_data" [options]="graph_options" #graphChart> + </chart> + </div> +</div> \ No newline at end of file diff --git a/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.scss b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.scss new file mode 100644 index 000000000..19266623b --- /dev/null +++ b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.scss @@ -0,0 +1,34 @@ +.graph-results-container{ + display: block; + background-color: white; +} + +.graph-profile-buttons { + padding-right: 10px; + padding-top: 4px; + margin-bottom: -30px; + text-align: right; + background-color: white; + + button { + margin-left: 3px; + width: auto; + + mat-icon { + &.scaled12 { + transform: scale(1.2); + } + } + + &:disabled { + mat-icon { + color: #bfbfbf; + } + } + } +} + +.fake-chart { + width: 100%; + padding-top: 50%; +} diff --git a/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.ts b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.ts new file mode 100644 index 000000000..50a014d99 --- /dev/null +++ b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.ts @@ -0,0 +1,268 @@ +import { Component, ViewChild, ChangeDetectorRef } from "@angular/core"; + +import { ChartComponent } from "angular2-chartjs"; + +import { I18nService } from "../../services/internationalisation.service"; +import { ResultsComponent } from "../fixedvar-results/results.component"; +import { IYSeries } from "../../results/y-series"; +import { FixedResults } from "../../results/fixed-results"; +import { VarResults } from "../../results/var-results"; +import { fv } from "../../util"; + +import { Jet } from "jalhyd"; + +@Component({ + selector: "jet-trajectory-graph", + templateUrl: "./jet-trajectory-graph.component.html", + styleUrls: [ + "./jet-trajectory-graph.component.scss" + ] +}) +export class JetTrajectoryGraphComponent extends ResultsComponent { + + @ViewChild(ChartComponent, { static: false }) + private chartComponent; + + private _results: FixedResults | VarResults; + + private _zoomWasChanged = false; + + private _varValuesLists: any = {}; + + /** used to briefly destroy/rebuild the chart component, to refresh axis labels (@see bug #137) */ + public displayChart = true; + + /* + * config du graphe + */ + public graph_data: { datasets: any[] }; + public graph_options: any = { + responsive: true, + maintainAspectRatio: true, + aspectRatio: 1.5, + animation: { + duration: 0 + }, + legend: { + display: true, + position: "bottom", + reverse: false + }, + title: { + display: true, + text: this.intlService.localizeText("INFO_JET_TITRE_TRAJECTOIRE") + }, + elements: { + line: { + tension: 0 + } + } + }; + + public constructor( + private intlService: I18nService, + private cd: ChangeDetectorRef + ) { + super(); + // do not move following block out of constructor or scale labels won't be rendered + this.graph_options["scales"] = { + xAxes: [{ + type: "linear", + position: "bottom", + ticks: { + precision: ResultsComponent.CHARTS_AXIS_PRECISION + }, + scaleLabel: { + display: true, + labelString: this.intlService.localizeText("INFO_LIB_ABSCISSE") + } + }], + yAxes: [{ + type: "linear", + position: "left", + ticks: { + precision: ResultsComponent.CHARTS_AXIS_PRECISION + }, + scaleLabel: { + display: true, + labelString: this.intlService.localizeText("INFO_LIB_ALTITUDE") + } + }] + }; + // enable zoom and pan (using "chartjs-plugin-zoom" package) + const that = this; + this.graph_options["plugins"] = { + zoom: { + pan: { + enabled: false, // conflicts with drag zoom + mode: "xy", + }, + zoom: { + enabled: true, + drag: { // conflicts with pan; set to false to enable mouse wheel zoom, + borderColor: "rgba(225,225,225,0.3)", + borderWidth: 1, + backgroundColor: "rgba(0,0,0,0.25)" + }, + mode: "xy", + // percentage of zoom on a wheel event + // speed: 0.1, + onZoomComplete: function(t: any) { return function() { t.zoomComplete(); }; }(that) + } + } + }; + // format numbers in tooltips + this.graph_options.tooltips = { + displayColors: false, + callbacks: { + label: (tooltipItem, data) => { + return "(" + fv(Number(tooltipItem.xLabel)) + ", " + fv(Number(tooltipItem.yLabel)) + ")"; + } + } + }; + } + + /** forces Angular to rebuild the chart @see bug #137 */ + private forceRebuild() { + this.displayChart = false; + const that = this; + setTimeout(() => { // trick + that.displayChart = true; + }, 10); + } + + public set results(r: FixedResults | VarResults) { + this.forceRebuild(); // used for (de)activating legend in generateScatterGraph() + this._results = r; + + if (this._results) { + const nub = this._results.result.sourceNub as Jet; + const length = nub.variatingLength(); + // extract variable values list for legend + if (nub.resultHasMultipleValues()) { + for (const p of nub.parameterIterator) { + if (p.hasMultipleValues) { + this._varValuesLists[p.symbol] = []; + if (nub.calculatedParam === p) { // calculated + for (let i = 0; i < length; i++) { + this._varValuesLists[p.symbol].push(nub.result.resultElements[i].vCalc); + } + } else { // variating + const iter = p.getExtendedValuesIterator(length); + while (iter.hasNext) { + const nv = iter.next(); + this._varValuesLists[p.symbol].push(nv.value); + } + } + } + } + } + } + } + + public zoomComplete() { + this._zoomWasChanged = true; + this.cd.detectChanges(); + } + + public get zoomWasChanged(): boolean { + return this._zoomWasChanged; + } + + public updateView() { + this.generateScatterGraph(); + } + + /** + * génère les données d'un graphe de type "scatter" + */ + private generateScatterGraph() { + const ySeries = this.getYSeries(); + + // hide legend when there is only 1 series + this.graph_options.legend.display = (ySeries.length > 1); + + this.graph_data = { + datasets: [] + }; + + // build Y data series + for (const ys of ySeries) { + if (ys.data.length > 0) { + // push series config + this.graph_data.datasets.push({ + label: ys.label, + data: ys.data, + borderColor: ys.color, // couleur de la ligne + backgroundColor: "rgba(0,0,0,0)", // couleur de remplissage sous la courbe : transparent + showLine: "true" + }); + } + } + } + + public exportAsImage(element: HTMLDivElement) { + const canvas: HTMLCanvasElement = element.querySelector("canvas"); + canvas.toBlob((blob) => { + saveAs(blob, "chart.png"); + }); // defaults to image/png + } + + public resetZoom() { + this.chartComponent.chart.resetZoom(); + this._zoomWasChanged = false; + } + + public get uitextResetZoomTitle() { + return this.intlService.localizeText("INFO_GRAPH_BUTTON_TITLE_RESET_ZOOM"); + } + + public get uitextExportImageTitle() { + return this.intlService.localizeText("INFO_GRAPH_BUTTON_TITLE_EXPORT_IMAGE"); + } + + public get uitextEnterFSTitle() { + return this.intlService.localizeText("INFO_GRAPH_BUTTON_TITLE_ENTER_FS"); + } + + public get uitextExitFSTitle() { + return this.intlService.localizeText("INFO_GRAPH_BUTTON_TITLE_EXIT_FS"); + } + + private getYSeries(): IYSeries[] { + const ret: IYSeries[] = []; + const palette = ResultsComponent.distinctColors; + const nub = (this._results.result.sourceNub as Jet); + const trajectories = nub.generateTrajectories(); + + for (let i = 0; i < trajectories.length; i++) { + const traj = trajectories[i]; + ret.push({ + label: trajectories.length === 0 ? "" /* legend is hidden */ : this.getLegendForSeries(i), + color: palette[i % palette.length], + // map to IYSeries format + data: traj.map((t) => { + return { + x: t[0], + y: t[1] + }; + }) + }); + } + console.log("Y series", ret.length, ret); + return ret; + } + + /** + * Returns a label showing the boundary conditions values for + * the given iteration + * @param n index of the variating parameter(s) iteration + */ + private getLegendForSeries(n: number): string { + return Object.keys(this._varValuesLists).map((symbol) => { + const values = this._varValuesLists[symbol]; + const val = fv(values[n]); + return `${symbol} = ${val}`; + }).join(", "); + } +} diff --git a/src/app/services/formulaire.service.ts b/src/app/services/formulaire.service.ts index 5bfbb27c0..9dc427094 100644 --- a/src/app/services/formulaire.service.ts +++ b/src/app/services/formulaire.service.ts @@ -327,7 +327,6 @@ export class FormulaireService extends Observable { * @param calculatorName nom du module, à afficher dans l'interface */ public createFormulaire(ct: CalculatorType, nub?: Nub, calculatorName?: string): Promise<FormulaireDefinition> { - console.log(">> Create form !!", ct); // Crée un formulaire du bon type const f: FormulaireDefinition = this.newFormulaire(ct); this._formulaires.push(f); diff --git a/src/locale/messages.en.json b/src/locale/messages.en.json index 57ac348e1..94cc1aa88 100644 --- a/src/locale/messages.en.json +++ b/src/locale/messages.en.json @@ -156,8 +156,11 @@ "INFO_WALL_REMOVED": "Wall #%s removed", "INFO_WALLS_AND_DEVICES_REMOVED": "%s wall(s) and %s device(s) removed", "INFO_WALLS_REMOVED": "%s wall(s) removed", + "INFO_JET_TITRE_TRAJECTOIRE": "Trajectory", "INFO_LECHAPTCALMON_TITRE_COURT": "Lechapt-C.", "INFO_LECHAPTCALMON_TITRE": "Lechapt-Calmon", + "INFO_LIB_ABSCISSE": "Abscissa (m)", + "INFO_LIB_ALTITUDE": "Altitude (m)", "INFO_LIB_LENGTHS": "Every length", "INFO_LIB_WIDTHS": "Every width", "INFO_LIB_SLOPES": "Every slope", diff --git a/src/locale/messages.fr.json b/src/locale/messages.fr.json index 69ae2bef5..685070474 100644 --- a/src/locale/messages.fr.json +++ b/src/locale/messages.fr.json @@ -158,6 +158,9 @@ "INFO_WALLS_REMOVED": "%s cloison(s) supprimée(s)", "INFO_LECHAPTCALMON_TITRE_COURT": "Lechapt-C.", "INFO_LECHAPTCALMON_TITRE": "Lechapt-Calmon", + "INFO_JET_TITRE_TRAJECTOIRE": "Trajectoire", + "INFO_LIB_ABSCISSE": "Abscisse (m)", + "INFO_LIB_ALTITUDE": "Altitude (m)", "INFO_LIB_LENGTHS": "Toutes les longueurs", "INFO_LIB_WIDTHS": "Toutes les largeurs", "INFO_LIB_SLOPES": "Toutes les pentes", -- GitLab