objets/CMathGraphDoc.js

/*
 * MathGraph32 Javascript : Software for animating online dynamic mathematics figures
 * https://www.mathgraph32.org/
 * @Author Yves Biton (yves.biton@sesamath.net)
 * @License: GNU AGPLv3 https://www.gnu.org/licenses/agpl-3.0.html
 */
import { saveAs } from 'file-saver'

import Color from '../types/Color'
import { base64Encode, mimeType, mtgFileExtension, getLanguage, version } from '../kernel/kernel'
import CListeObjets from './CListeObjets'
import CPrototype from './CPrototype'
import DataOutputStream from '../entreesSorties/DataOutputStream'
import Dimf from '../types/Dimf'

export default CMathGraphDoc

export const defaultSurfOpacity = 0.2

/**
 * Classe contenant des informations sur la figure et un array de CPrototype
 * (qui peut être vide) ainis que la CListeObjets contenant les objets proprement dits
 * graphiques et non graphiques.
 * Version réservée à mtgApp
 * @constructor
 * @param {string} idDoc  L'id du document ui est celui du svg contenant la figure.
 * @param {boolean} displayOnLoad  true si la figure doit être affichée
 * dès que son chargement est terminé.
 * @param {boolean} isActive  true si la figure est active et répond aux actions souris et clavier.
 * @param {boolean} decimalDot true si le séparateur décimal est le point
 * @returns {CMathGraphDoc}
 */
function CMathGraphDoc (idDoc, displayOnLoad, isActive, decimalDot = true) {
  this.idDoc = idDoc
  this.wheelListener = null //  fonction de callBack appelée quand on zoome ou dézoome avec la molette de la souris
  this.displayOnLoad = arguments.length > 1 ? displayOnLoad : true // Pas utilisé pour le moment
  this.isActive = arguments.length > 2 ? isActive : true
  this.numeroVersion = version
  this.listePr = new CListeObjets('radian', idDoc, decimalDot)
  this.listePr.associeA(this)
  this.language = getLanguage()
  this.texteFigure = ''
  // Version 9.0 : dimMinFig contient les dimensions mini à affecter à la figure quand on l'affiche ou l'édite
  this.dimMinFig = new Dimf(0, 0)
  /** @type {CPrototype[]} */
  this.tablePrototypes = []
  this.isDirty = false
  /** @type {CPt} */
  this.pointCapture = null
  this.couleurFond = Color.white
  this.imageFond = null
  this.imageFondVisible = true
  this.itemsCochesInterdits = true
  this.listeIdMenus = []
  this.type = '' // Pour les événements souris
  this.affichagesFiges = false
  this.marquesAnglesFigees = false
  this.buttonValueAllowed = true
  this.buttonFunctionAllowed = true
  /**
   * Sera mis à true si on rencontre un event mousemove (ça n'existe pas sur les les périphériques uniquement tactiles
   * à la différence de mousedown)
   * @type {boolean}
   */
  this.hasMouse = false // Sera mis à true si un mousemove event est rencontré
  /**
   * Curseur qui sera mis dans body.style.cursor par MtgAppLecteur.prototype.mousemove s'il n'y a pas d'objet proche
   * @type {string}
   */
  this.defaultCursor = 'default'
  /**
   * Une éventuelle map (eventName -> callback) utilisée par la méthode addSvgListener de l'api (toujours null sans usage de l'api)
   * @type {null|Map}
   */
  this.cbmap = null
}

// Modifié version 5.2. On passe un paramètre nVersion qui représente le N° de version de la liste à partir de laquelle le prototype est chargé
/**
 * Fonction chargeant les éventuelles constructions de la figure (CPrototype)
 * @param {DataInputStream} inps
 * @param nVersion Le n° de version de la liste dans laquelle on lit les prototypes
 */
CMathGraphDoc.prototype.readPrototypes = function (inps, nVersion) {
  // On lit d'abord le nombre de prototypes
  const nbProt = inps.readInt()
  for (let i = 0; i < nbProt; i++) {
    this.addPrototype(inps, nVersion)
  }
}

/**
 * Fonction ajoutant un prototype depuis le flux de données binaires inps
 * @param {DataInputStream} inps
 * @param nVersion Le n° de version de la liste dans laquelle on lit les prototypes
 */
CMathGraphDoc.prototype.addPrototype = function (inps, nVersion) {
  const prot = new CPrototype(this.listePr)
  prot.read(inps, nVersion)
  this.tablePrototypes.push(prot)
}

/**
 * Fonction renvoyant la construction de la figure de nom nomProto
 * @param {string} nomProto
 * @returns {CPrototype|null} null s'il n'y a pas de prototype de ce nom
 */
CMathGraphDoc.prototype.getPrototype = function (nomProto) {
  for (let i = 0; i < this.tablePrototypes.length; i++) {
    if (this.tablePrototypes[i].nom === nomProto) return this.tablePrototypes[i]
  }
  return null
}
/**
 * Fonction enregistrant les pprototypes (constructions) de la figure.
 * @param {DataOutputStream} oups
 * @returns {void}
 */
CMathGraphDoc.prototype.writePrototypes = function (oups) {
  oups.writeInt(this.tablePrototypes.length)
  for (let i = 0; i < this.tablePrototypes.length; i++) {
    const prot = this.tablePrototypes[i]
    prot.write(oups)
  }
}
/**
 * Fonction lisant le document depuis un flux de données binaires.
 * @param {DataInputStream} inps
 * @returns {void}
 */
CMathGraphDoc.prototype.read = function (inps) {
  // On saute les 17 premiers octets qui contiennent une chaine
  inps.skip(16)
  this.numeroVersion = inps.readInt()
  // Ligne suivante pour corriger un oubli dans la première version où le numéro de document
  // était nul
  if (this.numeroVersion === 0) this.numeroVersion = 5
  if (this.numeroVersion > version) { // Amélioré version 4.4
    throw new Error('VersionErr')
  }
  const numeroVersion = this.numeroVersion
  // A partir de la version 8.0 (numéro de version 20), les surfaces ont leur propre opacity
  // mais on garde un CMathGraphDoc.opacity pour l'affecter aux surfaces quand on lit les objets surfaces
  // pour les numéros de version supérieurs à 6 et inférieurs ou égaux à 19
  if (numeroVersion >= 6) {
    if (numeroVersion < 20) this.opacity = inps.readFloat()
    else this.opacity = defaultSurfOpacity
  } else this.opacity = 1 // pas de transparence pour les anciennes figures. A modifier dans Options Figure en cours
  this.language = inps.readUTF() // la langue dans laquelle a été créé le document
  // Changements pour la version 1.8 (version 3)
  // Changement pour version 2.0 : on enregistre le degré de transparence des surfaces pleines dans un float
  const couleurRouge = inps.readByte() // initialement readUnsignedByte()
  const couleurVert = inps.readByte()
  const couleurBleu = inps.readByte()
  this.couleurFond = new Color(couleurRouge, couleurVert, couleurBleu)
  const isOld = numeroVersion < 8
  this.imageFondVisible = isOld ? true : inps.readBoolean() // Nouveau version 4.4
  this.modeTraceActive = isOld ? false : inps.readBoolean() // Nouveau version 4.4
  if (isOld) {
    inps.readUTF()
    this.natImageFond = DataOutputStream.nat_noImage
  } else {
    this.natImageFond = inps.readByte()
  }
  if (this.natImageFond !== DataOutputStream.nat_noImage) {
    this.imageFond = inps.readBufferedImage()
    if (numeroVersion > 10) {
      this.widthImageFond = inps.readInt()
      this.heightImageFond = inps.readInt()
    } else {
      const svg = document.getElementById(this.idDoc) // Rajouté version 6.4.1
      this.widthImageFond = svg.getAttribute('width')
      this.heightImageFond = svg.getAttribute('height')
    }
  } else {
    this.imageFond = null
  }
  // On lit un booléen indiquant si les codes d'items de menu correspondent à
  // des items interdits ou non
  this.itemsCochesInterdits = inps.readBoolean()
  // On charge l'éventuelle liste d'items de menus
  const nombreItems = inps.readInt()
  /** @type {number[]} */
  this.listeIdMenus = []
  if (nombreItems !== 0) {
    for (let i = 0; i < nombreItems; i++) {
      const a = inps.readInt()
      this.listeIdMenus.push(a) // à revoir
    }
  }
  if (numeroVersion < 15) {
    inps.readUTF()
    inps.skip(5)
  }
  this.texteFigure = inps.readUTF()
  let minWidth = inps.readInt()
  let minHeight = inps.readInt()
  // Version 9 : On se sert maintenant de ces deux données qui représentent largeur et hauteur mini
  // allouées à la figure en pixels
  if (numeroVersion < 23) {
    minWidth = 0
    minHeight = 0
  }
  this.dimMinFig = new Dimf(minWidth, minHeight)
  this.affichagesFiges = inps.readBoolean()
  this.marquesAnglesFigees = inps.readBoolean()
  // Nouveau pour la version 4.6 : Deux boolean disant si les champs d'édition peuvent avoir des boutons
  // Valeur et Fontions prédéfinies
  if (numeroVersion >= 10) {
    this.buttonValueAllowed = inps.readBoolean()
    this.buttonFunctionAllowed = inps.readBoolean()
  }
  this.readPrototypes(inps, numeroVersion)
  // Nouveauté pour la version 2.5 : La liste doit être au courant du n° de
  // version de document
  const list = this.listePr
  list.numeroVersion = numeroVersion
  list.associeA(this) // Modifié version 4.8
  list.read(inps)
  list.numeroVersion = version
  this.numeroVersion = version
  // Il faut adapter les formules à la langue en cours d'utilisation
  list.reconstruitChainesCalcul()
}
/**
 * Fonction enregistrant le document dans un flux de données binaire.
 * @param {DataOutputStream} oups
 * @returns {void}
 */
CMathGraphDoc.prototype.write = function (oups) {
  oups.writeCString('MathGraphJava1.0')
  oups.writeInt(this.numeroVersion)
  // Changement pour version 2.0 : on enregistre le degré de transparence des surfaces pleines dans un float
  // A partir de la version 8.0 (n° de version 20) chaque élément surface a sa propre opacité et donc
  // on n'enregistre plus une opacity globale pour les surfaces dans le flux
  // oups.writeFloat(this.opacity)
  oups.writeUTF(this.language)
  oups.writeByte(this.couleurFond.getRed())
  oups.writeByte(this.couleurFond.getGreen())
  oups.writeByte(this.couleurFond.getBlue())
  oups.writeBoolean(this.imageFondVisible)
  // Nouveau version 4.4 : un boolean disant si le mode trace est activé
  oups.writeBoolean(this.modeTraceActive)
  if (this.imageFond !== null) {
    oups.writeByte(this.natImageFond)
    oups.writeBufferedImage(this.imageFond, this.natImageFond) // Si pas d'image Fond un int de valeur nulle est enregistré dans le flux.
    oups.writeInt(this.widthImageFond)
    oups.writeInt(this.heightImageFond)
  } else oups.writeByte(DataOutputStream.nat_noImage)
  // On lit un booléen indiquant si les codes d'items de menu correspondent à
  // des items interdits ou non
  oups.writeBoolean(this.itemsCochesInterdits)
  // On enregistre l'éventuelle liste d'items de menus
  const nombreItems = this.listeIdMenus.length
  oups.writeInt(nombreItems)
  if (nombreItems !== 0) {
    for (let i = 0; i < nombreItems; i++) {
      oups.writeInt(this.listeIdMenus[i])
    }
  }
  oups.writeUTF(this.texteFigure)
  oups.writeInt(this.dimMinFig.x)
  oups.writeInt(this.dimMinFig.y)
  oups.writeBoolean(this.affichagesFiges)
  oups.writeBoolean(this.marquesAnglesFigees)
  oups.writeBoolean(this.buttonValueAllowed)
  oups.writeBoolean(this.buttonFunctionAllowed)
  this.writePrototypes(oups)
  this.listePr.write(oups)
}

CMathGraphDoc.prototype.getBase64Code = function () {
  const oups = new DataOutputStream()
  this.write(oups)
  return base64Encode(oups.ba, false)
}

CMathGraphDoc.prototype.toString = function () {
  return this.getBase64Code()
}

/**
 * Retourne le blob binaire du doc (pour mutualiser avec MtgCli.saveAs)
 * @returns {Blob}
 */
CMathGraphDoc.prototype.getBlob = function () {
  const oups = new DataOutputStream()
  this.write(oups)
  const ba = oups.ba
  const len = ba.length
  const buffer = new ArrayBuffer(ba.length)
  const dataView = new DataView(buffer)
  for (let i = 0; i < len; i++) {
    dataView.setUint8(i, ba[i])
  }
  return new Blob([buffer], { type: mimeType })
}

/**
 * Sauvegarde la figure courante dans un fichier binaire mgj
 * @param {string} filename Le nom de fichier sans extension
 */
CMathGraphDoc.prototype.saveAs = function (filename) {
  const blob = this.getBlob()
  saveAs(blob, filename + '.' + mtgFileExtension)
  this.isDirty = false
}

/**
 * Ajout version 5.0.
 * Renvoie un coefficient qui servira à agrandir les marques de segment, d'angles et les points
 * pour les écrans de haute résolution.
 *
// Abandonné
// CMathGraphDoc.prototype.coefForPointsAndMarks = function() {
// On considère une figure de taille 600x400 comme la norme et au-delà on applique un coefficient
// proportionnel à l'aire de la figure
/*
  var d = Math.sqrt(this.sizeWindowx*this.sizeWindowy/800000);
  if (d < 1) return 1; else if (d > 2.5) return 2.5; else return d;

// Pour la version js, toujours égal à 1
//  return 1;
// };

/**
 * Fonction changeant l'état du document
 * @param bModeElectron true si l'applicaton est en mode electron
 * @param {boolean} val  true si le document a changé et false sinon
 */
CMathGraphDoc.prototype.setDirty = function (bModeElectron, val) {
  this.isDirty = val
  // Spécial version electron : Si isDirty est true et si app.electron est true
  // on appelle setDocumentDirty pour que l'utilisateur soit prévenu avant de quitter l'appli
  // setDocumentDirty est une fonction de index.html de electron
  if (bModeElectron && val) setDocumentDirty() // eslint-disable-line no-undef
}

/**
 * Fonction renvoyant true si l'outil d'inex index est autorisé par le document
 * @param index
 * @returns {boolean}
 */
CMathGraphDoc.prototype.toolDispo = function (index) {
  if (index === -1) return true
  let b = false
  for (let i = 0; i < this.listeIdMenus.length; i++) {
    b = b || (this.listeIdMenus[i] === index)
  }
  return this.itemsCochesInterdits ? !b : b
}

CMathGraphDoc.prototype.getList = function () {
  return this.listePr
}