/*
* Copyright 2016 Igor Maznitsa.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.igormaznitsa.sciareto.ui.misc;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.StringReader;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.SwingUtilities;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringEscapeUtils;
import com.google.common.base.Function;
import com.igormaznitsa.mindmap.model.MMapURI;
import com.igormaznitsa.mindmap.model.MindMap;
import com.igormaznitsa.mindmap.model.logger.Logger;
import com.igormaznitsa.mindmap.model.logger.LoggerFactory;
import com.igormaznitsa.sciareto.ui.MapUtils;
import com.igormaznitsa.sciareto.ui.UiUtils;
import edu.uci.ics.jung.algorithms.layout.ISOMLayout;
import edu.uci.ics.jung.graph.DirectedSparseGraph;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.util.EdgeType;
import edu.uci.ics.jung.visualization.DefaultVisualizationModel;
import edu.uci.ics.jung.visualization.GraphZoomScrollPane;
import edu.uci.ics.jung.visualization.VisualizationModel;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import edu.uci.ics.jung.visualization.control.DefaultModalGraphMouse;
import edu.uci.ics.jung.visualization.control.GraphMouseListener;
import edu.uci.ics.jung.visualization.control.LayoutScalingControl;
import edu.uci.ics.jung.visualization.control.PickingGraphMousePlugin;
import edu.uci.ics.jung.visualization.control.ScalingGraphMousePlugin;
import edu.uci.ics.jung.visualization.control.ViewScalingControl;
import edu.uci.ics.jung.visualization.decorators.ToStringLabeller;
import edu.uci.ics.jung.visualization.renderers.DefaultVertexLabelRenderer;
import edu.uci.ics.jung.visualization.renderers.Renderer;
public final class FileLinkGraphPanel extends javax.swing.JPanel {
private static final long serialVersionUID = -5145163577941732908L;
private static final Logger LOGGER = LoggerFactory.getLogger(FileLinkGraphPanel.class);
private FileVertex selectedVertex;
private static final Color COLOR_BACKGROUND = Color.WHITE;
private static final Color COLOR_ARROW = Color.ORANGE.darker();
private static final Color COLOR_LABELS = Color.BLACK;
private static final Icon RELAYOUT_ICON = new ImageIcon(UiUtils.loadIcon("graph16.png")); //NOI18N
public enum FileVertexType {
FOLDER("folder.png","Folder"), DOCUMENT("document.png","Document"), MINDMAP("mindmap.png","Mind Map"), UNKNOWN("unknown.png","Unknown"), NOTFOUND("notfound.png","Not found"); //NOI18N
private final Icon icon;
private final String text;
private FileVertexType(@Nonnull final String icon, @Nonnull final String text) {
this.icon = new ImageIcon(UiUtils.loadIcon("graph/" + icon)); //NOI18N
this.text = text;
}
@Override
public String toString(){
return text;
}
@Nonnull
public Icon getIcon() {
return this.icon;
}
}
public static final class FileVertex {
private final String text;
private final String tooltip;
private final FileVertexType type;
private final File file;
public FileVertex(@Nonnull final File file, @Nonnull final FileVertexType type) {
this.type = type;
this.text = file.getName();
this.tooltip = "<html><b>"+type.toString()+"</b><br>"+StringEscapeUtils.unescapeHtml(FilenameUtils.normalizeNoEndSeparator(file.getAbsolutePath()))+"</html>"; //NOI18N
this.file = file;
}
@Override
public int hashCode() {
return this.file.hashCode();
}
@Override
public boolean equals(@Nullable final Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
return obj instanceof FileVertex && ((FileVertex) obj).file.equals(this.file);
}
@Nonnull
@Override
public String toString() {
return this.text;
}
@Nonnull
public String getTooltip() {
return this.tooltip;
}
@Nonnull
public File getFile() {
return this.file;
}
@Nonnull
public FileVertexType getType() {
return this.type;
}
}
@Nonnull
private Graph<FileVertex, Number> makeGraph(@Nullable final File projectFolder, @Nullable final File startMindMap) {
final DirectedSparseGraph<FileVertex, Number> result = new DirectedSparseGraph<FileVertex, Number>();
final AtomicInteger edgeCounter = new AtomicInteger();
final Set<File> mapFilesInProcessing = new HashSet<File>();
if (startMindMap != null) {
addMindMapAndFillByItsLinks(null, result, projectFolder, startMindMap, edgeCounter, mapFilesInProcessing);
} else if (projectFolder != null) {
final Iterator<File> iterator = FileUtils.iterateFiles(projectFolder, new String[]{"mmd"}, true); //NOI18N
while (iterator.hasNext()) {
final File mmdFile = iterator.next();
if (mmdFile.isFile()) {
addMindMapAndFillByItsLinks(null, result, projectFolder, mmdFile, edgeCounter, mapFilesInProcessing);
}
}
}
return result;
}
@Nullable
private static FileVertex addMindMapAndFillByItsLinks(@Nullable final FileVertex parent, @Nonnull final @Nullable Graph<FileVertex, Number> graph, @Nullable final File projectFolder, @Nonnull final File mindMapFile, @Nonnull final AtomicInteger edgeCounter, @Nonnull Set<File> mapFilesInProcessing) {
MindMap map;
FileVertex thisVertex;
try {
thisVertex = new FileVertex(mindMapFile, FileVertexType.MINDMAP);
map = new MindMap(null, new StringReader(FileUtils.readFileToString(mindMapFile, "UTF-8"))); //NOI18N
if (parent != null) {
for (final MMapURI fileUri : MapUtils.extractAllFileLinks(map)) {
if (parent.getFile().equals(fileUri.asFile(projectFolder))) {
graph.addEdge(edgeCounter.getAndIncrement(), thisVertex, parent, EdgeType.DIRECTED);
break;
}
}
if (mapFilesInProcessing.contains(mindMapFile)) {
return null;
}
}
}
catch (final Exception ex) {
LOGGER.error("Can't load mind map : " + mindMapFile, ex); //NOI18N
thisVertex = new FileVertex(mindMapFile, FileVertexType.UNKNOWN);
map = null;
}
mapFilesInProcessing.add(mindMapFile);
graph.addVertex(thisVertex);
if (map != null) {
for (final MMapURI fileUri : MapUtils.extractAllFileLinks(map)) {
final FileVertex that;
final File convertedFile = convertUriInFile(mindMapFile, projectFolder, fileUri);
if (convertedFile == null) {
that = new FileVertex(fileUri.asFile(projectFolder), FileVertexType.NOTFOUND);
} else if (convertedFile.isDirectory()) {
that = new FileVertex(convertedFile, FileVertexType.FOLDER);
} else if (convertedFile.isFile()) {
if (convertedFile.getName().endsWith(".mmd")) { //NOI18N
if (convertedFile.equals(mindMapFile)) {
that = thisVertex;
} else {
that = addMindMapAndFillByItsLinks(thisVertex, graph, projectFolder, convertedFile, edgeCounter, mapFilesInProcessing);
}
} else {
that = new FileVertex(convertedFile, FileVertexType.DOCUMENT);
}
} else {
that = new FileVertex(convertedFile, convertedFile.exists() ? FileVertexType.UNKNOWN : FileVertexType.NOTFOUND);
}
if (that != null) {
graph.addEdge(edgeCounter.getAndIncrement(), thisVertex, that, EdgeType.DIRECTED);
}
}
}
return thisVertex;
}
@Nullable
private static File convertUriInFile(@Nonnull final File containingMindMap, @Nullable final File baseFolder, @Nonnull final MMapURI uri) {
File result = uri.asFile(baseFolder);
if (!uri.isAbsolute() && !result.exists()) {
File basePath = com.igormaznitsa.sciareto.ui.FileUtils.removeLastElementInPath(containingMindMap);
do {
result = uri.asFile(basePath);
if (result.exists()) {
break;
}
result = null;
basePath = com.igormaznitsa.sciareto.ui.FileUtils.removeLastElementInPath(basePath);
}while(!com.igormaznitsa.sciareto.ui.FileUtils.isRootFile(basePath));
}
return result;
}
public FileLinkGraphPanel(@Nullable final File projectFolder, @Nullable final File startMindMap) {
initComponents();
final Dimension SCROLL_COMPONENT_SIZE = new Dimension(600, 450);
final Graph<FileVertex, Number> graph = makeGraph(projectFolder, startMindMap);
final ISOMLayout<FileVertex, Number> graphLayout = new ISOMLayout<FileVertex, Number>(graph);
final VisualizationModel<FileVertex, Number> viewModel = new DefaultVisualizationModel<FileVertex, Number>(graphLayout, new Dimension(2000, 2000));
final VisualizationViewer<FileVertex, Number> graphViewer = new VisualizationViewer<FileVertex, Number>(viewModel,new Dimension(800, 800));
final DefaultModalGraphMouse graphMouse = new DefaultModalGraphMouse() {
@Override
protected void loadPlugins() {
this.scalingPlugin = new ScalingGraphMousePlugin(new ViewScalingControl(), 0);
this.pickingPlugin = new PickingGraphMousePlugin();
add(this.scalingPlugin);
add(this.pickingPlugin);
setMode(Mode.PICKING);
}
};
graphViewer.setGraphMouse(graphMouse);
graphViewer.getRenderContext().setVertexIconTransformer(new Function<FileVertex, Icon>() {
@Override
public Icon apply(@Nonnull final FileVertex f) {
return f.getType().getIcon();
}
});
graphViewer.setBackground(COLOR_BACKGROUND);
graphViewer.getRenderContext().setVertexLabelTransformer(new ToStringLabeller());
final DefaultVertexLabelRenderer labelRenderer = new DefaultVertexLabelRenderer(COLOR_LABELS);
graphViewer.getRenderContext().setVertexLabelRenderer(labelRenderer);
graphViewer.getRenderer().getVertexLabelRenderer().setPosition(Renderer.VertexLabel.Position.S);
final Function<Number, Paint> edgePaintTransformer = new Function<Number, Paint>() {
@Override
public Paint apply(@Nonnull final Number input) {
return COLOR_ARROW;
}
};
graphViewer.getRenderContext().setEdgeDrawPaintTransformer(edgePaintTransformer);
graphViewer.getRenderContext().setArrowFillPaintTransformer(edgePaintTransformer);
graphViewer.getRenderContext().setArrowDrawPaintTransformer(edgePaintTransformer);
graphViewer.setVertexToolTipTransformer(new Function<FileVertex, String>() {
@Override
@Nonnull
public String apply(@Nonnull final FileVertex f) {
return f.getTooltip();
}
});
graphViewer.addGraphMouseListener(new GraphMouseListener<FileVertex>() {
@Override
public void graphClicked(@Nonnull final FileVertex v, @Nonnull final MouseEvent me) {
if (!me.isPopupTrigger() && me.getClickCount() > 1 && v.getType() != FileVertexType.NOTFOUND) {
selectedVertex = v;
final Window window = SwingUtilities.getWindowAncestor(graphViewer);
if (window != null) {
window.setVisible(false);
}
}
}
@Override
public void graphPressed(@Nonnull final FileVertex v, @Nonnull final MouseEvent me) {
}
@Override
public void graphReleased(@Nonnull final FileVertex v, @Nonnull final MouseEvent me) {
}
});
final GraphZoomScrollPane scroll = new GraphZoomScrollPane(graphViewer);
scroll.setPreferredSize(SCROLL_COMPONENT_SIZE);
UiUtils.makeOwningDialogResizable(this);
graphViewer.scaleToLayout(new LayoutScalingControl());
final JButton layoutButton = new JButton(RELAYOUT_ICON);
layoutButton.setToolTipText("Relayout graph");
layoutButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
layoutButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
final Rectangle visible = scroll.getVisibleRect();
graphViewer.getGraphLayout().reset();
graphViewer.getGraphLayout().setSize(new Dimension(visible.width,visible.height));
graphViewer.repaint();
}
});
scroll.setCorner(layoutButton);
this.add(scroll, BorderLayout.CENTER);
}
@Nullable
public FileVertex getSelectedFile() {
return this.selectedVertex;
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
setLayout(new java.awt.BorderLayout());
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
// End of variables declaration//GEN-END:variables
}