/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2007 - 2008, Open Source Geospatial Foundation (OSGeo)
* (C) 2008 - 2009, Johann Sorel
* (C) 2009 - 2014, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package org.geotoolkit.gui.swing.style;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.JSpinner;
import javax.swing.JTree;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.geotoolkit.display2d.service.DefaultGlyphService;
import org.geotoolkit.factory.FactoryFinder;
import org.geotoolkit.font.FontAwesomeIcons;
import org.geotoolkit.font.IconBuilder;
import org.geotoolkit.style.MutableFeatureTypeStyle;
import org.geotoolkit.style.MutableRule;
import org.geotoolkit.style.MutableStyle;
import org.geotoolkit.style.MutableStyleFactory;
import org.geotoolkit.style.RandomStyleBuilder;
import org.jdesktop.swingx.JXTree;
import org.opengis.style.Description;
import org.opengis.style.Symbolizer;
import org.opengis.util.InternationalString;
/**
*
* @author Johann Sorel (Puzzle-GIS)
* @module
*/
public class JStyleTree<T> extends JXTree implements DragGestureListener, DragSourceListener, DropTargetListener {
private static final MutableStyleFactory SF = (MutableStyleFactory) FactoryFinder.getStyleFactory(null);
public static final ImageIcon ICON_STYLE = IconBuilder.createIcon(FontAwesomeIcons.ICON_BOOK,16,FontAwesomeIcons.DEFAULT_COLOR);
public static final ImageIcon ICON_FTS = IconBuilder.createIcon(FontAwesomeIcons.ICON_TAG,16,FontAwesomeIcons.DEFAULT_COLOR);
public static final ImageIcon ICON_RULE = IconBuilder.createIcon(FontAwesomeIcons.ICON_FILTER,16,FontAwesomeIcons.DEFAULT_COLOR);
public static final ImageIcon ICON_NEW = IconBuilder.createIcon(FontAwesomeIcons.ICON_PLUS,16,FontAwesomeIcons.DEFAULT_COLOR);
public static final ImageIcon ICON_DUPLICATE = IconBuilder.createIcon(FontAwesomeIcons.ICON_FILES_O,16,FontAwesomeIcons.DEFAULT_COLOR);
public static final ImageIcon ICON_DELETE = IconBuilder.createIcon(FontAwesomeIcons.ICON_TRASH_O,16,FontAwesomeIcons.DEFAULT_COLOR);
private T style = null;
private final StyleTreeModel treemodel = new StyleTreeModel(null);
/** Variables needed for DnD */
private DragSource dragSource = null;
public JStyleTree() {
super();
setModel(treemodel);
setEditable(false);
setCellRenderer(new StyleCellRenderer());
getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
setComponentPopupMenu(new StylePopup());
dragSource = DragSource.getDefaultDragSource();
DragGestureRecognizer dgr = dragSource.createDefaultDragGestureRecognizer(this,DnDConstants.ACTION_COPY_OR_MOVE, this);
dgr.setSourceActions(dgr.getSourceActions() & ~InputEvent.BUTTON3_MASK);
DropTarget dropTarget = new DropTarget(this, this);
}
public T getStyleElement() {
return style;
}
public void setStyleElement(final T style) {
this.style = style;
if (style != null) {
treemodel.setRoot(style);
revalidate();
}
expandAll();
}
//-------------Drag & drop -------------------------------------------------
private StyleElementTransferable dd = null;
@Override
public void dragGestureRecognized(final DragGestureEvent e) {
final TreePath path = getSelectionModel().getSelectionPath();
final Object[] pathObjs = path.getPath();
final Object dragged = path.getLastPathComponent();
if (dragged != null) {
final Object parent = (pathObjs.length>1) ? pathObjs[pathObjs.length-2] : null;
dd = new StyleElementTransferable(dragged, parent);
e.startDrag(null, dd);
}
}
//--------------------drag events-------------------------------------------
@Override
public void dragEnter(final DragSourceDragEvent dsde) {
}
@Override
public void dragOver(final DragSourceDragEvent dsde) {
}
@Override
public void dropActionChanged(final DragSourceDragEvent dsde) {
}
@Override
public void dragExit(final DragSourceEvent dse) {
}
@Override
public void dragDropEnd(final DragSourceDropEvent dsde) {
}
//--------------------drop events-------------------------------------------
@Override
public void dragEnter(final DropTargetDragEvent dtde) {
}
@Override
public void dragOver(final DropTargetDragEvent dtde) {
}
@Override
public void dropActionChanged(final DropTargetDragEvent dtde) {
}
@Override
public void dragExit(final DropTargetEvent dte) {
}
@Override
public void drop(final DropTargetDropEvent dtde) {
final Point loc = dtde.getLocation();
final TreePath targetPath = getPathForLocation(loc.x, loc.y);
final Transferable trs = dd;
if(!(trs instanceof StyleElementTransferable)) return;
final StyleElementTransferable strs = (StyleElementTransferable) trs;
if (targetPath != null && strs.getStyleElement() != null && strs.getParent() != null) {
final Object[] pathObjs = targetPath.getPath();
final Object targetObj = targetPath.getLastPathComponent();
final Object targetParentObj = (pathObjs.length>1) ? pathObjs[pathObjs.length-2] : null;
final Object movedObj = strs.getStyleElement();
final Object movedParentObj = strs.getParent();
if(targetObj == movedObj){
//same object , don't do anything
return;
}
if (targetObj instanceof MutableFeatureTypeStyle && movedObj instanceof MutableFeatureTypeStyle) {
if(movedParentObj != null){
((MutableStyle)movedParentObj).featureTypeStyles().remove((MutableFeatureTypeStyle)movedObj);
}
final MutableFeatureTypeStyle targetFTS = (MutableFeatureTypeStyle) targetObj;
final int targetIndex = ((MutableStyle)targetParentObj).featureTypeStyles().indexOf(targetFTS);
((MutableStyle)targetParentObj).featureTypeStyles().add(targetIndex,(MutableFeatureTypeStyle)movedObj);
} else if (targetObj instanceof MutableFeatureTypeStyle && movedObj instanceof MutableRule) {
if (movedParentObj == targetParentObj) {
return;
}
if(movedParentObj != null){
((MutableFeatureTypeStyle)movedParentObj).rules().remove((MutableRule)movedObj);
}
((MutableFeatureTypeStyle)targetObj).rules().add((MutableRule)movedObj);
} else if (targetObj instanceof MutableRule && movedObj instanceof MutableRule) {
if(movedParentObj != null){
((MutableFeatureTypeStyle)movedParentObj).rules().remove((MutableRule)movedObj);
}
final MutableRule targetRule = (MutableRule) targetObj;
final int targetIndex = ((MutableFeatureTypeStyle)targetParentObj).rules().indexOf(targetRule);
((MutableFeatureTypeStyle)targetParentObj).rules().add(targetIndex,(MutableRule)movedObj);
} else if (targetObj instanceof MutableRule && movedObj instanceof Symbolizer) {
if(movedParentObj != null){
((MutableRule)movedParentObj).symbolizers().remove((Symbolizer)movedObj);
}
((MutableRule)targetObj).symbolizers().add((Symbolizer)movedObj);
} else if (targetObj instanceof Symbolizer && movedObj instanceof Symbolizer) {
if(movedParentObj != null){
((MutableRule)movedParentObj).symbolizers().remove((Symbolizer)movedObj);
}
final Symbolizer targetSymbol = (Symbolizer) targetObj;
final int targetIndex = ((MutableRule)targetParentObj).symbolizers().indexOf(targetSymbol);
((MutableRule)targetParentObj).symbolizers().add(targetIndex,(Symbolizer)movedObj);
}
}
}
private static boolean isDeletable(final Object removeObject){
return removeObject instanceof MutableFeatureTypeStyle
|| removeObject instanceof MutableRule
|| removeObject instanceof Symbolizer;
}
//-------------private classes----------------------------------------------
class StyleCellRenderer extends DefaultTreeCellRenderer {
@Override
public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean selected,
final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) {
final Component comp = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
if (comp instanceof JLabel) {
final JLabel lbl = (JLabel) comp;
final Object val = value;
if (val instanceof MutableStyle) {
final MutableStyle style = (MutableStyle) val;
lbl.setText(text(style.getDescription()));
lbl.setIcon(ICON_STYLE);
} else if (val instanceof MutableFeatureTypeStyle) {
final MutableFeatureTypeStyle fts = (MutableFeatureTypeStyle) val;
lbl.setText(text(fts.getDescription()));
lbl.setIcon(ICON_FTS);
} else if (val instanceof MutableRule) {
final MutableRule r = (MutableRule) val;
lbl.setText(text(r.getDescription()));
lbl.setIcon(ICON_RULE);
} else if (val instanceof Symbolizer) {
final Symbolizer symb = (Symbolizer) val;
final Dimension dim = DefaultGlyphService.glyphPreferredSize(symb, null, null);
final BufferedImage img = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB);
DefaultGlyphService.render(symb, new Rectangle(dim),img.createGraphics(),null);
final Icon ico = new ImageIcon(img);
lbl.setText(symb.getName()==null?"":symb.getName());
lbl.setIcon(ico);
}
}
return comp;
}
private String text(Description desc){
if(desc == null){
return " - ";
}else{
final InternationalString str = desc.getTitle();
if(str != null && !str.toString().trim().isEmpty()){
return str.toString();
}else{
return " - ";
}
}
}
}
private class StylePopup extends JPopupMenu {
StylePopup() {
super();
}
@Override
public void setVisible(final boolean visible) {
TreePath path = JStyleTree.this.getSelectionModel().getSelectionPath();
final Point mousePosition = JStyleTree.this.getMousePosition();
if(mousePosition != null){
final int rowIndex = getRowForLocation(mousePosition.x, mousePosition.y);
if(rowIndex>=0){
path = JStyleTree.this.getPathForRow(rowIndex);
}
}
if (path != null && visible) {
removeAll();
final Object val = path.getLastPathComponent();
if (val instanceof MutableStyle) {
final MutableStyle style = (MutableStyle) val;
add(new NewFTSAction(style));
add(new JSeparator(SwingConstants.HORIZONTAL));
add(new ExpandAction(path));
add(new CollapseAction(path));
} else if (val instanceof MutableFeatureTypeStyle) {
final MutableFeatureTypeStyle fts = (MutableFeatureTypeStyle) val;
add(new NewRuleAction(fts));
add(new JSeparator(SwingConstants.HORIZONTAL));
add(new ExpandAction(path));
add(new CollapseAction(path));
add(new ChangeRuleScaleAction(fts));
add(new JSeparator(SwingConstants.HORIZONTAL));
add(new DuplicateAction(path));
} else if (val instanceof MutableRule) {
final MutableRule rule = (MutableRule) val;
final List<StyleElementEditor> editors = StyleElementEditor.findEditorsForType(Symbolizer.class);
for(StyleElementEditor editor : editors){
add(new NewSymbolizerAction(rule,editor));
}
add(new JSeparator(SwingConstants.HORIZONTAL));
add(new ExpandAction(path));
add(new CollapseAction(path));
add(new JSeparator(SwingConstants.HORIZONTAL));
add(new DuplicateAction(path));
} else if (val instanceof Symbolizer) {
add(new DuplicateAction(path));
}
if(isDeletable(path.getLastPathComponent())){
add(new JSeparator(SwingConstants.HORIZONTAL));
add(new DeleteAction(path));
}
}
super.setVisible(visible);
}
}
class CollapseAction extends AbstractAction{
private final TreePath path;
CollapseAction(final TreePath path) {
super("Collapse sub nodes.");
this.path = path;
}
@Override
public void actionPerformed(final ActionEvent ae) {
final Object parent = path.getLastPathComponent();
for(int i=0,n=treeModel.getChildCount(parent); i<n; i++){
final Object child = treemodel.getChild(parent, i);
collapsePath(path.pathByAddingChild(child));
}
}
}
class ExpandAction extends AbstractAction{
private final TreePath path;
ExpandAction(final TreePath path) {
super("Expand sub nodes.");
this.path = path;
}
@Override
public void actionPerformed(final ActionEvent ae) {
final Object parent = path.getLastPathComponent();
for(int i=0,n=treeModel.getChildCount(parent); i<n; i++){
final Object child = treemodel.getChild(parent, i);
final TreePath tp1 = path.pathByAddingChild(child);
expandPath(tp1);
for(int k=0,l=treeModel.getChildCount(child); k<l; k++){
expandPath(tp1.pathByAddingChild(treemodel.getChild(child, k)));
}
}
}
}
class ChangeRuleScaleAction extends AbstractAction{
private final MutableFeatureTypeStyle fts;
ChangeRuleScaleAction(final MutableFeatureTypeStyle cdt) {
super("Change rules valid scale.");
this.fts = cdt;
}
@Override
public void actionPerformed(final ActionEvent ae) {
final JPanel pan = new JPanel();
pan.add(new JLabel(" Min scale : "));
final JSpinner spiMin = new JSpinner(new SpinnerNumberModel());
spiMin.setPreferredSize(new Dimension(150, spiMin.getPreferredSize().height));
pan.add(spiMin);
pan.add(new JLabel(" Max scale : "));
final JSpinner spiMax = new JSpinner(new SpinnerNumberModel());
spiMax.setPreferredSize(new Dimension(150, spiMax.getPreferredSize().height));
spiMax.setValue(Double.MAX_VALUE);
pan.add(spiMax);
final JOptionPane jop = new JOptionPane(pan);
final JDialog dialog = jop.createDialog(JStyleTree.this,"Change scale");
dialog.pack();
dialog.setLocationRelativeTo(null);
dialog.setVisible(true);
final double min = ((Number)spiMin.getValue()).doubleValue();
final double max = ((Number)spiMax.getValue()).doubleValue();
for(MutableRule rule : fts.rules()){
rule.setMinScaleDenominator(min);
rule.setMaxScaleDenominator(max);
}
}
}
class NewFTSAction extends AbstractAction{
private final MutableStyle style;
NewFTSAction(final MutableStyle cdt) {
super("new FTS",ICON_NEW);
this.style = cdt;
}
@Override
public void actionPerformed(final ActionEvent ae) {
style.featureTypeStyles().add(SF.featureTypeStyle(RandomStyleBuilder.createRandomPointSymbolizer()));
}
}
class NewRuleAction extends AbstractAction{
private final MutableFeatureTypeStyle fts;
NewRuleAction(final MutableFeatureTypeStyle cdt) {
super("new Rule",ICON_NEW);
this.fts = cdt;
}
@Override
public void actionPerformed(final ActionEvent ae) {
fts.rules().add(SF.rule(RandomStyleBuilder.createRandomPointSymbolizer()));
}
}
class NewSymbolizerAction extends AbstractAction{
private final MutableRule rule;
private final StyleElementEditor editor;
NewSymbolizerAction(final MutableRule cdt, final StyleElementEditor editor) {
super(editor.getEditedClass().getSimpleName(),ICON_NEW);
this.rule = cdt;
this.editor = editor;
}
@Override
public void actionPerformed(final ActionEvent e) {
rule.symbolizers().add((Symbolizer)editor.create());
}
}
class DuplicateAction extends AbstractAction {
private final TreePath path;
DuplicateAction(final TreePath path) {
super("Duplicate", ICON_DUPLICATE);
this.path = path;
}
@Override
public void actionPerformed(final ActionEvent e) {
final Object[] pathObjs = path.getPath();
if(pathObjs.length>1){
treemodel.duplicateNode(pathObjs[pathObjs.length-2],pathObjs[pathObjs.length-1]);
}
}
}
class DeleteAction extends AbstractAction {
private final TreePath path;
DeleteAction(final TreePath path) {
super("Delete",ICON_DELETE);
this.path = path;
}
@Override
public void actionPerformed(final ActionEvent e) {
final Object[] pathObjs = path.getPath();
if(pathObjs.length>1){
treemodel.removeChild(pathObjs[pathObjs.length-2],pathObjs[pathObjs.length-1]);
}
}
}
private static class StyleElementTransferable implements Transferable{
private final Object styleElement;
private final Object parent;
public StyleElementTransferable(final Object styleElement, final Object parent){
this.styleElement = styleElement;
this.parent = parent;
}
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[0];
}
@Override
public boolean isDataFlavorSupported(final DataFlavor df) {
return true;
}
@Override
public Object getTransferData(final DataFlavor df) throws UnsupportedFlavorException, IOException {
return styleElement;
}
public Object getStyleElement(){
return styleElement;
}
public Object getParent(){
return parent;
}
}
}