param-definition.ts 39.8 KB
Newer Older
Mathias Chouet's avatar
Fix #67    
Mathias Chouet committed
1
import { CalculatorType, ComputeNode } from "../compute-node";
2
import { Session } from "../index";
3
import { LinkedValue } from "../linked-value";
4
import { Nub } from "../nub";
Mathias Chouet's avatar
Mathias Chouet committed
5
import { acSection } from "../section/section_type";
Mathias Chouet's avatar
Mathias Chouet committed
6
import { Interval } from "../util/interval";
7
import { Message, MessageCode } from "../util/message";
Mathias Chouet's avatar
Mathias Chouet committed
8
import { IObservable, Observable, Observer } from "../util/observer";
9
import { ParamDomain, ParamDomainValue } from "./param-domain";
Mathias Chouet's avatar
Mathias Chouet committed
10
import { INamedIterableValues, INumberIterator } from "./param-value-iterator";
11
import { ParamValueMode } from "./param-value-mode";
Mathias Chouet's avatar
Mathias Chouet committed
12
import { ParamValues } from "./param-values";
13
import { ParamsEquation } from "./params-equation";
14
15

/**
Mathias Chouet's avatar
Mathias Chouet committed
16
 * Calculabilité du paramètre
17
18
 */
export enum ParamCalculability {
19
20
21
    /** parameter may have any value, may not vary, may not be calculated */
    FIXED,
    /** parameter may have any value, may vary, may not be calculated */
22
    FREE,
23
    /** parameter may have any value, may vary, may be calculated using Newton method */
24
    EQUATION,
25
    /** parameter may have any value, may vary, may be calculated using dichotomy */
26
27
28
29
    DICHO
}

/**
30
31
32
 * Parameter family: defines linkability with other parameters/results
 */
export enum ParamFamily {
Mathias Chouet's avatar
Mathias Chouet committed
33
    ANY, // peut être lié à n'importe quel paramètre
34
35
36
    LENGTHS, // longueur
    WIDTHS, // largeur
    SLOPES, // pente
Mathias Chouet's avatar
Fix #91    
Mathias Chouet committed
37
38
39
    HEIGHTS, // profondeur, tirant d'eau
    BASINFALLS, // chute entre bassins
    TOTALFALLS, // chute totale
40
41
42
43
    ELEVATIONS, // cote
    VOLUMES,
    FLOWS, // débit
    DIAMETERS,
44
    SPEEDS // vitesses, seulement des résultats
45
46
}

47
48
49
50
51
52
53
54
55
56
57
58
59
/**
 * Strategy to apply when multiple parameters are variating, and
 * values series have to be extended for sizes to match
 */
export enum ExtensionStrategy {
    REPEAT_LAST,    // repeat last value as many times as needed
    RECYCLE         // repeat the whole series from the beginning
    // autres propositions :
    // PAD_LEFT     // pad with zeroes at the beginning ?
    // PAD_RIGHT    // pad with zeroes at the end ?
    // INTERPOLATE  // insert regular steps between first and last value
}

60
61
62
/**
 * Paramètre avec symbole, famille, domaine de définition, calculabilité,
 * pointant éventuellement vers un autre paramètre / résultat
63
 */
64
export class ParamDefinition implements INamedIterableValues, IObservable {
Mathias Chouet's avatar
Mathias Chouet committed
65

66
67
68
    /** le paramètre doit-il être exposé (par ex: affiché par l'interface graphique) ? */
    public visible: boolean = true;

69
70
71
    /** sandbox value used during calculation */
    public v: number;

72
    /** extension strategy, when multiple parameters vary */
73
    private _extensionStrategy: ExtensionStrategy;
74

75
76
77
    /** mode de génération des valeurs : min/max, liste, ... */
    private _valueMode: ParamValueMode;

Mathias Chouet's avatar
Mathias Chouet committed
78
79
80
    /** symbole */
    private _symbol: string;

81
82
83
    /** unité */
    private _unit: string;

84
85
    /** related parameters groups; indirectly gives access to the Nub using this parameter */
    private _parent: ParamsEquation;
86

Mathias Chouet's avatar
Mathias Chouet committed
87
88
89
90
    /** domaine de définition */
    private _domain: ParamDomain;

    /** calculabilité */
91
92
    private _calc: ParamCalculability;

Mathias Chouet's avatar
Mathias Chouet committed
93
94
95
    /** valeur(s) prise(s) */
    private _paramValues: ParamValues;

96
97
98
99
100
101
    /** parameters family, for linking */
    private _family: ParamFamily;

    /** pointer to another Parameter / Result, in case of LINK mode */
    private _referencedValue: LinkedValue;

Mathias Chouet's avatar
Mathias Chouet committed
102
103
104
    /** implémentation par délégation de IObservable */
    private _observable: Observable;

105
    constructor(parent: ParamsEquation, symb: string, d: ParamDomain | ParamDomainValue, unit?: string,
106
                val?: number, family?: ParamFamily, visible: boolean = true
107
    ) {
108
        this._parent = parent;
Mathias Chouet's avatar
Mathias Chouet committed
109
        this._symbol = symb;
110
        this._unit = unit;
Mathias Chouet's avatar
Mathias Chouet committed
111
112
        this._observable = new Observable();
        this._paramValues = new ParamValues();
113

114
        // set single value and copy it to sandbox value
Mathias Chouet's avatar
Mathias Chouet committed
115
        this._paramValues.singleValue = val;
116
117
        this.v = val;

118
        this._calc = ParamCalculability.FREE;
119
        this._family = family;
120
        this.visible = visible;
121
        this.valueMode = ParamValueMode.SINGLE;
122
        this.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
Mathias Chouet's avatar
Mathias Chouet committed
123

124
        this.setDomain(d);
Mathias Chouet's avatar
Mathias Chouet committed
125

126
        this.checkValueAgainstDomain(val);
Mathias Chouet's avatar
Mathias Chouet committed
127
128
    }

129
130
131
    /**
     * set parent a-posteriori; used by nghyd when populating forms
     */
132
    public set parent(parent: ParamsEquation) {
133
134
135
        this._parent = parent;
    }

136
137
138
139
140
    /**
     * identifiant unique de la forme
     *   (uid du JalhydObject (en général un Nub) + "_" + symbole du paramètre)
     * ex: 123_Q
     */
141
    public get uid(): string {
142
143
144
        return this.nubUid + "_" + this._symbol;
    }

145
146
147
148
    public get unit(): string {
        return this._unit;
    }

149
150
151
152
    public setUnit(u: string) {
        this._unit = u;
    }

153
154
    /**
     * pointer to the ComputeNode (usually Nub) that uses the ParamsEquation that
Mathias Chouet's avatar
Mathias Chouet committed
155
156
     * uses this ParamDefinition; for Section params, this means the Nub enclosing
     * the Section
157
158
     */
    public get parentComputeNode(): ComputeNode {
Mathias Chouet's avatar
Fix #67    
Mathias Chouet committed
159
160
161
162
        let parentCN: ComputeNode;
        if (this._parent) {
            // ComputeNode utilisant le ParamsEquation
            parentCN = this._parent.parent;
Mathias Chouet's avatar
Mathias Chouet committed
163
164
            // Section: go up to enclosing Nub if any (might not have any yet when unserializing sessions)
            if (parentCN instanceof acSection && parentCN.parent) {
Mathias Chouet's avatar
Mathias Chouet committed
165
166
                parentCN = parentCN.parent;
            }
Mathias Chouet's avatar
Fix #67    
Mathias Chouet committed
167
        } else {
Mathias Chouet's avatar
Mathias Chouet committed
168
169
            // fail silently (the story of my life)
            // throw new Error("ParamDefinition.parentComputeNode : parameter has no parent !");
Mathias Chouet's avatar
Fix #67    
Mathias Chouet committed
170
171
172
173
        }
        return parentCN;
    }

Mathias Chouet's avatar
Mathias Chouet committed
174
175
176
177
178
179
180
181
182
183
184
185
    /**
     * Returns the parent compute node as Nub if it is a Nub; returns
     * undefined otherwise
     */
    public get parentNub(): Nub {
        if (this.parentComputeNode && this.parentComputeNode instanceof Nub) {
            return this.parentComputeNode as Nub;
        } else {
            return undefined;
        }
    }

186
187
188
    /**
     * Identifiant unique du Nub parent
     */
189
190
    public get nubUid(): string {
        return this.parentComputeNode.uid;
191
192
193
    }

    /**
194
     * Type de module du Nub parent
195
     */
196
    public get nubCalcType(): CalculatorType {
197
        let parentCalcType: CalculatorType;
198
199
        if (this.parentComputeNode instanceof Nub) {
            parentCalcType = this.parentComputeNode.calcType;
200
201
        } else {
            throw new Error("ParamDefinition.nubCalcType : parameter has no parent !");
202
        }
203
        return parentCalcType;
204
205
    }

206
    public get symbol(): string {
Mathias Chouet's avatar
Mathias Chouet committed
207
208
209
        return this._symbol;
    }

210
211
212
213
214
215
216
217
    public setDomain(d: any) {
        if (d instanceof ParamDomain) {
            this._domain = d;
        } else {
            this._domain = new ParamDomain(d as ParamDomainValue);
        }
    }

218
    public get domain(): ParamDomain {
Mathias Chouet's avatar
Mathias Chouet committed
219
220
221
222
223
224
225
        return this._domain;
    }

    public get interval(): Interval {
        return this._domain.interval;
    }

226
227
228
229
230
231
232
    public get extensionStrategy(): ExtensionStrategy {
        return this._extensionStrategy;
    }

    public set extensionStrategy(strategy: ExtensionStrategy) {
        this._extensionStrategy = strategy;
        // synchronise with underlying local ParamValues (for iterator), except for links
Dorchies David's avatar
Dorchies David committed
233
        if ([ParamValueMode.SINGLE, ParamValueMode.MINMAX, ParamValueMode.LISTE].includes(this.valueMode)) {
234
235
236
237
238
239
            if (this._paramValues) {
                this._paramValues.extensionStrategy = strategy;
            }
        }
    }

240
241
242
243
    public get valueMode(): ParamValueMode {
        return this._valueMode;
    }

Mathias Chouet's avatar
Mathias Chouet committed
244
245
246
247
248
249
250
251
252
253
    /**
     * Easy setter that propagates the change to other parameters
     * (default behaviour); use setValueMode(..., false) to prevent
     * possible infinite loops
     */
    public set valueMode(newMode: ParamValueMode) {
        this.setValueMode(newMode);
    }

    /**
254
255
     * Sets the value mode and asks the Nub to ensure there is only one parameter
     * in CALC mode
Mathias Chouet's avatar
Mathias Chouet committed
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
     *
     * If propagateToCalculatedParam is true, any param that goes from CALC mode
     * to any other mode will trigger a reset of the default calculated param at
     * Nub level
     *
     * Propagates modes other than CALC and LINK to the underlying
     * ParamValues (local instance)
     */
    public setValueMode(newMode: ParamValueMode, propagateToCalculatedParam: boolean = true) {
        const oldMode = this.valueMode;

        // ignore idempotent calls
        if (oldMode === newMode) {
            return;
        }

272
273
274
275
276
277
        if (oldMode === ParamValueMode.CALCUL) {
            if (propagateToCalculatedParam) {
                // Set default calculated parameter, only if previous CALC param was
                // manually set to something else than CALC
                if (this.parentComputeNode && this.parentComputeNode instanceof Nub) {
                    this.parentComputeNode.resetDefaultCalculatedParam(this);
Mathias Chouet's avatar
Mathias Chouet committed
278
                }
279
280
281
282
283
284
            }
        } else if (newMode === ParamValueMode.CALCUL) {
            // set old CALC param to SINGLE mode
            if (this.parentComputeNode && this.parentComputeNode instanceof Nub) {
                this.parentComputeNode.unsetCalculatedParam(this);
            }
Mathias Chouet's avatar
Mathias Chouet committed
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
        }

        // set new mode
        this._valueMode = newMode;

        // synchronise with underlying local ParamValues (for iterator)
        if (
            newMode === ParamValueMode.SINGLE
            || newMode === ParamValueMode.MINMAX
            || newMode === ParamValueMode.LISTE
        ) {
            if (this._paramValues) {
                this._paramValues.valueMode = newMode;
            }
        }
    }

    /**
     * Returns true if this parameter is the calculatedParam of
     * its parent Nub
     */
    public get isCalculated(): boolean {
307
308
        if (this.parentNub) {
            return (this.parentNub.calculatedParam === this); // should be the same object
Mathias Chouet's avatar
Mathias Chouet committed
309
310
311
312
313
314
315
316
        }
        return false;
    }

    /**
     * Sets this parameter as the one to be calculated
     */
    public setCalculated() {
317
318
        if (this.parentNub) {
            this.parentNub.calculatedParam = this;
319
320
321
        }
    }

322
    /**
323
324
     * Current values set associated to this parameter; in LINK mode, points
     * to a remote set of values.
325
     */
Mathias Chouet's avatar
Mathias Chouet committed
326
    public get paramValues(): ParamValues {
Mathias Chouet's avatar
Mathias Chouet committed
327
        if (this.valueMode === ParamValueMode.LINK && this.isReferenceDefined()) {
328
            return this._referencedValue.getParamValues();
Mathias Chouet's avatar
Mathias Chouet committed
329
330
        } else {
            return this._paramValues;
Mathias Chouet's avatar
Mathias Chouet committed
331
        }
332
333
    }

334
335
336
337
338
339
340
341
    public get referencedValue() {
        return this._referencedValue;
    }

    public get family() {
        return this._family;
    }

Mathias Chouet's avatar
Mathias Chouet committed
342
343
344
345
    public undefineFamily() {
        this._family = undefined;
    }

Mathias Chouet's avatar
Mathias Chouet committed
346
347
348
349
350
351
352
353
354
355
356
357
358
359
    get calculability(): ParamCalculability {
        if (this._calc === undefined) {
            const e = new Message(MessageCode.ERROR_PARAMDEF_CALC_UNDEFINED);
            e.extraVar.symbol = this.symbol;
            throw e;
        }

        return this._calc;
    }

    set calculability(c: ParamCalculability) {
        this._calc = c;
    }

360
    /**
361
     * Returns true if current value (not singleValue !) is defined
362
     */
363
364
    public get hasCurrentValue(): boolean {
        return (this.v !== undefined);
365
366
    }

367
    /**
368
369
     * Returns true if held value (not currentValue !) is defined,
     * depending on the value mode
370
     */
Mathias Chouet's avatar
Mathias Chouet committed
371
    public get isDefined(): boolean {
372
373
374
375
376
377
378
379
380
381
382
383
384
        let defined = false;
        switch (this.valueMode) {
            case ParamValueMode.SINGLE:
                defined = (this.singleValue !== undefined);
                break;
            case ParamValueMode.MINMAX:
                defined = (
                    this.paramValues.min !== undefined
                    && this.paramValues.max !== undefined
                    && this.paramValues.step !== undefined
                );
                break;
            case ParamValueMode.LISTE:
Mathias Chouet's avatar
Mathias Chouet committed
385
                defined = (this.valueList && this.valueList.length > 0 && this.valueList[0] !== undefined);
386
387
                break;
            case ParamValueMode.CALCUL:
388
                if (this.parentNub && this.parentNub.result && this.parentNub.result.resultElements.length > 0) {
389
390
391
392
393
394
                    const res = this.parentNub.result;
                    defined = (res.vCalc !== undefined);
                }
                break;
            case ParamValueMode.LINK:
                defined = this.referencedValue.isDefined();
Mathias Chouet's avatar
Mathias Chouet committed
395
        }
396
        return defined;
397
398
    }

Mathias Chouet's avatar
Mathias Chouet committed
399
    // -- values getters / setters; in LINK mode, reads / writes from / to the target values
400
401

    public get singleValue(): number {
402
        this.checkValueMode([ParamValueMode.SINGLE, ParamValueMode.CALCUL, ParamValueMode.LINK]);
403
404
405
        return this.paramValues.singleValue;
    }

Mathias Chouet's avatar
Mathias Chouet committed
406
    public set singleValue(v: number) {
407
        this.checkValueMode([ParamValueMode.SINGLE, ParamValueMode.CALCUL, ParamValueMode.LINK]);
Mathias Chouet's avatar
Mathias Chouet committed
408
409
        this.paramValues.singleValue = v;
        this.notifyValueModified(this);
410
411
412
    }

    public get min() {
413
        this.checkValueMode([ParamValueMode.MINMAX, ParamValueMode.LINK]);
414
415
416
417
        return this.paramValues.min;
    }

    public set min(v: number) {
418
        this.checkValueMode([ParamValueMode.MINMAX]);
Mathias Chouet's avatar
Mathias Chouet committed
419
        this.paramValues.min = v;
420
421
422
    }

    public get max() {
423
        this.checkValueMode([ParamValueMode.MINMAX, ParamValueMode.LINK]);
424
425
426
427
        return this.paramValues.max;
    }

    public set max(v: number) {
428
        this.checkValueMode([ParamValueMode.MINMAX]);
Mathias Chouet's avatar
Mathias Chouet committed
429
        this.paramValues.max = v;
430
431
432
    }

    public get step() {
433
        this.checkValueMode([ParamValueMode.MINMAX, ParamValueMode.LINK]);
434
435
436
437
        return this.paramValues.step;
    }

    public set step(v: number) {
438
        this.checkValueMode([ParamValueMode.MINMAX]);
Mathias Chouet's avatar
Mathias Chouet committed
439
        this.paramValues.step = v;
440
441
442
443
444
445
    }

    /**
     * Generates a reference step value, given the current (local) values for min / max
     */
    public get stepRefValue(): Interval {
446
        this.checkValueMode([ParamValueMode.MINMAX]);
447
448
449
450
        return new Interval(1e-9, this._paramValues.max - this._paramValues.min);
    }

    public get valueList() {
451
        this.checkValueMode([ParamValueMode.LISTE, ParamValueMode.LINK]);
452
453
454
455
        return this.paramValues.valueList;
    }

    public set valueList(l: number[]) {
456
        this.checkValueMode([ParamValueMode.LISTE]);
Mathias Chouet's avatar
Mathias Chouet committed
457
        this.paramValues.valueList = l;
458
459
    }

460
461
462
463
    public count() {
        return this.paramValues.valuesIterator.count();
    }

464
465
466
    // for INumberiterator interface
    public get currentValue(): number {
        // magically follows links
Mathias Chouet's avatar
Mathias Chouet committed
467
        return this.paramValues.currentValue;
468
469
    }

470
471
472
473
474
    /**
     * Get single value
     */
    public getValue(): number {
        return this.paramValues.singleValue;
Mathias Chouet's avatar
Mathias Chouet committed
475
476
    }

477
478
479
480
481
482
    /**
     * Returns values as a number list, for LISTE and MINMAX modes;
     * in MINMAX mode, infers the list from min/max/step values
     */
    public getInferredValuesList() {
        this.checkValueMode([ParamValueMode.LISTE, ParamValueMode.MINMAX, ParamValueMode.LINK]);
Mathias Chouet's avatar
Mathias Chouet committed
483
        return this.paramValues.getInferredValuesList(false, undefined, true);
484
485
    }

486
    /**
Mathias Chouet's avatar
Mathias Chouet committed
487
     * Magic method to define current values into Paramvalues set; defines the mode
488
     * accordingly by detecting values nature
489
     */
490
    public setValues(o: number | any, max?: number, step?: number) {
Mathias Chouet's avatar
Mathias Chouet committed
491
        this.paramValues.setValues(o, max, step);
492
493
494
495
496
497
498
499
500
        if (typeof (o) === "number") {
            if (max === undefined) {
                this.valueMode = ParamValueMode.SINGLE;
            } else {
                this.valueMode = ParamValueMode.MINMAX;
            }
        } else if (Array.isArray(o)) {
            this.valueMode = ParamValueMode.LISTE;
        } else {
Mathias Chouet's avatar
Mathias Chouet committed
501
            throw new Error(`ParamValues.setValues() : invalid call`);
502
        }
503
504
505
506
507
508
509
510
    }

    /**
     * Sets the current value of the parameter's own values set, then
     * notifies all observers
     */
    public setValue(val: number, sender?: any) {
        this.checkValueAgainstDomain(val);
511
        this.paramValues.singleValue = val;
Mathias Chouet's avatar
Mathias Chouet committed
512
        // this.notifyValueModified(sender); // @TODO why ? currentValue is only used temporarily for calculation
Mathias Chouet's avatar
Mathias Chouet committed
513
514
    }

515
516
517
518
    /**
     * Checks that the current values set are compatible with the value mode
     */
    public checkValueAgainstMode() {
Mathias Chouet's avatar
Mathias Chouet committed
519
        this.paramValues.check();
520
521
    }

522
523
524
525
    /**
     * Validates the given numeric value against the definition domain; throws
     * an error if value is outside the domain
     */
526
    public checkValueAgainstDomain(v: number) {
Mathias Chouet's avatar
Mathias Chouet committed
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
        const sDomain = ParamDomainValue[this._domain.domain];

        switch (this._domain.domain) {
            case ParamDomainValue.ANY:
                break;

            case ParamDomainValue.POS:
                if (v <= 0) {
                    const f = new Message(MessageCode.ERROR_PARAMDEF_VALUE_POS);
                    f.extraVar.symbol = this.symbol;
                    f.extraVar.value = v;
                    throw f;
                }
                break;

            case ParamDomainValue.POS_NULL:
                if (v < 0) {
                    const f = new Message(MessageCode.ERROR_PARAMDEF_VALUE_POSNULL);
                    f.extraVar.symbol = this.symbol;
                    f.extraVar.value = v;
                    throw f;
                }
                break;

            case ParamDomainValue.NOT_NULL:
                if (v === 0) {
                    const f = new Message(MessageCode.ERROR_PARAMDEF_VALUE_NULL);
                    f.extraVar.symbol = this.symbol;
                    throw f;
                }
                break;

            case ParamDomainValue.INTERVAL:
                const min = this._domain.minValue;
                const max = this._domain.maxValue;
                if (v < min || v > max) {
                    const f = new Message(MessageCode.ERROR_PARAMDEF_VALUE_INTERVAL);
                    f.extraVar.symbol = this.symbol;
                    f.extraVar.value = v;
                    f.extraVar.minValue = min;
                    f.extraVar.maxValue = max;
                    throw f;
                }
                break;

            default:
                const e = new Message(MessageCode.ERROR_PARAMDOMAIN_INVALID);
                e.extraVar.symbol = this.symbol;
                e.extraVar.domain = sDomain;
                throw e;
        }
    }

    public checkMin(min: number): boolean {
581
        return this.isMinMaxDomainValid(min) && (min < this.max);
Mathias Chouet's avatar
Mathias Chouet committed
582
583
584
    }

    public checkMax(max: number): boolean {
585
        return this.isMinMaxDomainValid(max) && (this.min < max);
Mathias Chouet's avatar
Mathias Chouet committed
586
587
    }

588
    // includes checking min/max validity @TODO rename ?
Mathias Chouet's avatar
Mathias Chouet committed
589
    public checkStep(step: number): boolean {
590
        return this.isMinMaxValid && this.stepRefValue.intervalHasValue(step);
Mathias Chouet's avatar
Mathias Chouet committed
591
592
    }

593
    /**
594
     * Return true if single value is valid regarding the domain constraints
595
     */
Mathias Chouet's avatar
Mathias Chouet committed
596
597
    get isValueValid(): boolean {
        try {
598
            const v = this.paramValues.singleValue;
599
            this.checkValueAgainstDomain(v);
Mathias Chouet's avatar
Mathias Chouet committed
600
601
602
603
604
605
606
            return true;
        } catch (e) {
            return false;
        }
    }

    get isMinMaxValid(): boolean {
607
        return this.checkMinMax(this.min, this.max);
Mathias Chouet's avatar
Mathias Chouet committed
608
609
    }

610
611
612
    /**
     * Return true if current value is valid regarding the range constraints : min / max / step
     */
Mathias Chouet's avatar
Mathias Chouet committed
613
    public get isRangeValid(): boolean {
614
        switch (this._valueMode) {
Mathias Chouet's avatar
Mathias Chouet committed
615
616
617
618
            case ParamValueMode.LISTE:
                return this.isListValid;

            case ParamValueMode.MINMAX:
619
                // includes checking min/max
620
                return this.checkStep(this.step);
Mathias Chouet's avatar
Mathias Chouet committed
621
622
        }

623
        throw new Error(`ParamDefinition.isRangeValid() : valeur ${ParamValueMode[this._valueMode]}`
Dorchies David's avatar
Dorchies David committed
624
            + `de ParamValueMode non prise en compte`);
Mathias Chouet's avatar
Mathias Chouet committed
625
626
    }

627
628
629
630
631
632
633
634
635
636
637
638
639
640
    /**
     * Root method to determine if a field value is valid, regarding the model constraints.
     *
     * Does not take care of input validation (ie: valid number, not empty...) because an
     * invalid input should never be set as a value; input validation is up to the GUI.
     *
     * In LINK mode :
     *  - if target is a parameter, checks validity of the target value against the local model constraints
     *  - if target is a result :
     *    - is result is already computed, checks validity of the target result against the local model constraints
     *    - if it is not, checks the "computability" of the target Nub (ie. validity of the Nub) to allow
     *      triggering chain computation
     */
    public get isValid(): boolean {
641
        switch (this._valueMode) {
Mathias Chouet's avatar
Mathias Chouet committed
642
643
644
645
646
647
648
649
650
651
652
            case ParamValueMode.SINGLE:
                return this.isValueValid;

            case ParamValueMode.MINMAX:
            case ParamValueMode.LISTE:
                return this.isRangeValid;

            case ParamValueMode.CALCUL:
                return true;

            case ParamValueMode.LINK:
Dorchies David's avatar
Dorchies David committed
653
                if (!this.isReferenceDefined()) {
Mathias Chouet's avatar
Mathias Chouet committed
654
655
                    return false;
                }
656
657
658
659
660
                // valuesIterator covers both target param and target result cases
                const iterator = this.valuesIterator;
                if (iterator) {
                    try {
                        for (const v of iterator) {
661
                            this.checkValueAgainstDomain(v);
662
663
664
665
                        }
                        return true;
                    } catch (e) {
                        return false;
Mathias Chouet's avatar
Mathias Chouet committed
666
                    }
667
668
669
                } else { // undefined iterator means target results are not computed yet
                    // check target Nub computability
                    return this.referencedValue.nub.isComputable();
Mathias Chouet's avatar
Mathias Chouet committed
670
671
672
673
                }
        }

        throw new Error(
Mathias Chouet's avatar
Mathias Chouet committed
674
            `ParamDefinition.isValid() : valeur de ParamValueMode '${ParamValueMode[this._valueMode]}' inconnue`
Mathias Chouet's avatar
Mathias Chouet committed
675
676
677
        );
    }

678
679
680
681
682
683
684
685
686
687
688
    /**
     * Sets the current parameter to LINK mode, pointing to the given
     * symbol (might be a Parameter (computed or not) or an ExtraResult)
     * of the given Nub
     *
     * Prefer @see defineReferenceFromLinkedValue() whenever possible
     *
     * @param nub
     * @param symbol
     */
    public defineReference(nub: Nub, symbol: string) {
Mathias Chouet's avatar
Mathias Chouet committed
689
690
        // prevent loops - beware that all Nubs must be registered in the
        // current session or this test will always fail
Dorchies David's avatar
Dorchies David committed
691
        if (!this.isAcceptableReference(nub, symbol)) {
Mathias Chouet's avatar
Mathias Chouet committed
692
693
            throw new Error("defineReference() : link target is not an available linkable value");
        }
694
695
696
697
698
699
700
701
        // clear current reference
        this.undefineReference();
        // find what the symbol points to
        if (nub) {
            // 1. extra result ?
            // - start here to avoid extra results being presented as
            // parameters by the iterator internally used by nub.getParameter()
            // - extra results with no family are not linkable
702
            if (Object.keys(nub.resultsFamilies).includes(symbol)) {
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
                this._referencedValue = new LinkedValue(nub, undefined, symbol);
            } else {
                // 2. is it a parameter (possibly in CALC mode) ?
                const p = nub.getParameter(symbol);
                if (p) {
                    this._referencedValue = new LinkedValue(nub, p, symbol);
                }
            }
        }
        if (this._referencedValue) {
            // set value mode
            this.valueMode = ParamValueMode.LINK;
        } else {
            throw new Error(`defineReference - could not find target for ${nub.uid}.${symbol}`);
        }
    }

Mathias Chouet's avatar
Mathias Chouet committed
720
721
722
723
724
725
726
727
728
729
730
731
732
733
    /**
     * Asks the Session for available linkabke values for the current parameter,
     * and returns true if given { nub / symbol } pair is part of them
     */
    public isAcceptableReference(nub: Nub, symbol: string) {
        const linkableValues = Session.getInstance().getLinkableValues(this);
        for (const lv of linkableValues) {
            if (lv.nub.uid === nub.uid && lv.symbol === symbol) {
                return true;
            }
        }
        return false;
    }

734
735
736
737
738
739
740
741
742
743
744
745
746
    public defineReferenceFromLinkedValue(target: LinkedValue) {
        this._referencedValue = target;
        this.valueMode = ParamValueMode.LINK;
    }

    public undefineReference() {
        this._referencedValue = undefined;
    }

    public isReferenceDefined(): boolean {
        return (this._referencedValue !== undefined);
    }

747
748
    /**
     * Returns an object representation of the Parameter's current state
749
     * @param nubUidsInSession UIDs of Nubs that will be saved in session along with this one;
750
751
752
     *        useful to determine if linked parameters must be kept as links or have their value
     *        copied (if target is not in UIDs list); if undefined, wil consider all Nubs as
     *        available (ie. will not break links)
753
     */
754
    public objectRepresentation(nubUidsInSession?: string[]): { symbol: string, mode: string } {
755
756
757
        // parameter representation
        const paramRep: any = {
            symbol: this.symbol,
758
            mode: ParamValueMode[this._valueMode]
759
760
        };
        // adjust parameter representation depending on value mode
761
        switch (this._valueMode) {
762
            case ParamValueMode.SINGLE:
763
                paramRep.value = this.singleValue;
764
765
766
                break;

            case ParamValueMode.MINMAX:
767
768
769
                paramRep.min = this.min;
                paramRep.max = this.max;
                paramRep.step = this.step;
770
                paramRep.extensionStrategy = this.extensionStrategy;
771
772
773
                break;

            case ParamValueMode.LISTE:
774
                paramRep.values = this.valueList;
775
                paramRep.extensionStrategy = this.extensionStrategy;
776
777
778
                break;

            case ParamValueMode.LINK:
779
                if (nubUidsInSession === undefined || nubUidsInSession.includes(this._referencedValue.nub.uid)) {
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
                    // target Nub is available in session, link won't get broken
                    paramRep.targetNub = this._referencedValue.nub.uid;
                    paramRep.targetParam = this._referencedValue.symbol;
                } else {
                    // target Nub will be lost, copy value(s) to keep consistency
                    const targetPV = this._referencedValue.getParamValues(true);
                    paramRep.mode = ParamValueMode[targetPV.valueMode];
                    switch (targetPV.valueMode) {
                        case ParamValueMode.SINGLE:
                            paramRep.value = targetPV.singleValue;
                            break;
                        case ParamValueMode.MINMAX:
                            paramRep.min = targetPV.min;
                            paramRep.max = targetPV.max;
                            paramRep.step = targetPV.step;
                            paramRep.extensionStrategy = targetPV.extensionStrategy;
                            break;
                        case ParamValueMode.LISTE:
                            paramRep.values = targetPV.valueList;
                            paramRep.extensionStrategy = targetPV.extensionStrategy;
                            break;
                        // should never be in LINK or CALC mode (see LinkedValue methods)
                    }
                }
804
805
806
807
808
                break;
        }
        return paramRep;
    }

Mathias Chouet's avatar
Mathias Chouet committed
809
810
811
    /**
     * Fills the current Parameter, provided an object representation
     * @param obj object representation of a Parameter state
812
813
814
815
     * @returns object {
     *      calculated: boolean true if loaded parameter is in CALC mode
     *      hasErrors: boolean true if errors were encountered during loading
     * }
Mathias Chouet's avatar
Mathias Chouet committed
816
     */
817
818
819
820
821
822
823
824
    public loadObjectRepresentation(obj: any, setCalcMode: boolean = true)
        : { calculated: boolean, hasErrors: boolean } {

        const ret = {
            calculated: false,
            hasErrors: false
        };

Mathias Chouet's avatar
Mathias Chouet committed
825
826
827
828
829
830
        // set mode
        const mode: ParamValueMode = (ParamValueMode as any)[obj.mode]; // get enum index for string value

        // when loading parent Nub, setting mode to CALC would prevent ensuring
        // consistency when setting calculatedParam afterwards
        if (mode !== ParamValueMode.CALCUL || setCalcMode) {
Mathias Chouet's avatar
Fix #89    
Mathias Chouet committed
831
832
833
            try {
                this.valueMode = mode;
            } catch (err) {
834
835
836
837
838
839
                // silent fail : impossible to determine if this is an error, because
                // at this time, it is possible that no candidate for calculatedParam can
                // be found, since Nub children are not loaded yet (see nghyd#263)

                /* ret.hasErrors = true;
                console.error("loadObjectRepresentation: set valueMode error"); */
Mathias Chouet's avatar
Fix #89    
Mathias Chouet committed
840
            }
Mathias Chouet's avatar
Mathias Chouet committed
841
842
843
844
845
846
847
848
849
850
851
852
        }

        // set value(s)
        switch (mode) {
            case ParamValueMode.SINGLE:
                this.singleValue = obj.value;
                break;

            case ParamValueMode.MINMAX:
                this.min = obj.min;
                this.max = obj.max;
                this.step = obj.step;
853
854
855
                if (obj.extensionStrategy !== undefined) {
                    this.extensionStrategy = obj.extensionStrategy;
                }
Mathias Chouet's avatar
Mathias Chouet committed
856
857
858
859
                break;

            case ParamValueMode.LISTE:
                this.valueList = obj.values;
860
861
862
                if (obj.extensionStrategy !== undefined) {
                    this.extensionStrategy = obj.extensionStrategy;
                }
Mathias Chouet's avatar
Mathias Chouet committed
863
864
865
866
867
                break;

            case ParamValueMode.CALCUL:
                // although calculated param is set at Nub level (see below),
                // it is detected as "the only parameter in CALC mode"
868
                ret.calculated = true;
Mathias Chouet's avatar
Mathias Chouet committed
869
870
871
872
873
874
                break;

            case ParamValueMode.LINK:
                // formulaire dont le Nub est la cible du lien
                const destNub = Session.getInstance().findNubByUid(obj.targetNub);
                if (destNub) {
Mathias Chouet's avatar
Fix #89    
Mathias Chouet committed
875
876
877
                    try {
                        this.defineReference(destNub, obj.targetParam);
                    } catch (err) {
878
879
                        // silent fail : impossible to determine if this is an error, because
                        // fixLinks() might solve it later
Mathias Chouet's avatar
Fix #89    
Mathias Chouet committed
880
                    }
Mathias Chouet's avatar
Mathias Chouet committed
881
882
883
884
885
886
887
                } // si la cible du lien n'existe pas, Session.fixLinks() est censé s'en occuper
                break;

            default:
                throw new Error(`session file : invalid value mode '${obj.valueMode}' in param object ${obj.symbol}`);
        }

888
        return ret;
Mathias Chouet's avatar
Mathias Chouet committed
889
890
    }

891
    /**
892
     * Returns true if both the Nub UID and the symbol are identical
893
     */
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
    public equals(p: ParamDefinition): boolean {
        return (
            this.nubUid === p.nubUid
            && this.symbol === p.symbol
        );
    }

    /**
     * Returns true if this parameter's value can safely be linked to the
     * given parameter "src" (ie. without leading to circular dependencies)
     */
    public isLinkableTo(src: ParamDefinition): boolean {
        // did we loop back to the candidate parameter ?
        if (this.equals(src)) {
            // prevent infinite recursion
            return false;
        }

912
        if (this._valueMode === ParamValueMode.CALCUL) {
913
914
915
916
917
918
            // if my Nub doesn't depend on your result,
            // and my Nub doesn't already have a parameter linked to you,
            // you may depend on its result (me) !
            if (this._parent && this._parent.parent) {
                const myNub = (this.parentComputeNode as Nub);
                const ok = (
Dorchies David's avatar
Dorchies David committed
919
920
                    !myNub.dependsOnNubResult(src.parentNub)
                    && !myNub.dependsOnParameter(src)
921
922
                );
                return ok;
923
924
925
            }
        }

926
        if (this._valueMode === ParamValueMode.LINK && this.isReferenceDefined()) {
927
928
929
            // does the link point to an extra result ?
            if (this.referencedValue.isExtraResult()) {
                // if the target Nub doesn't depend on your result, you may depend on its extra result (me) !
Dorchies David's avatar
Dorchies David committed
930
                return (!this.referencedValue.nub.dependsOnNubResult(src.parentNub));
931
932
933
934
935
936
937
938
939
940
941
942
943

            } else { // the link points to a parameter, computed or not
                // recursively follow links, unless it loops back to the original parameter
                return (this.referencedValue.element as ParamDefinition).isLinkableTo(src);
            }
        }

        // SINGLE / MINMAX / LISTE parameters can always be linked without danger
        return true;
    }

    /**
     * Returns true if this parameter is ultimately (after followink links chain) linked
Mathias Chouet's avatar
Mathias Chouet committed
944
945
     * to a result or extra result of any of the given nubs.
     * If followLinksChain is false, will limit theexploration to the first target level only
946
     */
Mathias Chouet's avatar
Mathias Chouet committed
947
    public isLinkedToResultOfNubs(nubs: Nub[], followLinksChain: boolean = true): boolean {
948
        let linked = false;
949
        if (this._valueMode === ParamValueMode.LINK && this.isReferenceDefined()) {
950
951
952
953
954
955
956
957
958
            const targetNubUid = this.referencedValue.nub.uid;
            // is target a result ?
            if (this.referencedValue.isResult() || this.referencedValue.isExtraResult()) {
                outerloop:
                for (const y of nubs) {
                    if (y.uid === targetNubUid) {
                        // dependence found !
                        linked = true;
                        break outerloop;
959
960
961
962
                    } else if (this.referencedValue.nub.dependsOnNubResult(y)) {
                        // indirect result dependence found !
                        linked = true;
                        break outerloop;
963
                    }
964
                }
965
            } else { // target is a parameter
Mathias Chouet's avatar
Mathias Chouet committed
966
967
968
969
                // recursion ?
                if (followLinksChain) {
                    linked = (this.referencedValue.element as ParamDefinition).isLinkedToResultOfNubs(nubs);
                }
970
            }
Mathias Chouet's avatar
Mathias Chouet committed
971
        }
972
        return linked;
Mathias Chouet's avatar
Mathias Chouet committed
973
974
    }

975
    /**
976
977
     * Returns true if the current parameter is directly linked to the
     * Nub having UID "uid", its parent or any of its children
978
     * @param uid
979
980
981
982
     * @param symbol symbol of the target parameter whose value change triggered this method;
     *      if current Parameter targets this symbol, Nub will be considered dependent
     * @param includeValuesLinks if true, even if this Parameter targets a non-calculated non-modified
     *      parameter, Nub will be considered dependent @see jalhyd#98
983
     */
984
    public dependsOnNubFamily(uid: string, symbol?: string, includeValuesLinks: boolean = false): boolean {
985
        let linked = false;
986
        if (this._valueMode === ParamValueMode.LINK && this.isReferenceDefined()) {
987
            const ref = this._referencedValue;
988

989
990
991
992
993
            // direct, parent or children reference ?
            if (
                (ref.nub.uid === uid)
                || (ref.nub.getParent() && ref.nub.getParent().uid === uid)
                || (ref.nub.getChildren().map((c) => c.uid).includes(uid))
Dorchies David's avatar
Dorchies David committed
994
            ) {
995
996
997
998
999
                linked = (
                    (symbol !== undefined && symbol === ref.symbol)
                    || ref.isCalculated()
                    || includeValuesLinks
                );
1000
            } // else no recursion, checking level 1 only
1001
1002
        }
        return linked;
Mathias Chouet's avatar
Mathias Chouet committed
1003
1004
    }

1005
1006
    // interface IterableValues

1007
1008
    /**
     * Transparent proxy to own values iterator or targetted values iterator (in LINK mode)
Mathias Chouet's avatar
Mathias Chouet committed
1009
     * @TODO rewrite more simply ?
1010
     */
Mathias Chouet's avatar
Mathias Chouet committed
1011
    public get valuesIterator(): INumberIterator {
1012
        if (this.valueMode === ParamValueMode.LINK && this.isReferenceDefined()) {
1013
1014
1015
1016
1017
1018
            try {
                return this._referencedValue.getParamValues().valuesIterator;
            } catch (e) {
                // values are not computed yet
                return undefined;
            }
Mathias Chouet's avatar
Mathias Chouet committed
1019
1020
        } else {
            return this.paramValues.valuesIterator;
Mathias Chouet's avatar
Mathias Chouet committed
1021
1022
1023
        }
    }

1024
    public getExtendedValuesIterator(size: number): INumberIterator {
1025
        return this.paramValues.getValuesIterator(false, size, true);
1026
1027
    }

1028
1029
1030
1031
    /**
     * Returns true if there are more than 1 value associated to this parameter;
     * might be its own values (MINMAX / LISTE mode), or targetted values (LINK mode)
     */
Mathias Chouet's avatar
Mathias Chouet committed
1032
    public get hasMultipleValues(): boolean {
Mathias Chouet's avatar
Mathias Chouet committed
1033
        if (this._valueMode === ParamValueMode.LINK && this.isReferenceDefined()) {
1034
1035
            return this._referencedValue.hasMultipleValues();
        } else {
1036
            if (this._valueMode === ParamValueMode.CALCUL) {
Mathias Chouet's avatar
Mathias Chouet committed
1037
                return this.parentNub.resultHasMultipleValues();
1038
            } else {
Mathias Chouet's avatar
Mathias Chouet committed
1039
                return this.paramValues.hasMultipleValues;
1040
1041
            }
        }
Mathias Chouet's avatar
Mathias Chouet committed
1042
1043
1044
    }

    public initValuesIterator(reverse: boolean = false): INumberIterator {
1045
        return this.paramValues.getValuesIterator(reverse, undefined, true);
Mathias Chouet's avatar
Mathias Chouet committed
1046
1047
1048
    }

    public get hasNext(): boolean {
1049
        return this.paramValues.hasNext;
Mathias Chouet's avatar
Mathias Chouet committed
1050
1051
1052
    }

    public next(): IteratorResult<number> {
1053
        return this.paramValues.next();
Mathias Chouet's avatar
Mathias Chouet committed
1054
1055
1056
    }

    public [Symbol.iterator](): IterableIterator<number> {
1057
        return this.paramValues;
Mathias Chouet's avatar
Mathias Chouet committed
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
    }

    // interface IObservable

    /**
     * ajoute un observateur à la liste
     */
    public addObserver(o: Observer) {
        this._observable.addObserver(o);
    }

    /**
     * supprime un observateur de la liste
     */
    public removeObserver(o: Observer) {
        this._observable.removeObserver(o);
    }

    /**
     * notifie un événement aux observateurs
     */
    public notifyObservers(data: any, sender?: any) {
        this._observable.notifyObservers(data, sender);
    }

    /**
     * variable calculable par l'équation ?
     */
    public isAnalytical(): boolean {
        return this.calculability === ParamCalculability.EQUATION;
1088
1089
    }

Dorchies David's avatar
Dorchies David committed
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
    /**
     * Get the highest nub in the child nub hierarchy
     */
    get originNub(): Nub {
        let originNub = this.parentNub;
        while (true) {
            if (originNub.parent === undefined) {
                return originNub;
            }
            originNub = originNub.parent;
        }
    }

1103
1104
1105
1106
1107
    /**
     * Renvoie la valeur actuelle du paramètre (valeur courante ou résultat si calculé)
     */
    get V(): number {
        if (this.valueMode === ParamValueMode.CALCUL) {
1108
1109
1110
            if (this.parentNub.result !== undefined) {
                if (this.parentNub.result.resultElement.ok) {
                    return this.parentNub.result.vCalc;
1111
1112
1113
1114
1115
1116
                }
            }
        }
        return this.currentValue;
    }

1117
    public clone(): ParamDefinition {
1118
        const res = new ParamDefinition(this._parent, this._symbol,
1119
            this.domain.clone(), this._unit, this.singleValue, this._family, this.visible);
1120
1121
1122
        res._calc = this._calc;
        return res;
    }
Mathias Chouet's avatar
Mathias Chouet committed
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139

    /**
     * notification envoyée après la modification de la valeur du paramètre
     */
    private notifyValueModified(sender: any) {
        this.notifyObservers(
            { action: "paramdefinitionAfterValue" }, sender
        );
    }

    /**
     * vérifie si un min/max est valide par rapport au domaine de définition
     */
    private isMinMaxDomainValid(v: number): boolean {
        if (v === undefined) {
            return false;
        }
1140
        if (this._valueMode === ParamValueMode.MINMAX) {
Mathias Chouet's avatar
Mathias Chouet committed
1141
            try {
1142
                this.checkValueAgainstDomain(v);
Mathias Chouet's avatar
Mathias Chouet committed
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
            } catch (e) {
                return false;
            }
        }
        return true;
    }

    private checkMinMax(min: number, max: number): boolean {
        return this.isMinMaxDomainValid(min) && this.isMinMaxDomainValid(max) && (min < max);
    }

    private get isListValid(): boolean {
1155
        if (this.paramValues.valueList === undefined) {
Mathias Chouet's avatar
Mathias Chouet committed
1156
1157
1158
            return false;
        }

1159
        for (const v of this.paramValues.valueList) {
Mathias Chouet's avatar
Mathias Chouet committed
1160
            try {
1161
                this.checkValueAgainstDomain(v);
Mathias Chouet's avatar
Mathias Chouet committed
1162
1163
1164
1165
1166
1167
1168
            } catch (e) {
                return false;
            }
        }
        return true;
    }

1169
    /**
Mathias Chouet's avatar
Mathias Chouet committed
1170
1171
1172
     * Throws an error if this.paramValues._valueMode is not the expected value mode.
     * In LINK mode, the tested valueMode is the mode of the ultimately targetted
     * set of values (may never be LINK)
1173
     */
1174
    private checkValueMode(expected: ParamValueMode[]) {
Dorchies David's avatar
Dorchies David committed
1175
        if (!expected.includes(this.valueMode)) {
Mathias Chouet's avatar