reversound/src/gui/graphs/BargramView.java
2014-06-16 16:48:34 +02:00

512 lines
15 KiB
Java

/*
Reversound is used to get the music sheet of a piece from a music file.
Copyright (C) 2014 Gabriel AUGENDRE
Copyright (C) 2014 Gabriel DIENY
Copyright (C) 2014 Arthur GAUCHER
Copyright (C) 2014 Gabriel LEPETIT-AIMON
This program 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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package gui.graphs;
import conteneurs.Gamme;
import conteneurs.NoteGamme;
import conteneurs.RegularGamme;
import generictools.Instant;
import gui.PlayerControl;
import gui.PlayerControlEvent;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import processing.buffer.TemporalBuffer;
import javax.swing.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
/**
* Affiche un graphe en "barres" représentant l'amplitude d'une fréquence
* donnée en fonction du temps.
* Created by arthur on 20/05/14.
*/
public abstract class BargramView<T extends TemporalBuffer<?>> extends AbstractBufferGraph<T>{
protected TraceChart chart;
private StackPane pane;
private Rectangle cursorRect;
private Rectangle highlightedLine;
final int RESOLUTION = 50;
private float windowWidthMs = 5000;
private Instant boundMin = new Instant(0);
protected Gamme yAxisGamme;
/**
* Constructeur
* @param name Le nom du graphe
* @param type Le type de buffer à afficher
*/
public BargramView(String name, Class type){
super(name, type);
}
/**
* Crée tous les éléments de l'affichage
* @return Le panel
*/
Parent createContent(){
//définition des axes
CategoryAxis yAxis = new CategoryAxis();
yAxis.setAutoRanging(false);
yAxis.setTickLabelFont(Font.font(8));
yAxis.setTickLabelRotation(0);
setYAxisGamme(NoteGamme.gamme());
NumberAxis xAxis = new NumberAxis(0, 5000,500);
xAxis.setLabel("Temps (ms)");
//création d'un graphe TraceChart
chart = new TraceChart(xAxis,yAxis);
chart.setAnimated(false);
chart.setVerticalGridLinesVisible(false);
chart.setHorizontalGridLinesVisible(false);
chart.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
//création du curseur
cursorRect = new Rectangle();
cursorRect.setManaged(false);
cursorRect.setFill(Color.rgb(39, 93, 153));
cursorRect.setWidth(1);
cursorRect.setHeight(215);
//création d'une ligne de couleur horizontale pour mettre en
//surbrillance une ligne de fréquence
highlightedLine = new Rectangle();
highlightedLine.setManaged(false);
highlightedLine.setFill((Color.GREEN.deriveColor(0,1,1,0.2)));
highlightedLine.setWidth(150);
highlightedLine.setHeight(4);
highlightedLine.setMouseTransparent(true);
//creation du panel qui contient tous les éléments de l'affichage
pane = new StackPane();
pane.getChildren().add(chart);
pane.getChildren().add(cursorRect);
pane.getChildren().add(highlightedLine);
//quand la largeur de la fenêtre change, déplacer le curseur
pane.widthProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number oldHeight, Number newHeight) {
moveCursor(true);
}
});
//quand la hauteur de la fenêtre change, redimensionner le curseur
pane.heightProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number oldWidth, Number newWidth) {
resizeCursor();
}
});
//quand la souris bouge, redessiner la ligne de surbrillance
pane.setOnMouseMoved(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
highlightLine(selectTickLine(event));
float time = readTime(event);
getOnMouseMoved(readFreqID(event), Instant.fromTime(time - time % (windowWidthMs / RESOLUTION))).run();
}
});
//quand le bouton gauche de la souris est appuyé, changer la frame
pane.setOnMousePressed(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
if (event.isPrimaryButtonDown()) {
setFrame(event);
float time = readTime(event);
getOnMouseClicked(readFreqID(event), Instant.fromTime(time - time % (windowWidthMs / RESOLUTION))).run();
}
}
});
pane.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
if (event.isPrimaryButtonDown()) {
setFrame(event);
float time = readTime(event);
getOnMouseClicked(readFreqID(event), Instant.fromTime(time - time % (windowWidthMs / RESOLUTION))).run();
}
}
});
//avec ctrl+molette, zoomer ou dézoomer
pane.setOnScroll(new EventHandler<ScrollEvent>() {
@Override
public void handle(ScrollEvent scrollEvent) {
if(scrollEvent.isControlDown()){
setWindowWidthMs((float) -scrollEvent.getDeltaY() + windowWidthMs);
}
}
});
//rendre la ligne de surbrillance visible quand la souris rentre
//dans le panel
chart.setOnMouseEntered(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
highlightedLine.setVisible(true);
}
});
//rendre la ligne de surbrillance invisible quand la souris sort
//dans le panel
chart.setOnMouseExited(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
highlightedLine.setVisible(false);
}
});
highlightedLine.setVisible(false);
//donne une série vide à afficher au chart pour éviter les erreurs
chart.setData(FXCollections.observableArrayList(new XYChart.Series()));
return pane;
}
/**
* Définit une nouvelle frame
* @param event L'événement de la souris
*/
private void setFrame(MouseEvent event){
final Instant timeToSet = Instant.fromTime(readTime(event));
if(timeToSet.isGreaterThan(boundMin) && timeToSet.isLowerThan(boundMin.translated(windowWidthMs)))
PlayerControl.instance().setFrame(timeToSet);
}
/**
* Déplace le curseur en fonction de la frame actuelle
* @param locked Booléen indiquant si le curseur doit être suivi
*/
private void moveCursor(final Boolean locked){
final NumberAxis xAxis = (NumberAxis) chart.getXAxis();
final CategoryAxis yAxis = (CategoryAxis) chart.getYAxis();
final double yAxisInScene = yAxis.localToScene(0, 0).getX() + yAxis.getWidth();
if (locked) {
//si la frame est plus grande que la limite sup, translater la
//fenêtre vers la droite
if (PlayerControl.frame().toMs() >= boundMin.toMs()+windowWidthMs)
setBoundMin(boundMin.translated(windowWidthMs));
//si la frame est plus petite que la limite inf, translater la
//fenêtre vers la gauche
if (PlayerControl.frame().toMs() < boundMin.toMs())
setBoundMin(boundMin.translated(-windowWidthMs));
}
Platform.runLater(new Runnable() {
@Override
public void run() {
//positionne le curseur
cursorRect.setX((PlayerControl.frame().toMs() - boundMin.toMs()) * xAxis.getScale() + yAxisInScene);
}
});
}
/**
* Met en surbrillance une ligne horizontale correspondant à une
* valeur sur l'axe y
* @param category La valeur de la donnée sur l'axe y à mettre en
* surbrillance
*/
private void highlightLine(String category) {
Platform.runLater(new Runnable() {
@Override
public void run() {
final CategoryAxis yAxis = (CategoryAxis)chart.getYAxis();
double tickPosition = yAxis.getDisplayPosition(category) + yAxis.localToScene(0,0).getY();
highlightedLine.setY(tickPosition - highlightedLine.getHeight()/2);
resizeHiglightedLine();
}
});
}
/**
* Redimensionne la ligne de surbrillance
*/
private void resizeHiglightedLine(){
final CategoryAxis yAxis = (CategoryAxis)chart.getYAxis();
highlightedLine.setX(0);
highlightedLine.setWidth(pane.getWidth() > 0 ? pane.getWidth() : 100);
highlightedLine.setHeight(yAxis.getHeight() / yAxis.getCategories().size());
}
/**
* Sélectionne la donnée de l'axe y sous le curseur de la souris
* @param event L'événement de la souris
* @return La donnée de l'axe y
*/
private String selectTickLine(MouseEvent event){
final CategoryAxis yAxis = (CategoryAxis)chart.getYAxis();
return yAxis.getValueForDisplay(event.getY() - yAxis.localToScene(0,0).getY());
}
private int readFreqID(MouseEvent event){
final CategoryAxis yAxis = (CategoryAxis)chart.getYAxis();
return yAxis.getCategories().indexOf(yAxis.getValueForDisplay(event.getY() - yAxis.localToScene(0,0).getY()));
}
private float readTime(MouseEvent event){
final NumberAxis xAxis = (NumberAxis)chart.getXAxis();
final CategoryAxis yAxis = (CategoryAxis) chart.getYAxis();
final double yAxisInScene = yAxis.localToScene(0, 0).getX() + yAxis.getWidth();
final double mouseX = event.getX();
return xAxis.getValueForDisplay(mouseX - yAxisInScene).floatValue();
}
/**
* Redimensionne le curseur
*/
private void resizeCursor(){
Platform.runLater(() -> {
final CategoryAxis yAxis = (CategoryAxis) chart.getYAxis();
/*si l'axe y n'est pas complètement initialisé, cela peut arriver
si le graphe met un peu plus de temps à s'initialiser, positionner
le curseur avec des valeurs fixes*/
if (yAxis.getHeight() == 0){
cursorRect.setY(17);
moveCursor(true);
} else {
cursorRect.setY(yAxis.localToScene(0, 0).getY());
cursorRect.setHeight(yAxis.getHeight());
}
});
}
/**
* Définit une limite minimale pour dessiner
* @param bound
*/
private void setBoundMin(Instant bound){
if(bound.toMs() < 0)
bound = Instant.fromTime(0);
boundMin = bound;
applyWindowChange();
}
/**
* Définit la taille de la fenêtre du signal à afficher
* @param windowWidthMs La taille de la fenêtre
*/
private void setWindowWidthMs(float windowWidthMs){
this.windowWidthMs = windowWidthMs;
applyWindowChange();
}
/**
* Appelle setWindow() de BufferViewerState
*/
private void applyWindowChange(){
final NumberAxis xAxis = (NumberAxis)chart.getXAxis();
xAxis.setLowerBound(boundMin.toMs());
xAxis.setUpperBound(boundMin.toMs()+windowWidthMs);
cleanView();
state().setWindow(boundMin.mapToIndex(buffer()), boundMin.translated(windowWidthMs).mapToIndex(buffer()));
}
/**
* Définit la gamme qui peut alterner entre regular et chromaatique
* @param yAxisGamme La gamme
*/
public void setYAxisGamme(Gamme yAxisGamme){
this.yAxisGamme = yAxisGamme;
Platform.runLater(new Runnable(){
@Override
public void run() {
changeYAxis();
state().invalidate();
}
});
}
public Gamme getGamme(){
return yAxisGamme;
}
/**
* Alterne la gamme à afficher entre regular et chromatic
*/
private void changeYAxis(){
CategoryAxis yAxis = (CategoryAxis)chart.getYAxis();
ArrayList<String> categories = new ArrayList();
for(int i=0 ; i<yAxisGamme.gammeSize() ; i++){
if(yAxisGamme instanceof NoteGamme) {
categories.add(((NoteGamme)yAxisGamme).getNoteName(i));
} else {
categories.add(String.valueOf(yAxisGamme.getFreq(i)));
}
}
yAxis.setCategories(FXCollections.<String>observableArrayList(categories));
yAxis.invalidateRange(categories);
}
public void playerControlEvent(PlayerControlEvent e) {
if(!visibility())
return;
switch (e.getType()) {
case FRAME:
moveCursor(true);
break;
}
}
@Override
public Parent getMainNode() {
return pane;
}
/**
* Revoie la liste des éléments du menu contextuel
*
* @param contextMenu Le menu
*/
@Override
public void createContextMenu(JPopupMenu contextMenu) {
JMenu choixAxesX = new JMenu("Axe fréquence");
JRadioButtonMenuItem noteGamme = new JRadioButtonMenuItem("Gamme chromatique");
noteGamme.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange()==ItemEvent.SELECTED)
setYAxisGamme(NoteGamme.gamme());
}
});
JRadioButtonMenuItem regularGamme = new JRadioButtonMenuItem("Gamme linéaire");
regularGamme.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange()==ItemEvent.SELECTED)
setYAxisGamme(new RegularGamme(NoteGamme.getMaxFreq(), NoteGamme.getMaxFreq(), 1000));
}
});
ButtonGroup b = new ButtonGroup();
b.add(noteGamme);
b.add(regularGamme);
choixAxesX.add(noteGamme);
choixAxesX.add(regularGamme);
contextMenu.add(choixAxesX);
}
@Override
protected void initView() {
createContent();
cleanView();
state().setSlowingFactorOnDataChange(5);
setBoundMin(new Instant(0));
}
@Override
public void updateView(){
XYChart.Series series = new XYChart.Series<>();
ObservableList<String> categories = ((CategoryAxis)chart.getYAxis()).getCategories();
final float INC = windowWidthMs/RESOLUTION;
// On parcours la fenêtre selon le temps et si l'échantillon à l'instant considéré est non nul ...
for (float time = boundMin.toMs(); time <= boundMin.toMs()+windowWidthMs ; time+=INC) {
if (buffer().get(time) != null) {
//On parcours l'échantillon fréquence par fréquence
for (int idFreq = 0; idFreq < categories.size(); idFreq++) {
Color color = getColor(idFreq, Instant.fromTime(time));
if (color != null && (color.getBrightness() < .95 ||color.getSaturation() > .05 )) {
series.getData().add(new XYChart.Data(time, categories.get(idFreq),
new BarExtraValues(time+INC, color)));
}
}
}
}
Platform.runLater(new Runnable() {
@Override
public void run() {
chart.getData().set(0, series);
}
});
}
protected abstract Color getColor(int idFreq, Instant time);
protected Runnable getOnMouseMoved(int idFreq, Instant time){return new Runnable() {
@Override
public void run() {
}
};}
protected Runnable getOnMouseClicked(int idFreq, Instant time){return new Runnable() {
@Override
public void run() {
}
};}
@Override
public void cleanView() {
Platform.runLater(new Runnable() {
@Override
public void run() {
chart.setData(FXCollections.observableArrayList(new XYChart.Series()));
}
});
}
}