/*
* Copyright (C) 2015 Adrien Guille <adrien.guille@univ-lyon2.fr>
*
* 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 main.java.fr.ericlab.sondy.core.ui;
import java.time.Instant;
import java.util.Date;
import main.java.fr.ericlab.sondy.algo.eventdetection.EventDetectionMethod;
import main.java.fr.ericlab.sondy.core.app.AppParameters;
import main.java.fr.ericlab.sondy.core.app.Main;
import main.java.fr.ericlab.sondy.core.structures.Event;
import main.java.fr.ericlab.sondy.core.structures.Events;
import main.java.fr.ericlab.sondy.algo.Parameter;
import main.java.fr.ericlab.sondy.core.ui.factories.EventTableContextMenu;
import main.java.fr.ericlab.sondy.core.utils.ArrayUtils;
import main.java.fr.ericlab.sondy.core.utils.UIUtils;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.Separator;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.layout.VBoxBuilder;
import javafx.scene.paint.Color;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.EllipseBuilder;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import main.java.fr.ericlab.sondy.core.structures.Message;
import org.reflections.Reflections;
/**
*
* @author Adrien GUILLE, Laboratoire ERIC, Université Lumière Lyon 2
*/
public class EventDetectionUI {
public GridPane grid;
// Methods
ListView<String> methodList;
// - parameters
TableView<Parameter> parameterTable;
HashMap<String,String> methodMap;
// - apply
Label methodDescriptionLabel;
Button applyButton;
EventDetectionMethod selectedMethod;
// Detected events
// - filter
TextField filterEventsField;
// - list
TableView<Event> eventTable = new TableView<>();
Events eventList = new Events();
// - frequency chart
NumberAxis xAxis;
NumberAxis yAxis;
LineChart<Number,Number> frequencyChart;
int maxNumberOfCurves = 3;
Rectangle rectangleSelection;
public EventDetectionUI(){
// Initializing the main grid
grid = new GridPane();
grid.setPadding(new Insets(5, 5, 5, 5));
// Adding separators
grid.add(new Text("Available methods"),0,0);
grid.add(new Separator(),0,1);
grid.add(new Text("Detected events"),0,3);
grid.add(new Separator(),0,4);
availabeMethodsUI();
detectedEventsUI();
}
public final void availabeMethodsUI(){
initializeAvailableMethodList();
methodDescriptionLabel = new Label("Selected method description");
methodDescriptionLabel.setId("smalltext");
UIUtils.setSize(methodDescriptionLabel,Main.columnWidthLEFT,24);
VBox methodsLEFT = new VBox();
methodsLEFT.getChildren().addAll(methodList,new Rectangle(0,3),methodDescriptionLabel);
// Right part
applyButton = createApplyMethodButton();
UIUtils.setSize(applyButton, Main.columnWidthRIGHT, 24);
parameterTable = new TableView<>();
UIUtils.setSize(parameterTable, Main.columnWidthRIGHT, 64);
initializeParameterTable();
VBox methodsRIGHT = new VBox();
methodsRIGHT.getChildren().addAll(parameterTable,new Rectangle(0,3),applyButton);
// Both parts
HBox methodsBOTH = new HBox(5);
methodsBOTH.getChildren().addAll(methodsLEFT,methodsRIGHT);
grid.add(methodsBOTH,0,2);
}
public final void detectedEventsUI(){
// Detected events
HBox detectedEventsBOTH = new HBox(0);
initializeEventTable();
initializeFrequencyChart();
VBox detectedEventsRIGHT = new VBox(3);
filterEventsField = new TextField();
filterEventsField.setPromptText("Filter events");
UIUtils.setSize(filterEventsField, Main.columnWidthRIGHT, 24);
final EventHandler<KeyEvent> enterPressed =
new EventHandler<KeyEvent>() {
@Override
public void handle(final KeyEvent keyEvent) {
if(keyEvent.getCode()==KeyCode.ENTER){
eventTable.getItems().clear();
selectedMethod.events.filterList(filterEventsField.getText());
eventTable.setItems(selectedMethod.events.observableList);
}
}
};
filterEventsField.setOnKeyReleased(enterPressed);
detectedEventsRIGHT.getChildren().addAll(filterEventsField,eventTable,createTimelineButton());
detectedEventsBOTH.getChildren().addAll(frequencyChart,detectedEventsRIGHT);
grid.add(detectedEventsBOTH,0,5);
rectangleSelection = new Rectangle(0,240);
rectangleSelection.setOpacity(0.22);
rectangleSelection.setTranslateY(-28);
grid.add(rectangleSelection,0,5);
}
public final void initializeAvailableMethodList(){
methodMap = new HashMap<>();
methodList = new ListView<>();
methodList.setOrientation(Orientation.HORIZONTAL);
UIUtils.setSize(methodList,Main.columnWidthLEFT,64);
updateAvailableMethods();
methodList.getSelectionModel().selectedItemProperty().addListener((ObservableValue<? extends String> ov, String old_val, String new_val) -> {
try {
selectedMethod = (EventDetectionMethod) Class.forName(methodMap.get(new_val)).newInstance();
methodDescriptionLabel.setText(selectedMethod.getDescription());
parameterTable.setItems(selectedMethod.parameters.list);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException ex) {
Logger.getLogger(EventDetectionUI.class.getName()).log(Level.SEVERE, null, ex);
}
});
}
public final void initializeParameterTable(){
parameterTable.setEditable(true);
TableColumn keyColumn = new TableColumn("Parameter");
keyColumn.setMinWidth(Main.columnWidthRIGHT/2-1);
keyColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
TableColumn valueColumn = new TableColumn("Value");
valueColumn.setMinWidth(Main.columnWidthRIGHT/2-1);
valueColumn.setCellFactory(TextFieldTableCell.forTableColumn());
valueColumn.setCellValueFactory(new PropertyValueFactory<>("value"));
valueColumn.setOnEditCommit(
new EventHandler<CellEditEvent<Parameter, String>>() {
@Override
public void handle(CellEditEvent<Parameter, String> t) {
((Parameter) t.getTableView().getItems().get(t.getTablePosition().getRow())).setValue(t.getNewValue());
}
}
);
parameterTable.getColumns().addAll(keyColumn,valueColumn);
}
public final void initializeEventTable(){
eventTable = new TableView<>();
eventTable.setItems(eventList.observableList);
UIUtils.setSize(eventTable, Main.columnWidthRIGHT, 247);
TableColumn textualDescription = new TableColumn("Textual desc.");
textualDescription.setMinWidth(Main.columnWidthRIGHT/2);
TableColumn temporalDescription = new TableColumn("Temporal desc.");
temporalDescription.setMinWidth(Main.columnWidthRIGHT/2-1);
textualDescription.setCellValueFactory(new PropertyValueFactory<>("textualDescription"));
temporalDescription.setCellValueFactory(new PropertyValueFactory<>("temporalDescription"));
EventTableContextMenu tableCellFactory = new EventTableContextMenu(createSelectedEventHandler(), new ContextMenu());
textualDescription.setCellFactory(tableCellFactory);
eventTable.getColumns().addAll(textualDescription,temporalDescription);
}
public final Button createApplyMethodButton(){
Button button = new Button("Apply method");
button.setOnAction((ActionEvent ae) -> {
try {
if(!AppParameters.dataset.corpus.preprocessing.equals("")){
AppParameters.disableUI(true);
eventTable.getItems().clear();
frequencyChart.getData().clear();
rectangleSelection.setWidth(0);
LogUI.addLogEntry("Running '"+selectedMethod.getName()+"'...");
final Task<String> waitingTask = new Task<String>() {
@Override
public String call() throws Exception {
Thread job = new Thread(selectedMethod);
job.start();
job.join();
eventList = selectedMethod.events;
return "" ;
}
};
waitingTask.setOnSucceeded((WorkerStateEvent event1) -> {
eventTable.getItems().clear();
eventTable.getItems().addAll(eventList.observableList);
AppParameters.disableUI(false);
LogUI.addLogEntry("Done: "+selectedMethod.getLog());
});
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(waitingTask);
}else{
LogUI.addLogEntry("Error: no dataset loaded");
}
} catch (Exception ex) {
Logger.getLogger(EventDetectionUI.class.getName()).log(Level.SEVERE, null, ex);
}
});
return button;
}
public final void initializeFrequencyChart(){
xAxis = new NumberAxis(0,1,1);
yAxis = new NumberAxis();
xAxis.setTickLength(5);
yAxis.setTickLabelsVisible(false);
yAxis.setTickMarkVisible(false);
xAxis.setLabel("Time (days)");
frequencyChart = new LineChart<>(xAxis,yAxis);
frequencyChart.setLegendVisible(true);
frequencyChart.setCreateSymbols(false);
frequencyChart.setTranslateX(-5);
UIUtils.setSize(frequencyChart, Main.columnWidthLEFT+5, 300);
}
public final Button createTimelineButton(){
Button button = new Button("Generate timeline");
UIUtils.setSize(button,Main.columnWidthRIGHT,24);
button.setOnAction((ActionEvent ae) -> {
createTimelineStage();
});
return button;
}
public final void updateAvailableMethods(){
Reflections reflections = new Reflections("main.java.fr.ericlab.sondy.algo.eventdetection");
Set<Class<? extends EventDetectionMethod>> classes = reflections.getSubTypesOf(EventDetectionMethod.class);
for(Class<? extends EventDetectionMethod> aClass : classes){
try {
EventDetectionMethod method = (EventDetectionMethod) Class.forName(aClass.getName()).newInstance();
methodList.getItems().add(method.getName());
methodMap.put(method.getName(),aClass.getName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException ex) {
Logger.getLogger(EventDetectionUI.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
public final void updateFrequencyChart(){
xAxis.setTickUnit(0.5);
xAxis.setUpperBound(AppParameters.dataset.corpus.getLength());
frequencyChart.getData().clear();
String[] terms = AppParameters.event.getTextualDescription().split(" ");
for(int j = 0; j < maxNumberOfCurves && j < terms.length; j++){
short[] frequency = AppParameters.dataset.corpus.getTermFrequency(terms[j]);
int windowSize = AppParameters.dataset.corpus.messageDistribution.length/Main.columnWidthLEFT;
float[] smoothedFrequency;
if(windowSize > 1){
smoothedFrequency = ArrayUtils.smoothArray(frequency, windowSize);
}else{
smoothedFrequency = ArrayUtils.toFloatArray(frequency);
}
XYChart.Series series = new XYChart.Series();
series.setName(terms[j]);
for(int i = AppParameters.timeSliceA; i < AppParameters.timeSliceB; i++){
double x = AppParameters.dataset.corpus.convertTimeSliceToDay(i);
double y = smoothedFrequency[i];
series.getData().add(new XYChart.Data(x,y));
}
frequencyChart.getData().add(series);
}
String[] interval = AppParameters.event.getTemporalDescription().split(",");
double rectX = (Double.parseDouble(interval[0])/AppParameters.dataset.corpus.getLength())*(Main.columnWidthLEFT-5)+3;
double rectY = (Double.parseDouble(interval[1])/AppParameters.dataset.corpus.getLength())*(Main.columnWidthLEFT-5)+3;
rectangleSelection.setTranslateX(rectX);
rectangleSelection.setWidth(rectY-rectX);
}
public final EventHandler<MouseEvent> createSelectedEventHandler(){
EventHandler<MouseEvent> eventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent t) {
AppParameters.event = (Event) eventTable.getItems().get(((TableCell)t.getSource()).getIndex());
updateFrequencyChart();
if(t.getButton() == MouseButton.SECONDARY){
eventRelatedMessages();
}
}
};
return eventHandler;
}
public final void eventRelatedMessages(){
final Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
stage.initStyle(StageStyle.UTILITY);
stage.setTitle("Messages");
TableView<Message> messageTable = new TableView<>();
UIUtils.setSize(messageTable, Main.columnWidthLEFT, 360);
TableColumn authorColumn = new TableColumn("Author");
TableColumn timeColumn = new TableColumn("Timestamp");
TableColumn textColumn = new TableColumn("Text");
authorColumn.setCellValueFactory(new PropertyValueFactory<>("author"));
timeColumn.setCellValueFactory(new PropertyValueFactory<>("time"));
textColumn.setCellValueFactory(new PropertyValueFactory<>("text"));
messageTable.getColumns().addAll(authorColumn,timeColumn,textColumn);
messageTable.setItems(AppParameters.dataset.corpus.getMessages(AppParameters.event));
Text tableText = new Text("Related messages ("+messageTable.getItems().size()+")");
Label topicLabel = new Label();
topicLabel.setText(AppParameters.event.getTextualDescription());
Date date = AppParameters.dataset.corpus.start;
Instant fromInstant = date.toInstant();
Instant toInstant = date.toInstant();
String[] interval = AppParameters.event.getTemporalDescription().split(",");
fromInstant = fromInstant.plusSeconds((long)(Double.parseDouble(interval[0])*24*60*60));
toInstant = toInstant.plusSeconds((long)(Double.parseDouble(interval[1])*24*60*60));
topicLabel.setText(AppParameters.event.getTextualDescription());
Label intervalLabel = new Label();
intervalLabel.setText("From "+fromInstant+" to "+toInstant);
TextField filterMessagesField1 = new TextField("");
filterMessagesField1.setPromptText("Type a list of words (separated by a space)");
UIUtils.setSize(filterMessagesField1, 2*(Main.columnWidthLEFT-5)/3, 24);
ChoiceBox operatorChoiceBox = new ChoiceBox();
ObservableList<String> operatorList = FXCollections.observableArrayList();
operatorList.add("and");
operatorList.add("or");
operatorChoiceBox.setItems(operatorList);
operatorChoiceBox.getSelectionModel().select(0);
UIUtils.setSize(operatorChoiceBox, (Main.columnWidthLEFT-5)/3, 24);
HBox filterMessagesHBox = new HBox(5);
filterMessagesHBox.getChildren().add(filterMessagesField1);
filterMessagesHBox.getChildren().add(operatorChoiceBox);
Button filterMessagesButton = new Button("Filter messages");
UIUtils.setSize(filterMessagesButton, Main.columnWidthLEFT, 24);
filterMessagesButton.setOnAction((ActionEvent ae) -> {
messageTable.getItems().clear();
String[] words = filterMessagesField1.getText().split(" ");
messageTable.setItems(AppParameters.dataset.corpus.getFilteredMessages(AppParameters.event,words,operatorChoiceBox.getSelectionModel().getSelectedIndex()));
tableText.setText("Related messages ("+messageTable.getItems().size()+")");
});
Scene scene = new Scene(VBoxBuilder.create().children(new Text("Topic"),new Separator(),topicLabel,new Text("Time interval"),new Separator(),intervalLabel,tableText,new Separator(),messageTable,filterMessagesHBox,filterMessagesButton).alignment(Pos.CENTER).padding(new Insets(10)).spacing(3).build());
scene.getStylesheets().add("resources/fr/ericlab/sondy/css/GlobalStyle.css");
stage.setScene(scene);
stage.show();
}
public void createTimelineStage(){
HashMap<Integer,Integer> map = new HashMap<>();
Path path = new Path();
MoveTo moveTo = new MoveTo();
moveTo.setX(0);
moveTo.setY(0);
LineTo lineTo = new LineTo();
lineTo.setX(Main.columnWidthLEFT);
lineTo.setY(0);
path.getElements().add(moveTo);
path.getElements().add(lineTo);
path.setStrokeWidth(4);
path.setStroke(Color.STEELBLUE);
path.setTranslateY(30);
path.setTranslateX(10);
VBox timelineBox = new VBox(5);
Group groupTimeline = new Group();
groupTimeline.getChildren().add(path);
for(Event event : selectedMethod.events.observableList){
Date date = AppParameters.dataset.corpus.start;
Instant fromInstant = date.toInstant();
Instant toInstant = date.toInstant();
String[] interval = event.getTemporalDescription().split(",");
fromInstant = fromInstant.plusSeconds((long)(Double.parseDouble(interval[0])*24*60*60));
toInstant = toInstant.plusSeconds((long)(Double.parseDouble(interval[1])*24*60*60));
Tooltip tooltip = new Tooltip("topic: "+event.getTextualDescription()+"\ntime interval: from "+fromInstant+" to "+toInstant);
Ellipse circle = EllipseBuilder.create()
.centerX(0)
.centerY(0)
.radiusX(6)
.radiusY(6)
.strokeWidth(3)
.stroke(Color.web("#f2f2f2"))
.fill(Color.STEELBLUE)
.build();
int yPos = 30;
int xPos = (int) ((Double.parseDouble(interval[0])/AppParameters.dataset.corpus.getLength())*Main.columnWidthLEFT+10);
if(map.get(xPos) == null){
map.put(xPos, 1);
}else{
int count = map.get(xPos);
yPos += 12*count;
map.put(xPos, count+1);
}
circle.setTranslateY(yPos);
circle.setTranslateX(xPos);
Tooltip.install(circle, tooltip);
groupTimeline.getChildren().add(circle);
}
Label startTime = new Label(AppParameters.dataset.corpus.start.toString());
startTime.setFont(new Font(null,10));
startTime.setTranslateY(5);
startTime.setTranslateX(10);
Label endTime = new Label(AppParameters.dataset.corpus.end.toString());
endTime.setFont(new Font(null,10));
endTime.setTranslateY(5);
endTime.setTranslateX(Main.columnWidthLEFT-145);
groupTimeline.getChildren().addAll(startTime,endTime);
final Stage dialogStageTimeline = new Stage();
Button closeButton = new Button("Close");
UIUtils.setSize(closeButton, Main.columnWidthLEFT, 24);
closeButton.setOnAction((ActionEvent ae) -> {
dialogStageTimeline.close();
});
timelineBox.getChildren().addAll(groupTimeline,closeButton);
Scene sceneTimeline = new Scene(VBoxBuilder.create().children(groupTimeline,closeButton).alignment(Pos.CENTER).padding(new Insets(10)).spacing(3).build());
sceneTimeline.getStylesheets().add("resources/fr/ericlab/sondy/css/GlobalStyle.css");
dialogStageTimeline.initStyle(StageStyle.UNDECORATED);
dialogStageTimeline.initModality(Modality.APPLICATION_MODAL);
dialogStageTimeline.setScene(sceneTimeline);
dialogStageTimeline.show();
}
}