import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; import { Inject, Component, OnInit } from "@angular/core"; import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { I18nService } from "../../services/internationalisation/internationalisation.service"; import { NgParameter } from "../../formulaire/ngparam"; import { ParamValueMode } from "jalhyd"; import { sprintf } from "sprintf-js"; import { ApplicationSetupService } from "../../services/app-setup/app-setup.service"; @Component({ selector: "dialog-edit-param-values", templateUrl: "dialog-edit-param-values.component.html", styleUrls: ["dialog-edit-param-values.component.scss"] }) export class DialogEditParamValuesComponent implements OnInit { /** the related parameter to change the "variable" value of */ public param: NgParameter; /** available value modes (min / max, list) */ public valueModes: { value: ParamValueMode; label: string; }[]; /** available decimal separators */ public decimalSeparators: { label: string; value: string; }[]; /** current decimal separator */ public decimalSeparator: string; public valuesListForm: FormGroup; /** when true, shows the values chart instead of the edit form */ public viewChart = false; // chart config public chartData = {}; public chartOptions; constructor( public dialogRef: MatDialogRef<DialogEditParamValuesComponent>, private intlService: I18nService, private appSetupService: ApplicationSetupService, private fb: FormBuilder, @Inject(MAT_DIALOG_DATA) public data: any ) { this.param = data.param; // an explicit ReactiveForm is required for file input component const initialValue = (this.param.valueMode === ParamValueMode.LISTE ? this.valuesList : ""); this.valuesListForm = this.fb.group({ file: [""], valuesList: [ initialValue, [ Validators.required // Validators.pattern(new RegExp(this.valuesListPattern)) // behaves weirdly ] ] }); // available options for select controls this.valueModes = [ { value: ParamValueMode.MINMAX, label: this.intlService.localizeText("INFO_PARAMMODE_MINMAX") }, { value: ParamValueMode.LISTE, label: this.intlService.localizeText("INFO_PARAMMODE_LIST") } ]; this.decimalSeparators = [ { label: this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_SEPARATEUR_POINT"), value: "." }, { label: this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_SEPARATEUR_VIRGULE"), value: "," } ]; this.decimalSeparator = this.decimalSeparators[0].value; // chart configuration const nDigits = this.appSetupService.displayDigits; this.chartOptions = { responsive: true, maintainAspectRatio: true, animation: { duration: 0 }, legend: { display: false }, scales: { xAxes: [{ type: "linear", position: "bottom", ticks: { precision: nDigits } }], yAxes: [{ type: "linear", position: "left", ticks: { precision: nDigits } }] }, tooltips: { callbacks: { label: function(tooltipItem) { return Number(tooltipItem.yLabel).toFixed(nDigits); } } } }; } // proxy to model values public get minValue() { return this.param.minValue; } public set minValue(v) { this.param.setMinValue(this, v); } public get maxValue() { return this.param.maxValue; } public set maxValue(v) { this.param.setMaxValue(this, v); } public get stepValue() { return this.param.stepValue; } public set stepValue(v) { this.param.setStepValue(this, v); } /** * regular expression pattern for values list validation (depends on decimal separator) */ public get valuesListPattern() { // standard pattern for decimal separator "." : ^-?([0-9]*\.)?([0-9]+[Ee]-?)?[0-9]+$ const escapedDecimalSeparator = (this.decimalSeparator === "." ? "\\." : this.decimalSeparator); const numberSubPattern = `-?([0-9]*${escapedDecimalSeparator})?([0-9]+[Ee]-?)?[0-9]+`; const re = `^${numberSubPattern}(${this.separatorPattern}${numberSubPattern})*$`; return re; } /** * accepted separator: everything but [numbers, E, +, -, decimal separator], any length */ public get separatorPattern() { return "[^0-9-+Ee" + this.decimalSeparator + "]+"; } public get selectedValueMode() { return this.param.valueMode; } public set selectedValueMode(v) { this.param.valueMode = v; } public get isMinMax() { return this.param.valueMode === ParamValueMode.MINMAX; } public get isListe() { return this.param.valueMode === ParamValueMode.LISTE; } /** * renders model's numbers list as text values list (semicolon separated) */ public get valuesList() { return (this.param.valueList || []).join(";"); } /** * injects text values list into model's numbers list */ public set valuesList(list: string) { const vals = []; const separatorRE = new RegExp(this.separatorPattern); const parts = list.trim().split(separatorRE); parts.forEach((e) => { if (e.length > 0) { // ensure decimal separator is "." for Number() if (this.decimalSeparator !== ".") { const re = new RegExp(this.decimalSeparator, "g"); // @TODO remove "g" ? e = e.replace(re, "."); } vals.push(Number(e)); } }); this.param.setValueList(this, vals); } public toggleViewChart() { // validate list values before switching views ? if (! this.viewChart && this.param.valueMode === ParamValueMode.LISTE) { if (this.onValidate(false)) { // toggle this.viewChart = ! this.viewChart; } } else { // toggle this.viewChart = ! this.viewChart; } // refresh chart when displaying it only if (this.viewChart) { this.drawChart(); } } public onValidate(close = true) { const status = this.validateValuesListString(this.valuesListForm.controls.valuesList.value); if (status.ok) { this.valuesListForm.controls.valuesList.setErrors(null); this.valuesList = this.valuesListForm.controls.valuesList.value; if (close) { this.dialogRef.close(); } return true; } else { this.valuesListForm.controls.valuesList.setErrors({ "model": status.message }); return false; } } /** * Returns { ok: true } if every element of list is a valid Number, { ok: false, message: "reason" } otherwise * @param list a string containing a list of numbers separated by this.separatorPattern */ private validateValuesListString(list: string) { let message: string; // 1. validate against general pattern let ok = new RegExp(this.valuesListPattern).test(list); if (ok) { // 2. validate each value const separatorRE = new RegExp(this.separatorPattern); const parts = list.trim().split(separatorRE); for (let i = 0; i < parts.length && ok; i++) { let e = parts[i]; if (e.length > 0) { // should always be true as separator might be several characters long // ensure decimal separator is "." for Number() if (this.decimalSeparator !== ".") { const re = new RegExp(this.decimalSeparator, "g"); // @TODO remove "g" ? e = e.replace(re, "."); } // 2.1 check it is a valid Number const n = (Number(e)); // 2.2 validate against model let modelIsHappy = true; try { this.param.checkValue(n); } catch (e) { modelIsHappy = false; message = sprintf(this.intlService.localizeText("ERROR_INVALID_AT_POSITION"), i + 1) + " " + this.intlService.localizeMessage(e); } // synthesis ok = ( ok && !isNaN(n) && isFinite(n) && modelIsHappy ); } } } else { message = this.uitextMustBeListOfNumbers; } return { ok, message }; } public onFileSelected(event: any) { if (event.target.files && event.target.files.length) { const fr = new FileReader(); fr.onload = () => { this.valuesListForm.controls.valuesList.setErrors(null); // this.valuesList = String(fr.result); this.valuesListForm.controls.valuesList.setValue(String(fr.result)); }; fr.onerror = () => { fr.abort(); throw new Error("Erreur de lecture du fichier"); }; fr.readAsText(event.target.files[0]); } } public onValueModeChange(event) { this.initVariableValues(); } private initVariableValues() { // init min / max / step if (this.isMinMax) { if (this.param.minValue === undefined) { this.param.setMinValue(this, this.param.getValue() / 2); } if (this.param.maxValue === undefined) { this.param.setMaxValue(this, this.param.getValue() * 2); } let step = this.param.stepValue; if (step === undefined) { step = (this.param.maxValue - this.param.minValue) / 20; } this.param.setStepValue(this, step); } // init values list if (this.isListe) { if (this.param.valueList === undefined) { if (this.param.isDefined) { this.param.setValueList(this, [ this.param.getValue() ]); } else { this.param.setValueList(this, []); } // set form control initial value this.valuesListForm.controls.valuesList.setValue(this.valuesList); } } } /** * (re)Génère le graphique d'évolution des valeurs */ private drawChart() { const data = []; let i = 0; for (const v of this.param.valuesIterator) { data.push({ x: i, y: v }); i++; } this.chartData = { datasets: [{ label: "", data: data, borderColor: "#808080", // couleur de la ligne backgroundColor: "rgba(0,0,0,0)", // couleur de remplissage sous la courbe : transparent showLine: "true" }] }; } public get uiTextModeSelection() { return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_MODE"); } public get uitextValeurMini() { return this.intlService.localizeText("INFO_PARAMFIELD_VALEURMINI"); } public get uitextValeurMaxi() { return this.intlService.localizeText("INFO_PARAMFIELD_VALEURMAXI"); } public get uitextPasVariation() { return this.intlService.localizeText("INFO_PARAMFIELD_PASVARIATION"); } public get uitextClose() { return this.intlService.localizeText("INFO_OPTION_CLOSE"); } public get uitextCancel() { return this.intlService.localizeText("INFO_OPTION_CANCEL"); } public get uitextValidate() { return this.intlService.localizeText("INFO_OPTION_VALIDATE"); } public get uitextEditParamVariableValues() { return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_TITLE"); } public get uitextListeValeurs() { return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_VALUES_FORMAT"); } public get uitextMustBeANumber(): string { return this.intlService.localizeText("ERROR_PARAM_MUST_BE_A_NUMBER"); } public get uitextMustBeListOfNumbers() { return sprintf(this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_VALUES_FORMAT_ERROR"), this.separatorPattern); } public get uitextDecimalSeparator() { return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_SEPARATEUR_DECIMAL"); } public get uitextImportFile() { return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_IMPORT_FICHIER"); } public ngOnInit() { this.initVariableValues(); } }