/*
* #%L
* gitools-ui-app
* %%
* Copyright (C) 2013 Universitat Pompeu Fabra - Biomedical Genomics group
* %%
* 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/gpl-3.0.html>.
* #L%
*/
package org.gitools.ui.app.heatmap.editor;
import com.alee.extended.panel.GroupPanel;
import com.alee.extended.panel.GroupingType;
import com.alee.laf.panel.WebPanel;
import com.alee.laf.splitpane.WebSplitPane;
import com.google.common.base.Strings;
import org.gitools.api.ApplicationContext;
import org.gitools.api.PersistenceException;
import org.gitools.api.analysis.IProgressMonitor;
import org.gitools.api.matrix.IMatrix;
import org.gitools.api.matrix.IMatrixDimension;
import org.gitools.api.persistence.FileFormat;
import org.gitools.api.resource.IResourceLocator;
import org.gitools.api.resource.ResourceReference;
import org.gitools.heatmap.Heatmap;
import org.gitools.heatmap.HeatmapDimension;
import org.gitools.heatmap.format.HeatmapFormat;
import org.gitools.matrix.model.MatrixWrapper;
import org.gitools.persistence.locators.UrlResourceLocator;
import org.gitools.ui.app.commands.CommandLoadFile;
import org.gitools.ui.app.heatmap.panel.ColorScalePanel;
import org.gitools.ui.app.heatmap.panel.HeatmapMouseListener;
import org.gitools.ui.app.heatmap.panel.HeatmapPanel;
import org.gitools.ui.app.heatmap.panel.details.DetailsPanel;
import org.gitools.ui.app.heatmap.panel.search.HeatmapSearchPanel;
import org.gitools.ui.core.Application;
import org.gitools.ui.core.commands.Command;
import org.gitools.ui.core.components.boxes.Box;
import org.gitools.ui.core.components.editor.AbstractEditor;
import org.gitools.ui.core.components.editor.EditorsPanel;
import org.gitools.ui.core.components.wizard.SaveFileWizard;
import org.gitools.ui.core.pages.common.SaveHeatmapFilePage;
import org.gitools.ui.platform.IconUtils;
import org.gitools.ui.platform.icons.IconNames;
import org.gitools.ui.platform.progress.JobRunnable;
import org.gitools.ui.platform.progress.JobThread;
import org.gitools.ui.platform.settings.Settings;
import org.gitools.ui.platform.wizard.WizardDialog;
import org.gitools.utils.MemoryUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CancellationException;
public class HeatmapEditor extends AbstractEditor {
private static final Logger LOGGER = LoggerFactory.getLogger(HeatmapEditor.class);
private static final int DEFAULT_ACCORDION_WIDTH = 320;
private static final int MINIMUM_AVAILABLE_MEMORY_THRESHOLD = (int) (3 * Runtime.getRuntime().maxMemory() / 10);
private Heatmap heatmap;
private HeatmapPanel heatmapPanel;
private ColorScalePanel colorScalePanel;
private HeatmapSearchPanel searchPanel;
private DetailsPanel detailsPanel;
private int lastMouseRow = -1;
private int lastMouseCol = -1;
private Timer timer;
public HeatmapEditor() {
//weld requirement
}
public HeatmapEditor(final Heatmap heatmap) {
IResourceLocator locator = heatmap.getLocator();
if (locator != null && locator.getURL().getProtocol().equals("file")) {
try {
File file = new File(locator.getURL().toURI());
setFile(file);
} catch (URISyntaxException e) {
}
}
// Initialize and create heatmap model
heatmap.init();
this.heatmap = heatmap;
setIcon(IconUtils.getIconResource(IconNames.heatmap16));
if (!Strings.isNullOrEmpty(heatmap.getTitle())) {
setName(heatmap.getTitle());
} else {
heatmap.setTitle(getName());
}
createComponents(this);
setSaveAllowed(true);
setSaveAsAllowed(true);
setBackground(Color.WHITE);
// Add change listeners
PropertyChangeListener dirtyListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
HeatmapEditor.this.setDirty(true);
}
};
heatmap.addPropertyChangeListener(dirtyListener);
heatmap.getRows().addPropertyChangeListener(dirtyListener);
heatmap.getColumns().addPropertyChangeListener(dirtyListener);
heatmap.getLayers().addPropertyChangeListener(dirtyListener);
heatmap.getLayers().getTopLayer().getDecorator().addPropertyChangeListener(dirtyListener);
heatmap.addPropertyChangeListener(Heatmap.PROPERTY_TITLE, new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
setName(heatmap.getTitle());
}
});
// Create a timer that watches every 5 seconds the available memory
// and detach the heatmap if it is below a minimum threshold.
timer = new java.util.Timer();
timer.scheduleAtFixedRate(new CleanCacheTimer(), 5000, 5000);
}
@Override
public void setName(String name) {
if (this.heatmap != null && (this.heatmap.getTitle() == null || this.heatmap.getTitle().equals(""))) {
heatmap.setTitle(name);
}
super.setName(name);
}
private void createComponents(JComponent container) {
Dimension minimumSize = new Dimension(DEFAULT_ACCORDION_WIDTH, 100);
detailsPanel = new DetailsPanel(heatmap);
detailsPanel.setMinimumSize(minimumSize);
colorScalePanel = new ColorScalePanel(heatmap);
colorScalePanel.setMinimumSize(minimumSize);
WebPanel emptyPanel = new WebPanel();
emptyPanel.setBackground(Color.WHITE);
GroupPanel leftPanel = new GroupPanel(GroupingType.fillMiddle, false, detailsPanel, emptyPanel, colorScalePanel);
leftPanel.setUndecorated(true);
leftPanel.setBackground(Color.WHITE);
leftPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 0));
leftPanel.setMinimumSize(minimumSize);
heatmapPanel = new HeatmapPanel(heatmap);
heatmapPanel.requestFocusInWindow();
heatmapPanel.addHeatmapMouseListener(new HeatmapMouseListener() {
@Override
public void mouseMoved(int row, int col, MouseEvent e) {
HeatmapEditor.this.mouseMoved(row, col, e);
}
@Override
public void mouseClicked(int row, int col, MouseEvent e) {
HeatmapEditor.this.mouseClicked(row, col, e);
}
});
searchPanel = new HeatmapSearchPanel(heatmap, heatmapPanel);
searchPanel.setVisible(false);
JSplitPane splitPane = new WebSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, heatmapPanel);
heatmapPanel.setBorder(new CompoundBorder(BorderFactory.createEmptyBorder(20, 10, 20, 20), BorderFactory.createMatteBorder(0, 1, 1, 0, Color.GRAY)));
splitPane.setOneTouchExpandable(true);
splitPane.setDividerLocation(DEFAULT_ACCORDION_WIDTH);
splitPane.setLastDividerLocation(DEFAULT_ACCORDION_WIDTH);
splitPane.setContinuousLayout(false);
splitPane.setDividerSize(4);
splitPane.setBackground(Color.WHITE);
splitPane.setForeground(Color.WHITE);
container.setLayout(new BorderLayout());
container.add(searchPanel, BorderLayout.NORTH);
container.add(splitPane, BorderLayout.CENTER);
}
@Override
public Heatmap getModel() {
return heatmap;
}
@Override
public void refresh() {
}
@Override
public void doVisible() {
heatmapPanel.requestFocusInWindow();
}
@Override
public void doSaveAs(IProgressMonitor monitor) {
File file = getFile();
if (file != null) {
Settings.get().setLastPath(file.getParent());
}
String name = getName();
int heatmapExt = name.indexOf(".heatmap");
if (heatmapExt != -1) {
name = name.substring(0, heatmapExt);
}
name = name.replaceAll("\\.", "_");
SaveHeatmapFilePage page = new SaveHeatmapFilePage();
page.setTitle("Save heatmap as");
page.setFileNameWithoutExtension(name);
page.setFolder(Settings.get().getLastPath());
page.setFormats(new FileFormat[]{
new FileFormat("Heatmap, single file (*.heatmap.zip)", HeatmapFormat.EXTENSION + ".zip", false, false),
new FileFormat("Heatmap, multiple files (*.heatmap)", HeatmapFormat.EXTENSION, false, false)
});
if (heatmap.getRows().size() == heatmap.getContents().getRows().size() &&
heatmap.getColumns().size() == heatmap.getContents().getColumns().size()) {
page.enableDiscardHidden(false);
} else {
page.suggestDiscardHidden();
}
SaveFileWizard wiz = SaveFileWizard.createCustom(page);
final WizardDialog dlg = new WizardDialog(Application.get(), wiz);
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
dlg.setVisible(true);
}
});
} catch (InterruptedException e) {
throw new CancellationException();
} catch (InvocationTargetException e) {
throw new CancellationException();
}
if (dlg.isCancelled()) {
return;
}
file = wiz.getPathAsFile();
boolean discardHidden = page.isDiscardHidden();
boolean optimizeData = page.isOptimizeData();
doSaveAsNoUI(monitor, file, discardHidden, optimizeData, true);
}
public void doSaveAsNoUI(IProgressMonitor monitor, File file, boolean discardHidden, boolean optimizeData, boolean reload) {
setFile(file);
heatmap.setGitoolsVersion(Application.getGitoolsVersion());
IResourceLocator toLocator;
if (heatmap.getLocator() == null) {
toLocator = new UrlResourceLocator(file);
} else {
toLocator = new UrlResourceLocator(heatmap.getLocator().getReadFile(), file);
}
// Data matrix
IMatrix data = heatmap.getData().get();
if (discardHidden) {
// Discard hidden data
data = new MatrixWrapper(data) {
@Override
public IMatrixDimension getColumns() {
return heatmap.getColumns();
}
@Override
public IMatrixDimension getRows() {
return heatmap.getRows();
}
@Override
public boolean isChanged() {
return true;
}
};
heatmap.setData(new ResourceReference<>("data", data));
} else {
if (optimizeData) {
// Optimize mtabix index
data = new MatrixWrapper(data) {
@Override
public boolean isChanged() {
return true;
}
};
heatmap.setData(new ResourceReference<>("data", data));
} else {
IResourceLocator locator = heatmap.getData().getLocator();
heatmap.setData(new ResourceReference<>("data", data));
heatmap.getData().setLocator(locator);
heatmap.getData().setChanged(data.isChanged());
heatmap.getData().setBaseName(toLocator.getBaseName() + "-data");
}
}
HeatmapDimension rows = heatmap.getRows();
HeatmapDimension columns = heatmap.getColumns();
rows.setAnnotationsReference(new ResourceReference<>(rows.getId().toString().toLowerCase() + "-annotations", rows.getAnnotations()));
columns.setAnnotationsReference(new ResourceReference<>(columns.getId().toString().toLowerCase() + "-annotations", columns.getAnnotations()));
Settings.get().setLastPath(file.getAbsoluteFile().getParent());
doSave(toLocator, monitor, reload);
}
@Override
public void doSave(IProgressMonitor monitor) {
doSave(heatmap.getLocator(), monitor, true);
}
public void doSave(IResourceLocator toLocator, IProgressMonitor monitor, boolean reload) {
File file = getFile();
if (file == null) {
doSaveAs(monitor);
return;
}
heatmap.setGitoolsVersion(Application.getGitoolsVersion());
// Last saved and author info.
heatmap.setLastSaved(new Date());
Settings settings = Settings.get();
if (Strings.isNullOrEmpty(heatmap.getAuthorName())) {
heatmap.setAuthorName(settings.getAuthorName());
if (!Strings.isNullOrEmpty(settings.getAuthorEmail())) {
heatmap.setAuthorEmail(settings.getAuthorEmail());
}
}
try {
ApplicationContext.getPersistenceManager().store(toLocator, heatmap, monitor);
} catch (PersistenceException ex) {
monitor.exception(ex);
}
setDirty(false);
Settings.get().addRecentFile(file.getAbsolutePath());
Settings.get().save();
// Force to reload the data after save
if (reload && (!toLocator.equals(heatmap.getLocator()) || heatmap.getData().get().isChanged())) {
monitor.title("Reloading the heatmap...");
EditorsPanel editorPanel = Application.get().getEditorsPanel();
editorPanel.removeEditor(this);
CommandLoadFile loadFile = new CommandLoadFile(toLocator.getURL());
try {
loadFile.execute(monitor);
} catch (Command.CommandException e) {
throw new RuntimeException(e);
}
}
}
@Override
public boolean doClose() {
return doClose(true);
}
public boolean doClose(boolean UI) {
if (UI && isDirty()) {
int res = JOptionPane.showOptionDialog(Application.get(), "File " + getName() + " is modified.\n" +
"Save changes ?", "Close", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, new String[]{"Cancel", "Discard", "Save"}, "Save");
if (res == -1 || res == 0) {
return false;
} else if (res == 2) {
SaveFileWizard wiz = SaveFileWizard.createSimple("Save heatmap", getName(), Settings.get().getLastPath(), new FileFormat[]{new FileFormat("Heatmap", HeatmapFormat.EXTENSION)});
WizardDialog dlg = new WizardDialog(Application.get(), wiz);
dlg.setVisible(true);
if (dlg.isCancelled()) {
return false;
}
Settings.get().setLastPath(wiz.getFolder());
setFile(wiz.getPathAsFile());
JobThread.execute(Application.get(), new JobRunnable() {
@Override
public void run(IProgressMonitor monitor) {
doSave(monitor);
}
});
}
}
// Force free memory
timer.cancel();
timer.purge();
heatmap.detach(null);
if (heatmap.getData().getLocator() != null) {
heatmap.getData().unload();
}
heatmap = null;
return true;
}
public void showSearch(boolean searchColumns) {
if (searchPanel.isVisible() && searchPanel.searchRows() != searchColumns) {
searchPanel.close();
} else {
searchPanel.searchOnColumns(searchColumns);
searchPanel.setVisible(true);
}
}
void mouseMoved(int row, int col, MouseEvent e) {
}
void mouseClicked(int row, int col, MouseEvent e) {
}
@Override
public void detach() {
this.heatmap.detach(heatmap.getLayers().getTopLayer());
}
@Override
public Collection<Box> getBoxes() {
return detailsPanel.getBoxes();
}
private class CleanCacheTimer extends TimerTask {
private boolean trackException = true;
private long lastDetach;
private CleanCacheTimer() {
this.lastDetach = System.currentTimeMillis();
}
@Override
public void run() {
if (MemoryUtils.getAvailableMemory() < MINIMUM_AVAILABLE_MEMORY_THRESHOLD && ((System.currentTimeMillis() - lastDetach)) > 2000) {
LOGGER.warn("Memory too low, cleaning cache.");
this.lastDetach = System.currentTimeMillis();
if (trackException) {
Application.get().trackException("Memory too low");
trackException = false;
}
detach();
}
}
}
}