nub.ts 52 KB
Newer Older
1
import { CalculatorType, ComputeNode } from "./compute-node";
2
import { Dichotomie } from "./dichotomie";
3
4
import { acSection, MacrorugoCompound, Pab, ParamDefinition, ParamsEquation,
         Session, Structure } from "./index";
5
import { LinkedValue } from "./linked-value";
6
import { ParamCalculability, ParamFamily } from "./param/param-definition";
7
import { ParamValueMode } from "./param/param-value-mode";
Mathias Chouet's avatar
Mathias Chouet committed
8
import { ParamValues } from "./param/param-values";
9
10
import { IParamDefinitionIterator } from "./param/param_definition_iterator";
import { ParamsEquationArrayIterator } from "./param/params_equation_array_iterator";
Mathias Chouet's avatar
Mathias Chouet committed
11
import { Props } from "./props";
12
import { SessionSettings } from "./session_settings";
13
import { Message, MessageCode, MessageSeverity } from "./util/message";
14
import { IObservable, Observable, Observer } from "./util/observer";
15
import { Result } from "./util/result";
16
import { ResultElement } from "./util/resultelement";
17

18
/**
Mathias Chouet's avatar
Mathias Chouet committed
19
20
 * Classe abstraite de Noeud de calcul dans une session :
 * classe de base pour tous les calculs
21
 */
22
export abstract class Nub extends ComputeNode implements IObservable {
23

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    protected static concatPrms(p1: ParamsEquation[], p2: ParamsEquation[]): ParamsEquation[] {
        const p3: ParamsEquation[] = p1;
        for (const p of p2) {
            p3.push(p);
        }
        return p3;
    }

    private static progressPercentageAccordedToChainCalculation = 50;

    /** paramétrage de la dichotomie */
    public dichoStartIntervalMaxSteps: number = 100;

    /** pointer to parent Nub */
    public parent: Nub;

Mathias Chouet's avatar
Mathias Chouet committed
40
41
42
43
44
45
46
    /** type of children elements, used by GUI for translation */
    protected _childrenType: string = "";

    public get childrenType(): string {
        return this._childrenType;
    }

Mathias Chouet's avatar
Mathias Chouet committed
47
48
    public get result(): Result {
        return this._result;
49
50
    }

51
52
53
54
55
56
    /**
     * Local setter to set results of Equation() / Solve() / …  as current
     * ResultElement, instead of overwriting the whole Result object
     * (used by CalcSerie with varying parameters)
     */
    protected set currentResult(r: Result) {
57
58
59
        if (! this._result) {
            this.initNewResultElement();
        }
60
61
62
        this._result.resultElement = r.resultElement;
    }

63
    /** Returns Props object (observable set of key-values) associated to this Nub */
64
    public get properties(): Props {
Mathias Chouet's avatar
Mathias Chouet committed
65
66
        // completes props with calcType if not already set
        this._props.setPropValue("calcType", this.calcType);
Mathias Chouet's avatar
Mathias Chouet committed
67
68
69
        return this._props;
    }

70
    public set properties(props: Props) {
Mathias Chouet's avatar
Mathias Chouet committed
71
        this.setProperties(props);
Mathias Chouet's avatar
Mathias Chouet committed
72
73
    }

Dorchies David's avatar
Dorchies David committed
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
    /**
     * return ParamsEquation of all children recursively
     */
    public get childrenPrms(): ParamsEquation[] {
        const prms: ParamsEquation[] = [];
        if (this._children.length) { // if called within constructor, default class member value is not set yet
            for (const child of this._children) {
                prms.push(child.prms);
                if (child.getChildren()) {
                    if (child.getChildren().length) {
                        Nub.concatPrms(prms, child.childrenPrms);
                    }
                }
            }
        }
        return prms;
    }

92
93
94
95
96
97
    /**
     * Returns an array with the calculable parameters
     */
    public get calculableParameters(): ParamDefinition[] {
        const calcPrms: ParamDefinition[] = [];
        for (const p of this.parameterIterator) {
98
            if ([ParamCalculability.DICHO, ParamCalculability.EQUATION].includes(p.calculability)) {
99
100
101
102
103
104
                calcPrms.push(p);
            }
        }
        return calcPrms;
    }

Mathias Chouet's avatar
Mathias Chouet committed
105
106
107
108
109
110
111
112
    /**
     * Returns an iterator over :
     *  - own parameters (this._prms)
     *  - children parameters (this._children[*]._prms)
     */
    public get parameterIterator(): IParamDefinitionIterator {
        const prms: ParamsEquation[] = [];
        prms.push(this._prms);
Dorchies David's avatar
Dorchies David committed
113
114
        if (this._children) {
            Nub.concatPrms(prms, this.childrenPrms);
Mathias Chouet's avatar
Mathias Chouet committed
115
116
117
118
        }
        return new ParamsEquationArrayIterator(prms);
    }

119
120
121
122
123
    protected get progress() {
        return this._progress;
    }

    /**
124
125
     * Updates the progress percentage and notifies observers,
     * at most once per 300ms
126
127
128
     */
    protected set progress(v: number) {
        this._progress = v;
129
130
131
132
133
134
135
136
137
        const currentTime = new Date().getTime();
        if (
            (currentTime - this.previousNotificationTimestamp) > 30
            || v === 100
        ) {
            // console.log(">> notifying !");
            this.notifyProgressUpdated();
            this.previousNotificationTimestamp = currentTime;
        }
138
139
    }

140
    public get calcType(): CalculatorType {
Mathias Chouet's avatar
Mathias Chouet committed
141
        return this._calcType;
142
143
    }

Mathias Chouet's avatar
Mathias Chouet committed
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
    public get calculatedParam(): ParamDefinition {
        return this._calculatedParam;
    }

    /**
     * Sets p as the parameter to be computed; sets it to CALC mode
     */
    public set calculatedParam(p: ParamDefinition) {
        this._calculatedParam = p;
        this._calculatedParam.valueMode = ParamValueMode.CALCUL;
    }

    /**
     * Returns a parameter descriptor compatible with Calc() methods,
     * ie. a symbol string for a Nub's main parameter, or an object
     * of the form { uid: , symbol: } for a Nub's sub-Nub parameter
     * (ex: Structure parameter in ParallelStructure)
     */
    public get calculatedParamDescriptor(): string | { uid: string, symbol: string } {
        if (this.uid === this._calculatedParam.nubUid) {
            return this._calculatedParam.symbol;
        } else {
            return {
                uid: this._calculatedParam.nubUid,
                symbol: this._calculatedParam.symbol
            };
        }
    }

Dorchies David's avatar
Dorchies David committed
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
    /** parameter that is to be computed by default - to be overloaded by child classes */
    protected _defaultCalculatedParam: ParamDefinition;

    /** parameter that is to be computed */
    protected _calculatedParam: ParamDefinition;

    /**
     * List of children Nubs; browsed by parameters iterator.
     *  - for ParallelStructures: contains 0 or more Structures
     *  - for SectionNub : contains exactly 1 acSection
     */
    protected _children: Nub[];

    /** properties describing the Nub type */
    protected _props: Props = new Props();

189
    /** résultat de Calc()/CalcSerie() */
190
    protected _result: Result;
191

Dorchies David's avatar
Dorchies David committed
192
193
194
195
196
197
198
199
200
    /** implémentation par délégation de IObservable */
    private _observable: Observable;

    /** a rough indication of calculation progress, between 0 and 100 */
    private _progress: number = 0;

    /** allows notifying of progress every X milliseconds only */
    private previousNotificationTimestamp = 0;

201
202
    private _firstAnalyticalPrmSymbol: string;

Dorchies David's avatar
Dorchies David committed
203
204
    public constructor(prms: ParamsEquation, dbg: boolean = false) {
        super(prms, dbg);
205
        this.deleteAllChildren();
Dorchies David's avatar
Dorchies David committed
206
207
208
209
210
        this._observable = new Observable();
        this._defaultCalculatedParam = this.getFirstAnalyticalParameter();
        this.resetDefaultCalculatedParam();
    }

Mathias Chouet's avatar
Mathias Chouet committed
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
    // move code out of setter to ease inheritance
    public setProperties(props: Props) {
        // copy observers
        const observers = this._props.getObservers();
        // empty props
        this._props.reset();
        // restore observers
        for (const obs of observers) {
            this._props.addObserver(obs);
        }
        // set new props values
        for (const p of Object.keys(props.props)) {
            this._props.setPropValue(p, props.getPropValue(p));
        }
    }

Mathias Chouet's avatar
Mathias Chouet committed
227
    /**
228
229
230
231
     * Finds the previous calculated parameter and sets its mode to SINGLE
     */
    public unsetCalculatedParam(except: ParamDefinition) {
        for (const p of this.parameterIterator) {
232
233
234
235
            if (
                p.valueMode === ParamValueMode.CALCUL
                && p !== except
            ) {
236
237
238
239
240
241
242
243
244
245
                p.setValueMode(ParamValueMode.SINGLE, false);
            }
        }
    }

    /**
     * Tries to reset the calculated parameter, successively, to :
     *  - the default one if it is in SINGLE mode
     *  - the first SINGLE calculable parameter other than requirer
     *  - the first MINMAX/LISTE calculable parameter other than requirer
Dorchies David's avatar
Dorchies David committed
246
     *
Mathias Chouet's avatar
Mathias Chouet committed
247
248
249
250
251
252
253
     * If no default calculated parameter is defined, does nothing.
     */
    public resetDefaultCalculatedParam(requirer?: ParamDefinition) {
        if (this._defaultCalculatedParam) {
            // if default calculated param is not eligible to CALC mode
            if (
                requirer === this._defaultCalculatedParam
254
255
256
257
258
259
                || ! [
                        ParamValueMode.SINGLE,
                        ParamValueMode.MINMAX,
                        ParamValueMode.LISTE
                    ].includes(this._defaultCalculatedParam.valueMode
                )
Mathias Chouet's avatar
Mathias Chouet committed
260
            ) {
Mathias Chouet's avatar
Mathias Chouet committed
261
                // first SINGLE calculable parameter if any
262
                const newCalculatedParam = this.findFirstCalculableParameter(requirer);
Mathias Chouet's avatar
Mathias Chouet committed
263
264
265
                if (newCalculatedParam) {
                    this.calculatedParam = newCalculatedParam;
                } else {
266
                    // @TODO throws when a new linkable nub is added, and all parameters are already linked !
267
                    throw Error("resetDefaultCalculatedParam : could not find any SINGLE/MINMAX/LISTE parameter");
Mathias Chouet's avatar
Mathias Chouet committed
268
269
270
271
272
273
274
275
276
277
278
                }
            } else {
                // default one
                this.calculatedParam = this._defaultCalculatedParam;
            }
        } else {
            // do nothing (ex: Section Paramétrée)
        }
    }

    /**
279
280
     * Returns the first visible calculable parameter other than otherThan,
     * that is set to SINGLE, MINMAX or LISTE mode (there might be none)
Mathias Chouet's avatar
Mathias Chouet committed
281
     */
282
    public findFirstCalculableParameter(otherThan?: ParamDefinition) {
Mathias Chouet's avatar
Mathias Chouet committed
283
284
        for (const p of this.parameterIterator) {
            if (
285
286
287
288
                [ ParamCalculability.EQUATION, ParamCalculability.DICHO ].includes(p.calculability)
                && p.visible
                && p !== otherThan
                && [ ParamValueMode.SINGLE, ParamValueMode.MINMAX, ParamValueMode.LISTE ].includes(p.valueMode)
Mathias Chouet's avatar
Mathias Chouet committed
289
            ) {
290
                return p;
Mathias Chouet's avatar
Mathias Chouet committed
291
292
            }
        }
293
        return undefined;
Mathias Chouet's avatar
Mathias Chouet committed
294
295
    }

296
    /**
297
298
     * Formule utilisée pour le calcul analytique (solution directe ou méthode de résolution spécifique)
     */
299
    public abstract Equation(sVarCalc: string): Result;
300

301
    /**
302
     * Calculate and put in cache the symbol of first parameter calculable analytically
303
     */
304
    get firstAnalyticalPrmSymbol(): string {
305
306
307
        if (this._firstAnalyticalPrmSymbol === undefined) {
            this._firstAnalyticalPrmSymbol = this.getFirstAnalyticalParameter().symbol;
        }
308
309
310
311
312
313
314
315
        return this._firstAnalyticalPrmSymbol;
    }

    /**
     * Run Equation with first analytical parameter to compute
     * Returns the result in number form
     */
    public EquationFirstAnalyticalParameter(): number {
316
317
318
319
320
321
        const res = this.Equation(this.firstAnalyticalPrmSymbol);
        if (! res.ok) {
            this._result = res;
            throw new Error(this.constructor.name + ".EquationFirstAnalyticalParameter(): fail");
        }
        return res.vCalc;
322
323
    }

324
    /**
325
326
     * Calcul d'une équation quelle que soit l'inconnue à calculer; déclenche le calcul en
     * chaîne des modules en amont si nécessaire
327
328
329
     * @param sVarCalc nom de la variable à calculer
     * @param rInit valeur initiale de la variable à calculer dans le cas de la dichotomie
     */
Mathias Chouet's avatar
Mathias Chouet committed
330
331
332
333
334
335
336
337
    public Calc(sVarCalc?: string, rInit?: number): Result {
        let computedVar;
        // overload calculated param at execution time ?
        if (sVarCalc) {
            computedVar = this.getParameter(sVarCalc);
        } else {
            computedVar = this.calculatedParam;
        }
338

339
        if (rInit === undefined) {
340
            rInit = computedVar.v;
341
        }
342
        if (computedVar.isAnalytical()) {
343
            this.currentResult = this.Equation(sVarCalc);
344
            this._result.symbol = sVarCalc;
345
            return this._result;
346
        }
347

348
        const resSolve: Result = this.Solve(sVarCalc, rInit);
349
        if (!resSolve.ok) {
350
            this.currentResult = resSolve;
351
            this._result.symbol = sVarCalc;
352
            return this._result;
353
354
        }
        const sAnalyticalPrm: string = this.getFirstAnalyticalParameter().symbol;
355
        computedVar.v = resSolve.vCalc;
356
357
        const res: Result = this.Equation(sAnalyticalPrm);
        res.vCalc = resSolve.vCalc;
358

359
        this.currentResult = res;
360
        this._result.symbol = sVarCalc;
361
362
363

        this.notifyResultUpdated();

364
        return this._result;
365
366
    }

367
    /**
368
369
     * Calculates required Nubs so that all input data is available;
     * uses 50% of the progress
370
     * @returns true if everything went OK, false otherwise
371
     */
372
    public triggerChainCalculation(): { ok: boolean, message: Message } {
Mathias Chouet's avatar
Mathias Chouet committed
373
        const requiredNubs1stLevel = this.getRequiredNubs();
374
375
376
        if (requiredNubs1stLevel.length > 0) {
            const progressStep = Nub.progressPercentageAccordedToChainCalculation / requiredNubs1stLevel.length;
            for (const rn of requiredNubs1stLevel) {
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
                const r = rn.CalcSerie();
                if (r.hasGlobalError() || r.hasOnlyErrors) {
                    // something has failed in chain
                    return {
                        ok: false,
                        message: new Message(MessageCode.ERROR_IN_CALC_CHAIN)
                    };
                } else if (
                    this.resultHasMultipleValues
                    && (
                        r.hasErrorMessages() // some steps failed
                        // or upstream Nub has already triggered a warning message; pass it on
                        || r.globalLog.contains(MessageCode.WARNING_ERROR_IN_CALC_CHAIN_STEPS)
                    )
                ) {
                    // if a parameter varies, errors might have occurred for
                    // certain steps (but not all steps)
                    return {
                        ok: true,
                        message: new Message(MessageCode.WARNING_ERROR_IN_CALC_CHAIN_STEPS)
                    };
                }
399
400
401
402
                this.progress += progressStep;
            }
            // round progress to accorded percentage
            this.progress = Nub.progressPercentageAccordedToChainCalculation;
403
        }
404
405
406
407
        return {
            ok: true,
            message: undefined
        };
408
409
    }

410
    /**
411
412
     * Effectue une série de calculs sur un paramètre; déclenche le calcul en chaîne
     * des modules en amont si nécessaire
413
     * @param rInit solution approximative du paramètre
414
     * @param sDonnee éventuel symbole / paire symbole-uid du paramètre à calculer
415
     */
416
    public CalcSerie(rInit?: number): Result {
417
        // variated parameters caracteristics
418
        const variated: Array<{ param: ParamDefinition, values: ParamValues }> = [];
419

420
        // prepare calculation
421
        let extraLogMessage: Message; // potential chain calculation warning to add to result at the end
422
        this.progress = 0;
423
        this.resetResult();
424
425
426
427
428
429
430
431
432
433
434
435
        const ccRes = this.triggerChainCalculation();
        if (ccRes.ok) {
            // might still have a warning log
            if (ccRes.message !== undefined) {
                extraLogMessage = ccRes.message;
            }
        } else {
            // something went wrong in the chain
            this._result = new Result(undefined, this);
            this._result.globalLog.add(ccRes.message);
            return this._result;
        }
436
        this.copySingleValuesToSandboxValues();
437
438

        // check which values are variating, if any
439
        for (const p of this.parameterIterator) {
440
441
442
443
444
445
446
447
448
449
450
451
452
            if (
                p.valueMode === ParamValueMode.LISTE
                || p.valueMode === ParamValueMode.MINMAX
                || (
                    p.valueMode === ParamValueMode.LINK
                    && p.isReferenceDefined()
                    && p.referencedValue.hasMultipleValues()
                )
            ) {
                variated.push({
                    param: p,
                    // extract variated values from variated Parameters
                    // (in LINK mode, proxies to target data)
453
                    values: p.paramValues
454
                });
455
456
457
            }
        }

458
        // find calculated parameter
459
        const computedSymbol = this.findCalculatedParameter();
460

461
        if (rInit === undefined && this.calculatedParam) {
Mathias Chouet's avatar
Mathias Chouet committed
462
            rInit = this.calculatedParam.v;
463
        }
464

465
466
467
        // reinit Result and children Results
        this.reinitResult();

468
        if (variated.length === 0) { // no parameter is varying
469
470
471
            // prepare a new slot to store results
            this.initNewResultElement();
            this.doCalc(computedSymbol, rInit); // résultat dans this.currentResult (resultElement le plus récent)
472
473
            this.progress = 100;

474
475
476
477
478
479
480
481
482
        } else { // at least one parameter is varying
            // find longest series
            let longest = 0;
            for (let i = 0; i < variated.length; i++) {
                if (variated[i].values.valuesIterator.count() > variated[longest].values.valuesIterator.count()) {
                    longest = i;
                }
            }
            const size = variated[longest].values.valuesIterator.count();
Mathias Chouet's avatar
Mathias Chouet committed
483

484
485
486
487
            // grant the remaining percentage of the progressbar to local calculation
            // (should be 50% if any chain calculation occurred, 100% otherwise)
            let progressStep;
            const remainingProgress = 100 - this.progress;
488
            progressStep = remainingProgress / size;
489

490
491
492
            // (re)init all iterators
            for (const v of variated) {
                if (v === variated[longest]) {
493
                    v.values.initValuesIterator(false, undefined, true);
494
                } else {
495
                    v.values.initValuesIterator(false, size, true);
496
497
498
499
                }
            }

            // iterate over longest series (in fact any series would do)
500
501
502
            while (variated[longest].values.hasNext) {
                // get next value for all variating parameters
                for (const v of variated) {
503
504
                    const currentIteratorValue = v.values.next();
                    v.param.v = currentIteratorValue.value;
505
                }
506
507
                // prepare a new slot to store results
                this.initNewResultElement();
508
                // calculate
509
510
                this.doCalc(computedSymbol, rInit); // résultat dans this.currentResult (resultElement le plus récent)
                if (this._result.resultElement.ok) {
511
                    rInit = this._result.resultElement.vCalc;
512
                }
513
514
                // update progress
                this.progress += progressStep;
515
            }
516
517
            // round progress to 100%
            this.progress = 100;
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532

            // abstract of iterations logs
            let errors = 0;
            let warnings = 0;
            for (const re of this._result.resultElements) {
                for (const lm of re.log.messages) {
                    if (lm.getSeverity() === MessageSeverity.ERROR) {
                        errors ++;
                    } else if (lm.getSeverity() === MessageSeverity.WARNING) {
                        warnings ++;
                    }
                }
            }
            // String()ify numbers below, to avoid decimals formatting on screen (ex: "3.000 errors encoutered...")
            if (errors > 0) {
533
534
535
                this._result.globalLog.add(
                    new Message(MessageCode.WARNING_ERRORS_ABSTRACT, { nb: String(errors) })
                );
536
537
            }
            if (warnings > 0) {
538
539
540
                this._result.globalLog.add(
                    new Message(MessageCode.WARNING_WARNINGS_ABSTRACT, { nb: String(warnings) })
                );
541
            }
542
543
        }

544
545
        if (computedSymbol !== undefined) {
            const realSymbol = (typeof computedSymbol === "string") ? computedSymbol : computedSymbol.symbol;
546
            this._result.symbol = realSymbol;
547
        }
548

549
550
        this.notifyResultUpdated();

551
552
553
554
        if (extraLogMessage !== undefined) {
            this._result.globalLog.add(extraLogMessage);
        }

555
        return this._result;
556
557
    }

Mathias Chouet's avatar
Mathias Chouet committed
558
559
560
561
562
563
564
565
    public addChild(child: Nub, after?: number) {
        if (after !== undefined) {
            this._children.splice(after + 1, 0, child);
        } else {
            this._children.push(child);
        }
        // add reference to parent collection (this)
        child.parent = this;
Mathias Chouet's avatar
Fix #86    
Mathias Chouet committed
566
567
        // postprocessing
        this.adjustChildParameters(child);
Mathias Chouet's avatar
Mathias Chouet committed
568
    }
569
570

    public getChildren(): Nub[] {
Mathias Chouet's avatar
Mathias Chouet committed
571
        return this._children;
572
573
574
    }

    public getParent(): Nub {
Mathias Chouet's avatar
Mathias Chouet committed
575
        return this.parent;
576
577
    }

578
579
580
581
582
583
584
585
586
587
588
589
    /**
     * Returns true if all parameters are valid; used to check validity of
     * parameters linked to Nub results
     */
    public isComputable() {
        let valid = true;
        for (const p of this.prms) {
            valid = valid && p.isValid;
        }
        return valid;
    }

590
591
592
593
594
    /**
     * Liste des valeurs (paramètre, résultat, résultat complémentaire) liables à un paramètre
     * @param src paramètre auquel lier d'autres valeurs
     */
    public getLinkableValues(src: ParamDefinition): LinkedValue[] {
595
        let res: LinkedValue[] = [];
596
597
        const symbol = src.symbol;

598
599
600
601
        // If parameter comes from the same Nub, its parent or any of its children,
        // no linking is possible at all.
        // Different Structures in the same parent can get linked to each other.
        if (! this.isParentOrChildOf(src.nubUid)) {
602

Mathias Chouet's avatar
Mathias Chouet committed
603
            // 1. own parameters
604
605
606
607
608
609
            for (const p of this._prms) {
                // if symbol and Nub type are identical, or if family is identical
                if (
                    (p.symbol === symbol && this.calcType === src.nubCalcType)
                    || (p.family !== undefined && (p.family === src.family))
                ) {
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
                    // if variability doesn't cause any problem (a non-variable
                    // parameter cannot be linked to a variating one)
                    if (src.calculability !== ParamCalculability.FIXED || ! p.hasMultipleValues) {
                        // if it is safe to link p's value to src
                        if (p.isLinkableTo(src)) {
                            // if p is a CALC param of a Structure other than "Q"
                            // (structures always have Q as CALC param and cannot have another)
                            // or a CALC param of a Section, that is not sibling of the target
                            // (to prevent circular dependencies among ParallelStructures),
                            // expose its parent
                            if (
                                (
                                (this instanceof Structure && p.symbol !== "Q" && ! this.isSiblingOf(src.nubUid))
                                || this instanceof acSection
                                )
                                && (p.valueMode === ParamValueMode.CALCUL)
                            ) {
                                // trick to expose p as a result of the parent Nub
                                res.push(new LinkedValue(this.parent, p, p.symbol));
                            } else {
                                // do not suggest parameters that are already linked to another one
                                if (p.valueMode !== ParamValueMode.LINK) {
                                    res.push(new LinkedValue(this, p, p.symbol));
                                }
634
                            }
635
                        }
636
637
638
                    }
                }
            }
639

640
            // 2. extra results
641
            if (this._resultsFamilies) {
642
                // if I don't depend on your result, you may depend on mine !
Mathias Chouet's avatar
Mathias Chouet committed
643
                if (! this.dependsOnNubResult(src.parentNub)) {
644
                    const erk = Object.keys(this._resultsFamilies);
645
                    // browse extra results
646
                    for (const erSymbol of erk) {
647
                        const erFamily = this._resultsFamilies[erSymbol];
648
649
650
651
652
653
654
655
                        // if family is identical and variability doesn't cause any problem
                        if (
                            erFamily === src.family
                            && (
                                src.calculability !== ParamCalculability.FIXED
                                || ! this.resultHasMultipleValues
                            )
                        ) {
656
657
658
659
                            res.push(new LinkedValue(this, undefined, erSymbol));
                        }
                    }
                }
660
661
            }
        }
662

663
664
        // 3. children Nubs, except for PAB and MRC
        if (! (this instanceof Pab || this instanceof MacrorugoCompound)) {
665
666
667
            for (const cn of this.getChildren()) {
                res = res.concat(cn.getLinkableValues(src));
            }
668
669
        }

670
        return res;
671
    }
672

673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
    /**
     * Returns true if the given Nub UID is either of :
     *  - the current Nub UID
     *  - the current Nub's parent Nub UID
     *  - the UID of any of the current Nub's children
     */
    public isParentOrChildOf(uid: string): boolean {
        if (this.uid === uid) {
            return true;
        }
        const parent = this.getParent();
        if (parent && parent.uid === uid) {
            return true;
        }
        for (const c of this.getChildren()) {
            if (c.uid === uid) {
                return true;
            }
        }
        return false;
    }

Mathias Chouet's avatar
Mathias Chouet committed
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
    /**
     * Returns true if the given Nub UID :
     *  - is the current Nub UID
     *  - is the UID of any of the current Nub's siblings (children of the same parent)
     */
    public isSiblingOf(uid: string): boolean {
        if (this.uid === uid) {
            return true;
        }
        const parent = this.getParent();
        if (parent) {
            for (const c of parent.getChildren()) {
                if (c.uid === uid) {
                    return true;
                }
            }
            return true;
        }
        return false;
    }

Mathias Chouet's avatar
Mathias Chouet committed
716
717
718
719
720
    /**
     * Returns all Nubs whose results are required by the given one,
     * without following links (stops when it finds a Nub that has to
     * be calculated). Used to trigger chain calculation.
     */
721
    public getRequiredNubs(visited: string[] = []): Nub[] {
Mathias Chouet's avatar
Mathias Chouet committed
722
        const requiredNubs: Nub[] = [];
Mathias Chouet's avatar
Mathias Chouet committed
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
        // prevent loops
        if (! visited.includes(this.uid)) {
            visited.push(this.uid);

            // inspect all target Nubs
            for (const p of this.parameterIterator) {
                if (p.valueMode === ParamValueMode.LINK && p.isReferenceDefined()) {
                    const nub = p.referencedValue.nub;

                    // a Nub is required if I depend on its result
                    if (this.dependsOnNubResult(nub, false)) {
                        requiredNubs.push(nub);
                    }
                }
            }
        }
        return requiredNubs;
    }

742
743
744
745
746
747
748
749
750
    /**
     * Returns all Nubs whose results are required by the given one,
     * following links. Used by Solveur.
     */
    public getRequiredNubsDeep(visited: string[] = []): Nub[] {
        let requiredNubs: Nub[] = this.getRequiredNubs(visited);
        for (const rn of requiredNubs) {
            requiredNubs = requiredNubs.concat(rn.getRequiredNubsDeep(visited));
        }
751
752
753
        requiredNubs = requiredNubs.filter(
            (item, index) => requiredNubs.indexOf(item) === index // deduplicate
        );
754
755
756
        return requiredNubs;
    }

757
    /**
758
     * Returns all Nubs whose parameters or results are targetted
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
     * by the given one.
     * (used for dependencies checking at session saving time)
     */
    public getTargettedNubs(visited: string[] = []) {
        const targettedNubs: Nub[] = [];
        // prevent loops
        if (! visited.includes(this.uid)) {
            visited.push(this.uid);

            // inspect all target Nubs
            for (const p of this.parameterIterator) {
                if (p.valueMode === ParamValueMode.LINK && p.isReferenceDefined()) {
                    targettedNubs.push(p.referencedValue.nub);
                }
            }
        }
        return targettedNubs;
    }

778
    /**
779
780
781
782
783
784
785
786
787
788
     * Returns true if
     *  - this Nub
     *  - any of this Nub's children
     *  - this Nub's parent
     * depends on
     *  - the result of the given Nub
     *  - the result of any of the given Nub's children
     *  - the result of the given Nub's parent
     *
     * (ie. "my family depends on the result of your family")
Mathias Chouet's avatar
Mathias Chouet committed
789
790
     *
     * If followLinksChain is false, will limit the exploration to the first target level only
791
     */
Mathias Chouet's avatar
Mathias Chouet committed
792
    public dependsOnNubResult(nub: Nub, followLinksChain: boolean = true): boolean {
793
794
795
796
797
798
799
        let thisFamily: Nub[] = [this];
        const tp = this.getParent();
        if (tp) {
            thisFamily = thisFamily.concat(tp);
        }
        thisFamily = thisFamily.concat(this.getChildren());

800
        let yourFamily: Nub[] = [];
Mathias Chouet's avatar
Mathias Chouet committed
801
        const you = nub;
802
803
804
805
806
807
808
        if (you) {
            yourFamily = [you];
            const yp = you.getParent();
            if (yp) {
                yourFamily = yourFamily.concat(yp);
            }
            yourFamily = yourFamily.concat(you.getChildren());
809
810
811
812
813
814
815
816
817
        }

        let depends = false;

        outerloop:
        for (const t of thisFamily) {
            // look for a parameter linked to the result of any Nub of yourFamily
            for (const p of t.prms) {
                if (p.valueMode === ParamValueMode.LINK) {
Mathias Chouet's avatar
Mathias Chouet committed
818
                    if (p.isLinkedToResultOfNubs(yourFamily, followLinksChain)) {
819
820
821
822
823
824
825
826
827
828
829
                        // dependence found !
                        depends = true;
                        break outerloop;
                    }
                }
            }
        }

        return depends;
    }

830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
    /**
     * Returns true if this nub (parent and children included)
     * directly requires (1st level only) @TODO follow links ?
     * the given parameter
     */
    public dependsOnParameter(src: ParamDefinition) {
        let thisFamily: Nub[] = [this];
        const tp = this.getParent();
        if (tp) {
            thisFamily = thisFamily.concat(tp);
        }
        thisFamily = thisFamily.concat(this.getChildren());

        let depends = false;

        outerloop:
        for (const t of thisFamily) {
            // look for a parameter of thisFamily that is directly linked to src
            for (const p of t.prms) {
                if (
                    p.valueMode === ParamValueMode.LINK
                    && p.isReferenceDefined()
                    && p.referencedValue.isParameter()
                ) {
                    if (
                        p.referencedValue.nub.uid === src.nubUid
                        && p.referencedValue.symbol === src.symbol
                    ) {
                        // dependence found !
                        depends = true;
                        break outerloop;
                    }
                }
            }
        }

        return depends;
    }

869
870
871
872
    /**
     * Returns true if the computation of the current Nub (parent and children
     * included) directly requires (1st level only), anything (parameter or result)
     * from the given Nub UID "uid", its parent or any of its children
873
     * @param uid
874
875
876
877
     * @param symbol symbol of the target parameter whose value change triggered this method;
     *      if current Nub targets this symbol, it will be considered dependent
     * @param includeValuesLinks if true, even if this Nub targets only non-calculated non-modified
     *      parameters, it will be considered dependent @see jalhyd#98
878
     */
879
880
881
882
883
884
    public resultDependsOnNub(
        uid: string,
        visited: string[] = [],
        symbol?: string,
        includeValuesLinks: boolean = false
    ): boolean {
885
886
887
888
889
890
        if (uid !== this.uid && ! visited.includes(this.uid)) {
            visited.push(this.uid);

            // does any of our parameters depend on the target Nub ?
            for (const p of this._prms) {
                if (p.valueMode === ParamValueMode.LINK) {
891
                    if (p.dependsOnNubFamily(uid, symbol, includeValuesLinks)) {
892
893
894
895
896
897
898
                        return true;
                    }
                }
            }
            // does any of our parent's parameters depend on the target Nub ?
            const parent = this.getParent();
            if (parent) {
899
                if (parent.resultDependsOnNub(uid, visited, symbol, includeValuesLinks)) {
900
901
902
903
904
                    return true;
                }
            }
            // does any of our children' parameters depend on the target Nub ?
            for (const c of this.getChildren()) {
905
                if (c.resultDependsOnNub(uid, visited, symbol, includeValuesLinks)) {
906
                    return true;
907
                }
908
            }
909
        }
910
911
        return false;
    }
912

913
914
915
916
917
918
    /**
     * Returns true if the computation of the current Nub has multiple values
     * (whether it is already computed or not), by detecting if any parameter,
     * linked or not, is variated
     */
    public resultHasMultipleValues(): boolean {
919
        let hmv = false;
Mathias Chouet's avatar
Mathias Chouet committed
920
        for (const p of this.parameterIterator) {
921
            if (p.valueMode === ParamValueMode.MINMAX || p.valueMode === ParamValueMode.LISTE) {
922
                hmv = true;
Mathias Chouet's avatar
Mathias Chouet committed
923
            } else if (p.valueMode === ParamValueMode.LINK && p.isReferenceDefined()) {
924
                // indirect recursivity
925
                hmv = hmv || p.referencedValue.hasMultipleValues();
926
            }
927
        }
928
        return hmv;
929
    }
930

931
932
    /**
     * Sets the current result to undefined, as well as the results
Mathias Chouet's avatar
Mathias Chouet committed
933
934
     * of all depending Nubs; also invalidates all fake ParamValues
     * held by LinkedValues pointing to this result
935
     */
936
    public resetResult(visited: string[] = []) {
937
        this._result = undefined;
938
        visited.push(this.uid);
939
940
        const dependingNubs = Session.getInstance().getDependingNubs(this.uid);
        for (const dn of dependingNubs) {
941
942
943
            if (! visited.includes(dn.uid)) {
                dn.resetResult(visited);
            }
Mathias Chouet's avatar
Mathias Chouet committed
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
            dn.resetLinkedParamValues(this);
        }
    }

    /**
     * For all parameters pointing to the result of the given Nub,
     * invalidates fake ParamValues held by pointed LinkedValue
     */
    public resetLinkedParamValues(nub: Nub) {
        for (const p of this.parameterIterator) {
            if (p.valueMode === ParamValueMode.LINK && p.isReferenceDefined()) {
                if (
                    (p.referencedValue.isResult() || p.referencedValue.isExtraResult())
                    && p.referencedValue.nub.uid === nub.uid
                ) {
                    p.referencedValue.invalidateParamValues();
                }
            }
962
963
964
        }
    }

Mathias Chouet's avatar
Mathias Chouet committed
965
966
967
968
969
970
971
972
973
    /**
     * Duplicates the current Nub, but does not register it in the session
     */
    public clone(): Nub {
        const serialised = this.serialise();
        const clone = Session.getInstance().unserialiseSingleNub(serialised, false);
        return clone.nub;
    }

974
975
976
    /**
     * Returns a JSON representation of the Nub's current state
     * @param extra extra key-value pairs, for ex. calculator title in GUI
977
978
979
     * @param nubUidsInSession UIDs of Nubs that will be saved in session along with this one;
     *                         useful to determine if linked parameters must be kept as links
     *                         or have their value copied (if target is not in UIDs list)
980
     */
981
982
    public serialise(extra?: object, nubUidsInSession?: string[]) {
        return JSON.stringify(this.objectRepresentation(extra, nubUidsInSession));
983
984
985
986
987
    }

    /**
     * Returns an object representation of the Nub's current state
     * @param extra extra key-value pairs, for ex. calculator title in GUI
988
989
990
     * @param nubUidsInSession UIDs of Nubs that will be saved in session along with this one;
     *                         useful to determine if linked parameters must be kept as links
     *                         or have their value copied (if target is not in UIDs list)
991
     */
992
    public objectRepresentation(extra?: object, nubUidsInSession?: string[]): object {
993
994
        let ret: any = {
            uid: this.uid,
995
            props: Session.invertEnumKeysAndValuesInProperties(this.properties.props),
996
        };
Mathias Chouet's avatar
Mathias Chouet committed
997

998
999
1000
        if (extra) {
            ret =  {...ret, ...{ meta: extra } }; // merge properties
        }
Mathias Chouet's avatar
Mathias Chouet committed
1001
        ret = {...ret, ...{  children: [], parameters: [] } }; // extend here to make "parameters" the down-most key
1002

Mathias Chouet's avatar
Mathias Chouet committed
1003
1004
1005
        // iterate over local parameters
        const localParametersIterator =  new ParamsEquationArrayIterator([this._prms]);
        for (const p of localParametersIterator) {
1006
            if (p.visible) {
1007
                ret.parameters.push(p.objectRepresentation(nubUidsInSession));
1008
1009
1010
            }
        }

Mathias Chouet's avatar
Mathias Chouet committed
1011
1012
        // iterate over children Nubs
        for (const child of this._children) {
1013
            ret.children.push(child.objectRepresentation(undefined, nubUidsInSession));
Mathias Chouet's avatar
Mathias Chouet committed
1014
1015
        }

1016
1017
1018
        return ret;
    }

1019
1020
1021
    /**
     * Fills the current Nub with parameter values, provided an object representation
     * @param obj object representation of a Nub content (parameters)
Mathias Chouet's avatar
Mathias Chouet committed
1022
1023
     * @returns the calculated parameter found, if any - used by child Nub to notify
     *          its parent of the calculated parameter to set
1024
     */
1025
1026
1027
1028
1029
1030
    public loadObjectRepresentation(obj: any): { p: ParamDefinition, hasErrors: boolean } {
        // return value
        const ret: { p: ParamDefinition, hasErrors: boolean } = {
            p: undefined,
            hasErrors: false
        };
1031
1032
1033
1034
        // set parameter modes and values
        if (obj.parameters && Array.isArray(obj.parameters)) {
            for (const p of obj.parameters) {
                const param = this.getParameter(p.symbol);
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
                if (param) {
                    // load parameter
                    const pRet = param.loadObjectRepresentation(p, false);
                    if (pRet.calculated) {
                        ret.p = param;
                    }
                    // forward errors
                    if (pRet.hasErrors) {
                        ret.hasErrors = true;
                    }
                } else {
1046
                    // tslint:disable-next-line:no-console
1047
1048
                    console.error(`session file : cannot find parameter ${p.symbol} in target Nub`);
                    ret.hasErrors = true;
1049
1050
                }
            }
Mathias Chouet's avatar
Mathias Chouet committed
1051
            // define calculated param at Nub level
1052
            // @TODO except if we are a Section / Structure / Cloisons ?
1053
1054
            if (ret.p) {
                this.calculatedParam = ret.p;
Mathias Chouet's avatar
Mathias Chouet committed
1055
            }
1056
        }
Mathias Chouet's avatar
Mathias Chouet committed
1057
1058
1059
1060

        // iterate over children if any
        if (obj.children && Array.isArray(obj.children)) {
            for (const s of obj.children) {
1061
1062
                // decode properties
                const props = Session.invertEnumKeysAndValuesInProperties(s.props, true);
Mathias Chouet's avatar
Mathias Chouet committed
1063
                // create the Nub
1064
                const subNub = Session.getInstance().createNub(new Props(props), this);
Mathias Chouet's avatar
Mathias Chouet committed
1065
1066
1067
1068
                // try to keep the original ID
                if (! Session.getInstance().uidAlreadyUsed(s.uid)) {
                    subNub.setUid(s.uid);
                }
1069
                const childRet = subNub.loadObjectRepresentation(s);
Mathias Chouet's avatar
Mathias Chouet committed
1070
                // add Structure to parent
1071
                this.addChild(subNub);
Mathias Chouet's avatar
Mathias Chouet committed
1072
                // set calculated parameter for child ?
1073
1074
1075
1076
1077
1078
                if (childRet.p) {
                    this.calculatedParam = childRet.p;
                }
                // forward errors
                if (childRet.hasErrors) {
                    ret.hasErrors = true;
Mathias Chouet's avatar
Mathias Chouet committed
1079
1080
1081
1082
                }
            }
        }

1083
        return ret;
1084
1085
    }

1086
1087
1088
1089
    /**
     * Once session is loaded, run a second pass on all linked parameters to
     * reset their target if needed
     */
1090
1091
1092
1093
1094
    public fixLinks(obj: any): { hasErrors: boolean } {
        // return value
        const ret = {
            hasErrors: false
        };
1095
1096
1097
1098
1099
1100
1101
1102
        if (obj.parameters && Array.isArray(obj.parameters)) {
            for (const p of obj.parameters) {
                const mode: ParamValueMode = (ParamValueMode as any)[p.mode]; // get enum index for string value
                if (mode === ParamValueMode.LINK) {
                    const param = this.getParameter(p.symbol);
                    // formulaire dont le Nub est la cible du lien
                    const destNub = Session.getInstance().findNubByUid(p.targetNub);
                    if (destNub) {
Mathias Chouet's avatar
Fix #89    
Mathias Chouet committed
1103
1104
1105
                        try {
                            param.defineReference(destNub, p.targetParam);
                        } catch (err) {
1106
                            // tslint:disable-next-line:no-console
Mathias Chouet's avatar
Fix #95    
Mathias Chouet committed
1107
1108
                            console.error(`fixLinks: defineReference error`
                                + ` (${this.uid}.${param.symbol} => ${destNub.uid}.${p.targetParam})`);
1109
1110
                            // forward error
                            ret.hasErrors = true;
Mathias Chouet's avatar
Fix #89    
Mathias Chouet committed
1111
                        }
1112
                    } else {
1113
                        // tslint:disable-next-line:no-console
1114
                        console.error("fixLinks : cannot find target Nub");
1115
1116
                        // forward error
                        ret.hasErrors = true;
1117
1118
1119
1120
                    }
                }
            }
        }
1121
        return ret;
1122
1123
    }

Mathias Chouet's avatar
Mathias Chouet committed
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
    /**
     * Replaces a child Nub
     * @param index index of child in _children array
     * @param child new child
     * @param resetCalcParam if true, resets default calculated parameter after replacing child
     * @param keepParametersState if true, mode and value of parameters will be kept between old and new section
     */
    public replaceChild(index: number, child: Nub, resetCalcParam: boolean = false,
                        keepParametersState: boolean = true
    ) {
        if (index > -1 && index < this._children.length) {
            const hasOldChild = (this._children[index] !== undefined);
            const parametersState: any = {};

            // store old parameters state
            if (keepParametersState && hasOldChild) {
                for (const p of this._children[index].parameterIterator) {
1141
1142
1143
1144
1145
                    // if p is also present and visible in new Nub
                    const cp = child.getParameter(p.symbol);
                    if (cp !== undefined && cp.visible && p.visible) {
                        parametersState[p.symbol] = p.objectRepresentation([]);
                    }
Mathias Chouet's avatar
Mathias Chouet committed
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
                }
            }

            // replace child
            this._children[index] = child;

            // reapply parameters state
            if (keepParametersState && hasOldChild) {
                for (const p of this._children[index].parameterIterator) {
                    if (p.symbol in parametersState) {
                        p.loadObjectRepresentation(parametersState[p.symbol]);
                    }
                }
            }
        } else {
            throw new Error(`${this.constructor.name}.replaceChild invalid index ${index}`
                          + ` (children length: ${this._children.length})`);
        }
1164
1165
1166
        // postprocessing
        this.adjustChildParameters(child);
        // ensure one parameter is calculated
Mathias Chouet's avatar
Mathias Chouet committed
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
        if (resetCalcParam) {
            this.resetDefaultCalculatedParam();
        }
        // add reference to parent collection (this)
        child.parent = this;
    }

    /**
     * Finds oldChild in the list, and replaces it at the same index with newChild;
     * if the current calculated parameter belonged to the old child, resets a default one
     * @param oldChild child to get the index for
     * @param newChild child to set at this index
     */
    public replaceChildInplace(oldChild: Nub, newChild: Nub) {
        const calcParamLost = (
            typeof this.calculatedParamDescriptor !== "string"
            && this.calculatedParamDescriptor.uid === oldChild.uid
        );
        const index = this.getIndexForChild(oldChild);
        if (index === -1) {
            throw new Error("old child not found");
        }
        this.replaceChild(index, newChild, calcParamLost);
1190
    }
Mathias Chouet's avatar
Mathias Chouet committed
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223

    /**
     * Returns the current index of the given child if any,
     * or else returns -1
     * @param child child or child UID to look for