/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Copyright (C) 2011-2013 Marchand Eric <ricoh51@free.fr>
This file is part of Freegressi.
Freegressi is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Freegressi is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Freegressi. If not, see <http://www.gnu.org/licenses/>.
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
package freegressi.fit;
import freegressi.fit.Fit.TendanceType;
import freegressi.fit.functions.*;
import freegressi.graphics.GraphicModel;
import freegressi.graphics.GraphicModel.GraphicChangedEvent;
import freegressi.graphics.GraphicModel.GraphicSizeChangedEvent;
import freegressi.graphics.GraphicModel.GraphicStyleChangedEvent;
import freegressi.parser.Verificateur;
import freegressi.tableur.SpreadSheets.ActiveSheetChangedEvent;
import freegressi.tableur.SpreadSheets.SheetAddedEvent;
import freegressi.tableur.SpreadSheets.SheetDeletedEvent;
import freegressi.tableur.SpreadSheets.SheetDescriptionChangedEvent;
import freegressi.tableur.SpreadSheets.SheetsAngleChangedEvent;
import freegressi.tableur.SpreadSheets.SheetsCSChangedEvent;
import freegressi.tableur.SpreadSheets.SheetsColumnAddedEvent;
import freegressi.tableur.SpreadSheets.SheetsColumnDeletedEvent;
import freegressi.tableur.SpreadSheets.SheetsColumnModifiedEvent;
import freegressi.tableur.SpreadSheets.SheetsColumnMovedEvent;
import freegressi.tableur.SpreadSheets.SheetsColumnSortedEvent;
import freegressi.tableur.SpreadSheets.SheetsColumnsEditedEvent;
import freegressi.tableur.SpreadSheets.SheetsListener;
import freegressi.tableur.SpreadSheets.SheetsRenamedEvent;
import freegressi.tableur.*;
import freegressi.tableur.Tableur.SheetCellChangedEvent;
import freegressi.tableur.Tableur.SheetLinesAddedEvent;
import freegressi.tableur.Tableur.SheetLinesDeletedEvent;
import freegressi.tableur.Tableur.TableurChangeListener;
import freegressi.utils.Utils;
import jaolho.data.lma.LMA;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import javax.swing.event.EventListenerList;
/**
*
* @author marchand
*/
public class FitModel implements TableurChangeListener,
SheetsListener, GraphicModel.GraphicListener{
/** La liste des écouteurs */
private EventListenerList listeners = new EventListenerList();
/** Le nombre de pixels de large du graphe */
private int nombrePixels = 0;
/** La liste des tendances du JPanelTendance */
private List<Fit> listeTendance = new ArrayList<>();
/** Le nombre de chiffres significatifs dans l'expression du résultat */
private int nombreCS = 2;
private int numeroTendance = 0;
private static FitModel tendanceModel;
/**
* Constructeur privé pour singleton
* @param tableur
*/
private FitModel() {
}
/**
* Return l'instance du singleton
* @return
*/
public static FitModel getInstance() {
if (tendanceModel == null) {
tendanceModel = new FitModel();
}
return tendanceModel;
}
public int donneProchainNumeroTendance() {
numeroTendance++;
return numeroTendance;
}
public void setTableur(Tableur tableur) {
//this.tableur = tableur;
}
public List<Fit> getListTendances(){
return listeTendance;
}
// /**
// * Le controller nous informe qu'une variable vient d'être supprimée
// * Il faut alors supprimer les tendances et les courbes associées à
// * cette variable
// * @param tableur
// */
// public void variableSupprimee(Tableur tableur, String nom) {
// for (Fit tendance : listeTendance) {
// if (tendance.getNomX().equals(nom) || tendance.getNomY().equals(nom)) {
// fireTendanceDeleted(tendance, true);
// }
// }
// }
/**
* Définit le nombre de pixel de large du graphique, utile pour calculer
* le tableau de valeurs des courbes de tendance
* @param nombrePixels
*/
public void setNombrePixels(int nombrePixels) {
this.nombrePixels = nombrePixels;
System.out.println("Nombre pixels : " + nombrePixels);
// recalculer la liste de points des courbes de tendance
for (Fit tendance : listeTendance) {
calculeListePointsTendance(tendance);
// et informer les écouteurs
if (tendance != null) {
fireTendanceChanged(tendance, tendance, false);
}
}
}
// @Override
// public void tailleChangee(GraphiqueTailleChangeeEvent event) {
// //throw new UnsupportedOperationException("Not supported yet.");
// //@TODO à Implémenter
// setNombrePixels(event.getGraphique().getLargeur());
// }
//
// @Override
// public void doitSeRepeindre(GraphiqueDoitSeRepeindreEvent event) {
// // ne rien faire, cet évenement est pour les graphiques
// }
//
// @Override
// public void graphicStyleChanged(GraphicStyleChangedEvent event) {
// //throw new UnsupportedOperationException("Not supported yet.");
// }
@Override
public void activeSheetChanged(ActiveSheetChangedEvent event) {
//throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void sheetAdded(SheetAddedEvent event) {
//throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void sheetDeleted(SheetDeletedEvent event) {
// Supprimer toutes les tendances s'il n'y a plus de feuille
if (SpreadSheets.getInstance().getList().isEmpty()){
for (int i = listeTendance.size() - 1; i > -1; i--){
notifySupprimeTendance(listeTendance.get(i));
}
}
}
@Override
public void columnAdded(SheetsColumnAddedEvent event) {
// throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void columnDeleted(SheetsColumnDeletedEvent event) {
//throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void sheetRenamed(SheetsRenamedEvent event) {
//throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void angleChanged(SheetsAngleChangedEvent event) {
//throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void columnModified(SheetsColumnModifiedEvent event) {
//throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void columnSorted(SheetsColumnSortedEvent event) {
//throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void columnMoved(SheetsColumnMovedEvent event) {
//throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void columnsEdited(SheetsColumnsEditedEvent event) {
//throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void sheetDescriptionChanged(SheetDescriptionChangedEvent event) {
// throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void sheetsCSChanged(SheetsCSChangedEvent event) {
nombreCS = event.getNewCS();
}
@Override
public void graphicSizeChanged(GraphicSizeChangedEvent event) {
nombrePixels = event.getNewWidth();
}
@Override
public void graphicChanged(GraphicChangedEvent event) {
//throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void graphicStyleChanged(GraphicStyleChangedEvent event) {
//throw new UnsupportedOperationException("Not supported yet.");
}
/* **********************************************************************
*
* Les calculs
*
*********************************************************************** */
private int indexParametre = -1;
public void setIndexParameter(int index){
indexParametre = index;
}
// pas chouette, ce sont des calculs, ils devraient être dans le tableur
// et c'est spécifique aux tendances, ça a donc sa place ici...
// et le code est redondant avec calculeNoeud de tableur!
public double calculeTendance(Noeud noeud, double x, double[] a) {
//@TODO Vérifier les calculs, redondant
int type = noeud.getType();
Noeud filsG = noeud.getFilsG();
Noeud filsD = noeud.getFilsD();
// si c'est un ID, c'est forcément x
if (type == Sym.ID) {
return x;
}
if (type == Sym.PARAMETRE) {
indexParametre++;
return a[indexParametre];
}
if (type == Sym.NOMBRE) {
return noeud.getValeur();
}
double d1 = (filsG == null) ? 0 : calculeTendance(filsG, x, a);
double d2 = (filsD == null) ? 0 : calculeTendance(filsD, x, a);
switch (type) {
case Sym.PLUS:
return d1 + d2;
case Sym.MOINS:
return d1 - d2;
case Sym.FOIS:
return d1 * d2;
case Sym.DIVISE:
return d1 / d2;
case Sym.SIN:
return Math.sin(d2);
case Sym.COS:
return Math.cos(d2);
case Sym.TAN:
return Math.tan(d2);
case Sym.ASIN:
return Math.asin(d2);
case Sym.ACOS:
return Math.acos(d2);
case Sym.ATAN:
return Math.atan(d2);
case Sym.LOG:
return Math.log10(d2);
case Sym.LN:
return Math.log(d2);
case Sym.EXP:
return Math.exp(d2);
case Sym.PUIS:
return Math.pow(d1, d2);
case Sym.PUISN:
return Math.pow(d1, d2);
case Sym.PI:
return Math.PI;
default: {
System.err.println("Erreur de calcul (calculeTendance)!! (pas d'opération définie) : " + type);
return Double.NaN;
}
}
}
/**
* Calcule la tendance et remplit les champs :
* - resultat
* - ecart
* - noeud
* - listeX
* - listeY
*/
private void calculerTendance(Fit tendance) {
if (tendance == null) {
System.err.println("Tendance null à calculer!!");
return;
}
Tableur tableur = tendance.getTableur();
FreegressiFunction func = null;
String nomY = tendance.getNomY();
String nomX = tendance.getNomX();
TendanceType type = tendance.getType();
double params[];
boolean ok = true;
Noeud racineManuelle;
switch (type) {
case LINEAR: func = new LinearFunc(); break;
case AFFINE: func = new AffineFunc(); break;
case POLYNOMIAL: func = new PolyFunc(tendance.getDegrePolynome()); break;
case ANTOINE1: func = new Antoine1Func(); break;
case EXPO1: func = new Expo1Func(); break;
case EXPO2: func = new Expo2Func(); break;
case HAND:
racineManuelle = Verificateur.parseExpressionTendance(tendance.getExpression(), nomX);
if (racineManuelle != null && racineManuelle.getType() != Sym.ERROR
&& racineManuelle.getType() != Sym.PARSER_COHERENCE_ERROR
&& racineManuelle.getType() != Sym.PARSER_ERROR) {
func = new AnyFunc(racineManuelle, tendance.getExpression());
} else {
ok = false;
}
break;
default:
break;
}
int nombreParams = func.getParamNumber();
if (ok) {
params = new double[nombreParams];
for (int i = 0; i < params.length; i++) {
params[i] = 1;
}
double[][] tabXY = tableur.donneTableau(nomX, nomY);
LMA lma = new LMA(func, params, tabXY);
lma.verbose = true;
try {
lma.fit();
double[] vraie = tabXY[1];
double[] abcisse = tabXY[0];
double[] fausse = func.generateData(abcisse, lma.parameters);
double erreur = Stats.getErreur(vraie, fausse);
String str = func.getTextToDisplay(lma.parameters, nomX, nombreCS);
tendance.setResultat(nomY + " = " + str);
Noeud noeud = func.getNode(tableur, lma.parameters, nomX);
tendance.setNoeud(noeud);
tendance.setEcart(Utils.formatteNombre(erreur, 2) + " %");
calculeListePointsTendance(tendance);
} catch (Exception e) {
tendance.setResultat("Données non valides");
tendance.setEcart("Données non valides");
}
}
}
/**
* Calcule deux listes de valeurs listeX et listeY (des double) qui
* permettront de tracer la courbe de tendance.
* Ce calcul se base sur la largeur du graphique (nombrePixels) et, pour
* gagner du temps d'affichage :
* - ne calcule que deux points pour les droites
* - calcule un point tous les 10 pixels
* @param tendance la tendance à calculer
*/
private void calculeListePointsTendance(Fit tendance) {
if (tendance == null || nombrePixels < 2) {
return;
}
// @TODO Ne calculer que deux points dans le cas des droites
// récupérer les bornes de l'axe x
double x1 = tendance.getMinX();
double x2 = tendance.getMaxX();
System.out.println("x1 = " + x1 + " ; x2 = " + x2);
int nombrePoints = nombrePixels / 10;
double pas = (x2 - x1) / (nombrePoints - 1);
double x, y;
Noeud noeud = tendance.getNoeud();
ArrayList<Double> listeX = new ArrayList<>();
ArrayList<Double> listeY = new ArrayList<>();
for (int i = 0; i < nombrePoints; i++) {
x = x1 + i * pas;
listeX.add(x);
y = calculeTendance(noeud, x);
listeY.add(y);
}
tendance.setListeX(listeX);
tendance.setListeY(listeY);
}
/**
* Renvoit une valeur, calculée à l'aide de l'entrée x
* et du noeud de calcul noeud
* @param noeud
* @param x
* @return
*/
public double calculeTendance(Noeud noeud, double x) {
// @TODO A VIRER
int type = noeud.getType();
Noeud filsG = noeud.getFilsG();
Noeud filsD = noeud.getFilsD();
// si c'est un ID, c'est forcément x
if (type == Sym.ID) {
return x;
}
// if (type == Sym.PARAMETRE) {
// indexParametre++;
// return a[indexParametre];
// }
if (type == Sym.NOMBRE) {
return noeud.getValeur();
}
double d1 = (filsG == null) ? 0 : calculeTendance(filsG, x);
double d2 = (filsD == null) ? 0 : calculeTendance(filsD, x);
switch (type) {
case Sym.PLUS:
return d1 + d2;
case Sym.MOINS:
return d1 - d2;
case Sym.FOIS:
return d1 * d2;
case Sym.DIVISE:
return d1 / d2;
case Sym.SIN:
return Math.sin(d2);
case Sym.COS:
return Math.cos(d2);
case Sym.TAN:
return Math.tan(d2);
case Sym.ASIN:
return Math.asin(d2);
case Sym.ACOS:
return Math.acos(d2);
case Sym.ATAN:
return Math.atan(d2);
case Sym.LOG:
return Math.log10(d2);
case Sym.LN:
return Math.log(d2);
case Sym.EXP:
return Math.exp(d2);
case Sym.PUIS:
return Math.pow(d1, d2);
case Sym.PUISN:
return Math.pow(d1, d2);
case Sym.PI:
return Math.PI;
default: {
System.err.println("Erreur de calcul!! (pas d'opération définie) : " + type);
return Double.NaN;
}
}
}
/* **********************************************************************
*
* Gestion des évenements
*
*********************************************************************** */
/**
* Ajoute un écouteur à la liste
* @param listener l'écouteur à ajouter
*/
public final void addTendanceListener(FitListener listener) {
listeners.add(FitListener.class, listener);
}
/**
* Retire un écouteur de la liste
* @param listener l'écouteur à retirer
*/
public void removeTendanceListener(FitListener listener) {
listeners.remove(FitListener.class, listener);
}
private void recalculeToutesLesTendances() {
System.out.println("********** recalculeToutesLesTendances");
for (Fit tendance : listeTendance) {
// Mettre à jour le minX et le maxX de chaque tendance
String nomX = tendance.getNomX();
Colonne colonneX = tendance.getTableur().donneColonne(nomX);
tendance.setMinX(colonneX.getMin());
tendance.setMaxX(colonneX.getMax());
String nomY = tendance.getNomY();
Colonne colonneY = tendance.getTableur().donneColonne(nomY);
tendance.setMinY(colonneY.getMin());
tendance.setMaxY(colonneY.getMax());
calculerTendance(tendance);
fireTendanceChanged(tendance, tendance, false);
}
}
@Override
public void sheetCellChanged(SheetCellChangedEvent event) {
recalculeToutesLesTendances();
}
@Override
public void sheetLinesAdded(SheetLinesAddedEvent event) {
recalculeToutesLesTendances();
}
@Override
public void sheetLinesDeleted(SheetLinesDeletedEvent event) {
recalculeToutesLesTendances();
}
// @Override
// public void bornesTendanceChangees(GraphiqueBornesTendanceChangeesEvent event) {
// throw new UnsupportedOperationException("Not supported yet.");
// }
/* **********************************************************************
*
* Creation d'une regression
*
*********************************************************************** */
/**
*
* @author marchand
*/
public class TendanceCreatedEvent extends EventObject {
private Fit newTendance;
private final boolean undoable;
public TendanceCreatedEvent(Object source, Fit newTendance, boolean undoable) {
super(source);
this.newTendance = newTendance;
this.undoable = undoable;
}
public Fit getNewTendance() {
return newTendance;
}
public boolean isUndoable() {
return undoable;
}
public String getTexte() {
return "Ajouter une regression " + newTendance.getNomY() + "=f(" + newTendance.getNomX() + ")";
}
}
private Fit createNewTendance(Tableur tableur){
// @TODO vérifier qu'il y a deux colonnes
String nomY = tableur.getListeColonnes().get(1).getColonneDescription().getNom();
String nomX = tableur.getListeColonnes().get(0).getColonneDescription().getNom();
Fit tendance = new Fit(tableur, donneProchainNumeroTendance(),
false, TendanceType.LINEAR,
nomY,
nomX,
null, null,
null,
null, null,
null,
tableur.donneColonne(nomX).getMin(),
tableur.donneColonne(nomX).getMax(),
tableur.donneColonne(nomY).getMin(),
tableur.donneColonne(nomY).getMax(),
nombrePixels, 2, true, null, true);
return tendance;
}
/**
* Une tendance vient d'être ajoutée, il faut la calculer et
* informer les écouteurs
* @param newTendance la nouvelle tendance
*/
public void notifyAjouteTendance(Tableur tableur) {
Fit newTendance = createNewTendance(tableur);
listeTendance.add(newTendance);
calculerTendance(newTendance);
fireTendanceCreated(newTendance, true);
//System.out.println("Creer tendance");
}
/**
* Envoit du message "Une tendance vient d'être créée" à tous les écouteurs
* @param tendance la tenace qui vient d'être créée
*/
private void fireTendanceCreated(Fit tendance, boolean undoable) {
FitListener[] listenerList = (FitListener[]) listeners.getListeners(FitListener.class);
for (FitListener listener : listenerList) {
listener.tendanceCreated(new TendanceCreatedEvent(this, tendance, undoable));
}
}
public void undoRegressionAdded(TendanceCreatedEvent event) {
Fit tendance = event.getNewTendance();
listeTendance.remove(tendance);
// supprimer cet écouteur
FitListener tendanceListener = tendance.getTendanceListener();
removeTendanceListener(tendanceListener);
// Informer les écouteurs
fireTendanceDeleted(tendance, false);
}
public void redoRegressionAdded(TendanceCreatedEvent event) {
Fit tendance = event.getNewTendance();
listeTendance.add(tendance);
//addTendanceListener(tendance.getTendanceListener()); //@TODO A VERIFIER
calculerTendance(tendance);
fireTendanceCreated(tendance, false);
}
/* **********************************************************************
*
* Destruction d'une regression
*
*********************************************************************** */
/**
*
* @author marchand
*/
public class TendanceDeletedEvent extends EventObject {
private Fit tendance;
private final boolean undoable;
public TendanceDeletedEvent(Object source, Fit tendance, boolean undoable) {
super(source);
this.tendance = tendance;
this.undoable = undoable;
}
public boolean isUndoable() {
return undoable;
}
public Fit getTendance() {
return tendance;
}
public String getTexte() {
return "Supprimer la regression " + tendance.getNomY() + "=f(" + tendance.getNomX() + ")";
}
}
/**
* Une tendance doit être supprimée, on la retire de la liste,
* on informe les écouteurs et on retire l'écouteur associé de la
* liste des écouteurs
* @param tendance la tendance à retirer
* @param tendanceListener l'écouteur de cette tendance
*/
public void notifySupprimeTendance(Fit tendance) {
listeTendance.remove(tendance);
// supprimer cet écouteur
FitListener tendanceListener = tendance.getTendanceListener();
removeTendanceListener(tendanceListener);
// Informer les écouteurs
fireTendanceDeleted(tendance, true);
}
/**
* Envoit du message "Une tendance vient d'être détruite" à tous les écouteurs
* @param tendance la tendance détruite
*/
private void fireTendanceDeleted(Fit tendance, boolean undoable) {
FitListener[] listenerList = (FitListener[]) listeners.getListeners(FitListener.class);
for (FitListener listener : listenerList) {
listener.tendanceDeleted(new TendanceDeletedEvent(this, tendance, undoable));
}
}
public void undoRegressionDeleted(TendanceDeletedEvent event) {
Fit tendance = event.getTendance();
listeTendance.add(tendance);
//addTendanceListener(tendance.getTendanceListener()); //@TODO A VERIFIER
calculerTendance(tendance);
fireTendanceCreated(tendance, false);
}
public void redoRegressionDeleted(TendanceDeletedEvent event) {
Fit tendance = event.getTendance();
listeTendance.remove(tendance);
// supprimer cet écouteur
FitListener tendanceListener = tendance.getTendanceListener();
removeTendanceListener(tendanceListener);
// Informer les écouteurs
fireTendanceDeleted(tendance, false);
}
/* **********************************************************************
*
* Modification d'une regression
*
*********************************************************************** */
/**
* Classe qui définit l'évenement :
* - tendance changée
* @author marchand
*/
public class TendanceChangedEvent extends EventObject {
private final Fit newTendance;
private final boolean undoable;
private final Fit oldTendance;
public TendanceChangedEvent(Object source, Fit oldTendance,
Fit newTendance, boolean undoable) {
super(source);
this.newTendance = newTendance;
this.undoable = undoable;
this.oldTendance = oldTendance;
}
public Fit getNewTendance() {
return newTendance;
}
public Fit getOldTendance() {
return oldTendance;
}
public boolean isUndoable() {
return undoable;
}
public String getTexte() {
return "Changer la regression " + oldTendance.getNomY() + "=f(" + oldTendance.getNomX() + ")";
}
}
/**
* Envoit du message "Une tendance a changé" à tous les écouteurs
* @param tendance la tendance qui a changé
*/
private void fireTendanceChanged(Fit oldTendance, Fit newTendance, boolean undoable) {
FitListener[] listenerList = (FitListener[]) listeners.getListeners(FitListener.class);
for (FitListener listener : listenerList) {
listener.tendanceChanged(new TendanceChangedEvent(this, oldTendance, newTendance, undoable));
}
}
private void deleteRegressionByNumber(int number){
for (Fit t: listeTendance){
if (t.getNumero() == number){
listeTendance.remove(t);
break;
}
}
}
/**
* Un des paramètres de la tendance a été modifiée, il
* faut la recalculer et informer les écouteurs
* @param tendance la tendance modifiée
*/
public void notifyModifieTendance(Fit oldTendance, Fit newTendance) {
// supprimer la tendance possedant le même numero
int numero = oldTendance.getNumero();
deleteRegressionByNumber(numero);
// Ajouter cette tendance
listeTendance.add(newTendance);
// ici je dois recalculer la nouvelle tendance
calculerTendance(newTendance);
// et informer les écouteurs
fireTendanceChanged(oldTendance, newTendance, true);
System.out.println("Modifie tendance");
}
public void undoRegressionChanged(TendanceChangedEvent event){
Fit oldTendance = event.getOldTendance();
Fit newTendance = event.getNewTendance();
// supprimer la newTendance
listeTendance.remove(newTendance);
// recalculer et retablir la old
calculerTendance(oldTendance);
listeTendance.add(oldTendance);
fireTendanceChanged(newTendance, oldTendance, false);
}
public void redoRegressionChanged(TendanceChangedEvent event){
Fit oldTendance = event.getOldTendance();
Fit newTendance = event.getNewTendance();
// supprimer la oldTendance
boolean b = listeTendance.remove(oldTendance);
// recalculer et retablir la new
calculerTendance(newTendance);
listeTendance.add(newTendance);
fireTendanceChanged(oldTendance, newTendance, false);
}
public String toXML(String tab) {
String tab2 = tab + "\t";
String str = "";
for (Fit tendance : listeTendance) {
str += tendance.toXML(tab2);
}
return str;
}
}