modules/arbres.js

import { add, number, multiply } from 'mathjs'
import { homothetie, latexParCoordonnees, latexParPoint, point, segment, translation, vecteur } from './2d.js'
import { fraction } from './fractions.js'
import { arrondi, calcul } from './outils.js'

export function texProba (proba, rationnel, precision) {
  return rationnel ? fraction(proba, 1).toLatex() : number(arrondi(proba, precision)).toString().replace('.', '{,}')
}
/**
 * classe pour faire des arbres de probabilités
 * @author Jean-Claude Lhote
 * la classe Arbre permet de définir un arbre de probabilité.
 * à son sommet, il y a un Arbre dont la proba est 1 et qui a la propriété racine = true (c'est le seul)
 * Ses enfants sont eux-mêmes Arbre(s).
 * Une terminaison de l'arbre est un arbre qui a pour enfants []
 * Un Arbre possède un nom (de type string) qui l'identifie de façon unique (c'est important si on veut éviter des proba aléatoires)
 * chaque Arbre possède une proba. C'est la probabilité qu'on a d'atteindre cet arbre à partir de son parent.
 * exemple : const pins = new Arbre({nom: 'pins',proba: 1, rationnel: true, racine: true) définit la racine de l'arbre
 * pins.enfants[0] = new Arbre(nom: 'malade', proba: 0.3, rationnel: true)
 * pins.enfants[1] = new Arbre(nom: 'sain', rationnel: true, proba: 0.7)
 * Note : on peut aussi utiliser la méthode setFils()
 * par exemple, la dernière assertions peut être remplacée par
 * pins.setFils('sain', 0.7, true)
 */
export class Arbre {
  /**
   * @param {object} parametres
   * @param {string} [parametres.nom]
   * @param {numberparametres.rationnel | FractionX} [parametres.proba]
   * @param {Arbre[]} [parametres.enfants]
   * @param {boolean} [parametres.rationnel]
   * @param {boolean} [parametres.visible]
   * @param {string} [parametres.alter]
   * @param {boolean} [parametres.racine]
   */
  constructor ({ nom, proba, enfants, rationnel, visible, alter, racine } = {}) {
    this.racine = racine !== undefined ? Boolean(racine) : false
    this.enfants = enfants !== undefined ? Array(...enfants) : []
    this.nom = nom !== undefined ? String(nom) : ''
    this.rationnel = rationnel !== undefined ? Boolean(rationnel) : true
    this.proba = proba !== undefined ? (rationnel ? fraction(proba, 1) : number(proba)) : 0
    this.visible = visible !== undefined ? visible : true
    this.alter = alter !== undefined ? String(alter) : ''
    this.taille = 0
    this.pos = 0
  }

  // questionnement : est-ce qu'on vérifie à chaque ajout que la somme des probabilités ne dépasse pas 1 ?
  /**
   * @param {String} nom Le nom de cet Arbre-fils
   * @param {Number} proba La probabilité d'aller à ce fils depuis le père.
   * @returns l'Arbre-fils créé
   * Exemple : const sylvestre = pin.setFils('sylvestre', 0.8) un 'pin' a une probabilité de 0.8 d'être 'sylvestre'.
   */
  setFils (nom, proba, rationnel) {
    const arbre = new Arbre(this, nom, proba, (rationnel || this.rationnel))
    this.enfants.push(arbre)
    return arbre
  }

  /**
   * Fonction récursive qui cherche dans la descendance complète un arbre nommé.
   * @param {String} nom Le nom de l'Arbre recherché dans les fils
   * @returns l'Arbre descendant portant ce nom.
   * Exemple : const unArbre = pin.getFils('sylvestre')
   */
  getFils (nom) {
    const monArbre = []
    for (const arbre of this.enfants) {
      if (arbre.nom === nom) return [arbre]
      else {
        monArbre.push(...arbre.getFils(nom))
      }
    }
    return monArbre
  }

  // est-ce qu'on vérifie si la somme des probabilités ne dépasse pas 1 ?
  /**
   *
   * @param {String} nom Le nom de l'Arbre recherché dans les fils
   * @param {Number} proba La probabilité du fils pour le père.
   * @param {boolean} rationnel true si la proba doit être mise sous la forme d'une fraction
   * @returns l'Arbre-fils.
   */
  setFilsProba (nom, proba, rationnel) { // si le fils nommé nom existe, on fixe sa proba (en gros, on la modifie)
    let arbre = this.getFils(nom)
    if (arbre) {
      arbre.proba = (rationnel || this.rationnel) ? fraction(proba, 1) : number(proba)
    } else { // sinon on ajoute ce fils.
      arbre = new Arbre(this, nom, proba, (rationnel || this.rationnel))
      this.enfants.push(arbre)
    }
    return arbre
  }

  isFraction (obj) {
    return (typeof obj === 'object' && ['Fraction', 'FractionX'].indexOf(obj.type) !== -1)
  }

  /**
   * fonction récursive pour calculer la probabilité d'atteindre un enfant à partir de l'arbre courant.
   * exemple : pin.getProba('malade', true)
   * @param {String} nom Le nom d'un descendant ou pas
   * @param {boolean} rationnel si true alors on retourne une fraction
   * @returns Probabilité conditionnelle ou pas d'atteindre l'arbre nommé à partir du père.
   * Exemple : si pin.getFilsProba('sylvestre')===0.8 et si sylvestre.getFilsProba('malade')===0.5
   * alors pin.getProba('malade')===0.4 et sylvestre.getProba('malade')===0.4 aussi ! par contre
   * sylvestre.getProba('malade', 1)= 0.5
   */
  getProba (nom, rationnel) {
    let p = rationnel ? fraction(0, 1) : 0
    let probaArbre = rationnel ? fraction(0, 1) : 0
    let getPro
    if (this.nom === nom) return (rationnel || this.rationnel) ? (this.isFraction(this.proba) ? this.proba : fraction(this.proba, 1)) : number(this.proba)
    else {
      for (const arbre of this.enfants) {
        if (arbre.nom === nom) {
          p = add(p, (rationnel || this.rationnel) ? ((typeof arbre.proba === 'number' || typeof arbre.proba === 'string') ? fraction(arbre.proba, 1) : arbre.proba) : number(arbre.proba))
        } else {
          if (rationnel) {
            getPro = arbre.getProba(nom, true)
            probaArbre = add(this.isFraction(probaArbre) ? probaArbre : fraction(probaArbre, 1),
              multiply(this.isFraction(arbre.proba) ? arbre.proba : fraction(arbre.proba, 1),
                this.isFraction(getPro) ? getPro : fraction(getPro, 1)))
          } else {
            getPro = arbre.getProba(nom, false)
            probaArbre = number(probaArbre) + number(multiply(arbre.proba, number(getPro)))
          }
        }
      }
      p = add(p, (rationnel || this.rationnel) ? this.isFraction(probaArbre) ? probaArbre : fraction(probaArbre, 1) : number(probaArbre))
    }
    return rationnel ? (this.isFraction(p) ? p : fraction(p, 1)) : number(p)
  }

  // méthode pour compter les descendants de l'arbre (le nombre de feuilles terminales).
  branches () {
    let nbBranches = 0
    if (this.enfants.length === 0) return 1
    else {
      for (const enfant of this.enfants) {
        nbBranches += enfant.branches()
      }
    }
    return nbBranches
  }

  // Methode à appeler avant de représenter l'arbre car elle va récursivement définir toutes les tailles...
  setTailles () {
    try {
      this.taille = this.branches()
      for (const arbre of this.enfants) {
        arbre.setTailles()
      }
    } catch (error) {
      console.log(error)
      return false
    }
    return true
  }

  /**
   * @param {number} xOrigine
   * @param {number} yOrigine
   * @param {number} decalage
   * @param {number} echelle
   * @param {boolean} vertical true : vertical, false : horizontal
   * @param {number} sens -1 ou 1 définit le sens de l'arbre gauche/droite ou haut/bas
   * @param {number} tailleCaracteres définit la taille pour ce qui est écrit.
   * xOrigine et yOrigine définissent le point de référence de l'arbre... c'est un angle du cadre dans lequel l'arbre est construit par la position de la racine
   * decalage vaut 0 lors de l'appel initial... cette valeur est modifiée pendant le parcours de l'arbre.
   * echelle est à fixé à 3 si on utilise des fractions et peut être déscendu à 2 si on utilise des nombres décimaux... echelle peut être décimal.
   * vertical est un booléen. Si true, alors l'arbre sera construit de bas en haut ou de haut en bas, sinon, il sera construit de gauche à droite ou de droite à gauche.
   * sens indique la direction de pousse : 1 positif, -1 négatif.
   */
  represente (xOrigine = 0, yOrigine = 0, decalage = 0, echelle = 1, vertical = false, sens = -1, tailleCaracteres) {
    tailleCaracteres = tailleCaracteres || 5
    const objets = []
    const A = point(vertical
      ? xOrigine
      : xOrigine + decalage + this.taille * echelle / 2
    , vertical
      ? yOrigine + decalage - this.taille * echelle / 2
      : yOrigine
    , '', 'center')
    const B = point(vertical
      ? xOrigine - sens * 5
      : xOrigine
    , vertical
      ? yOrigine
      : yOrigine - sens * 5
    )
    const labelA = latexParCoordonnees(this.nom, A.x + (vertical ? 0.5 * sens : 0), A.y + (vertical ? 0 : 0.5 * sens), 'black', 15 * this.nom.length, 20, 'white', tailleCaracteres)
    const positionProba = vertical ? translation(homothetie(A, B, 0.6), vecteur(0, A.y > B.y ? 0.5 : -0.5), '', 'center') : translation(homothetie(A, B, 0.6), vecteur(A.x > B.x ? 0.5 : -0.5, 0), '', 'center') // Proba au 2/5 de [AB] en partant de A.
    const probaA = this.visible
      ? latexParPoint(texProba(this.proba, this.rationnel, 2), positionProba, 'black', 20, 24, '', tailleCaracteres)
      : latexParPoint(this.alter, positionProba, 'black', 20, 24, 'white', tailleCaracteres)
    if (this.enfants.length === 0) {
      return [segment(B, A), labelA, probaA]
    } else {
      for (let i = 0; i < this.enfants.length; i++) {
        objets.push(...this.enfants[i].represente(vertical
          ? xOrigine + sens * 5
          : xOrigine + decalage + this.taille * echelle / 2
        , vertical
          ? yOrigine + decalage - this.taille * echelle / 2
          : yOrigine + sens * 5
        , vertical
          ? calcul(echelle * ((this.enfants.length / 2 - i) * this.enfants[i].taille))
          : calcul(echelle * ((i - this.enfants.length / 2) * this.enfants[i].taille)),
        echelle, vertical, sens, tailleCaracteres))
      }
      if (this.racine) {
        objets.push(labelA)
      } else {
        objets.push(segment(B, A), labelA, probaA)
      }
    }
    return objets
  }
}