/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2014-2015, 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.javafx.style;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import static javafx.beans.binding.Bindings.*;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableStringValue;
import javafx.beans.value.ObservableValue;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.Modality;
import javafx.util.Callback;
import org.geotoolkit.display2d.service.DefaultGlyphService;
import org.geotoolkit.font.FontAwesomeIcons;
import org.geotoolkit.font.IconBuilder;
import org.geotoolkit.gui.javafx.contexttree.menu.ActionMenuItem;
import org.geotoolkit.gui.javafx.layer.FXLayerStylePane;
import static org.geotoolkit.gui.javafx.style.FXStyleElementController.getStyleFactory;
import org.geotoolkit.gui.javafx.util.FXUtilities;
import org.geotoolkit.internal.GeotkFX;
import org.geotoolkit.map.FeatureMapLayer;
import org.geotoolkit.map.MapLayer;
import org.geotoolkit.style.FeatureTypeStyleListener;
import org.geotoolkit.style.MutableFeatureTypeStyle;
import org.geotoolkit.style.MutableRule;
import org.geotoolkit.style.MutableStyle;
import org.geotoolkit.style.RandomStyleBuilder;
import org.geotoolkit.style.RuleListener;
import org.geotoolkit.style.StyleListener;
import org.geotoolkit.style.StyleUtilities;
import org.geotoolkit.util.collection.CollectionChangeEvent;
import org.opengis.style.FeatureTypeStyle;
import org.opengis.style.Rule;
import org.opengis.style.SemanticType;
import org.opengis.style.Style;
import org.opengis.style.Symbolizer;
import org.opengis.util.GenericName;
/**
* Utility classes to build a style editor tree.
*
* @author Johann Sorel (Geomatys)
*/
public class FXStyleTree {
public static final Image ICON_GROUP = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_FOLDER_O,24,FontAwesomeIcons.DEFAULT_COLOR),null);
/**
* Validate the editor for given path.
* If edited object is a symbolizer this method will properly replace the symbolizer.
*
* @param editor can be null
* @param oldPath original tree item
*/
public static void applyTreeItemEditor(final FXStyleElementController editor, final TreeItem oldPath){
if(editor == null) return;
//create implies a call to apply if a style element is present
final Object obj = editor.value.getValue();
if(obj instanceof Symbolizer){
//in case of a symbolizer we must update it.
if(oldPath != null){
final Symbolizer symbol = (Symbolizer) oldPath.getValue();
if(!symbol.equals(obj) && oldPath.getParent()!=null){
oldPath.setValue(obj);
//new symbol created is different, update in the rule
final MutableRule rule = (MutableRule) oldPath.getParent().getValue();
final int index = oldPath.getParent().getChildren().indexOf(oldPath);
if(index >= 0){
rule.symbolizers().set(index, (Symbolizer) obj);
}
}
}
}
}
public static class StyleTreeItem extends TreeItem<Object> implements StyleListener, FeatureTypeStyleListener, RuleListener{
public StyleTreeItem(Object val) {
valueProperty().addListener(new ChangeListener<Object>() {
@Override
public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
updateNode(oldValue, newValue);
updateChildren(null, CollectionChangeEvent.ITEM_ADDED);
}
});
setValue(val);
}
private void updateNode(Object oldValue, Object newValue){
if (oldValue instanceof MutableStyle) {
final MutableStyle style = (MutableStyle) oldValue;
style.removeListener(StyleTreeItem.this);
} else if (oldValue instanceof MutableFeatureTypeStyle) {
final MutableFeatureTypeStyle fts = (MutableFeatureTypeStyle) oldValue;
fts.removeListener(StyleTreeItem.this);
} else if (oldValue instanceof MutableRule) {
final MutableRule r = (MutableRule) oldValue;
r.removeListener(StyleTreeItem.this);
}
final ImageView img = new ImageView();
if (newValue instanceof MutableStyle) {
final MutableStyle style = (MutableStyle) newValue;
style.addListener(StyleTreeItem.this);
img.setImage(GeotkFX.ICON_STYLE);
setGraphic(img);
} else if (newValue instanceof MutableFeatureTypeStyle) {
final MutableFeatureTypeStyle fts = (MutableFeatureTypeStyle) newValue;
fts.addListener(StyleTreeItem.this);
img.setImage(ICON_GROUP);
setGraphic(img);
} else if (newValue instanceof MutableRule) {
final MutableRule r = (MutableRule) newValue;
r.addListener(StyleTreeItem.this);
img.setImage(GeotkFX.ICON_RULE);
setGraphic(img);
} else if (newValue instanceof Symbolizer) {
//do nothing, the name column handle the graphic and label
setGraphic(null);
}
//lbl.setTextAlignment(TextAlignment.CENTER);
//lbl.setContentDisplay(ContentDisplay.LEFT);
}
private void updateChildren(CollectionChangeEvent event, int type){
if(type != CollectionChangeEvent.ITEM_ADDED && type != CollectionChangeEvent.ITEM_REMOVED) return;
// if(type == CollectionChangeEvent.ITEM_CHANGED){
// if(event!=null && event.getChangeEvent()!=null) return;
// }
final Object item = getValue();
//rebuild structure
final Map<Object,StyleTreeItem> cache = new IdentityHashMap<>();
for(TreeItem ti : getChildren()){
cache.put(ti.getValue(), (StyleTreeItem)ti);
}
getChildren().clear();
List itemChildren = Collections.EMPTY_LIST;
if(item instanceof MutableStyle){
itemChildren = ((MutableStyle)item).featureTypeStyles();
}else if(item instanceof MutableFeatureTypeStyle){
itemChildren = ((MutableFeatureTypeStyle)item).rules();
}else if(item instanceof MutableRule){
itemChildren = ((MutableRule)item).symbolizers();
}
for(Object child : itemChildren){
StyleTreeItem tmi = cache.get(child);
if(tmi==null) tmi = new StyleTreeItem(child);
getChildren().add(tmi);
}
}
@Override
public void featureTypeStyleChange(CollectionChangeEvent<MutableFeatureTypeStyle> event) {
updateChildren(event,event.getType());
}
@Override
public void ruleChange(CollectionChangeEvent<MutableRule> event) {
updateChildren(event,event.getType());
}
@Override
public void symbolizerChange(CollectionChangeEvent<Symbolizer> event) {
updateChildren(event,event.getType());
}
@Override
public void featureTypeNameChange(CollectionChangeEvent<GenericName> event) {}
@Override
public void semanticTypeChange(CollectionChangeEvent<SemanticType> event) {}
@Override
public void propertyChange(PropertyChangeEvent evt) {
}
}
private static void hackClearSelection(){
//bug in javafx JDK 8u20 : https://javafx-jira.kenai.com/browse/RT-24055
//clear the selection rather then have an incorrect selection
//tree.getSelectionModel().clearSelection();
}
public static class CollapseAction extends ActionMenuItem{
public CollapseAction() {
super(GeotkFX.getString(FXUserStyle.class, "collapse"),null);
}
@Override
protected void handle(ActionEvent event) {
super.handle(event);
for(TreeItem ti : items){
ti.setExpanded(false);
}
}
}
public static class ExpandAction extends ActionMenuItem{
public ExpandAction() {
super(GeotkFX.getString(FXUserStyle.class, "expand"),null);
}
@Override
protected void handle(ActionEvent event) {
for(TreeItem ti : items){
ti.setExpanded(true);
}
}
}
public static class NewFTSAction extends ActionMenuItem{
public NewFTSAction() {
super(GeotkFX.getString(FXUserStyle.class, "newfts"),GeotkFX.ICON_NEW);
}
@Override
public MenuItem init(List<? extends TreeItem> selectedItems) {
super.init(selectedItems);
return uniqueAndType(selectedItems, MutableStyle.class) ? menuItem : null;
}
@Override
protected void handle(ActionEvent event) {
final MutableStyle style = (MutableStyle) items.get(0).getValue();
style.featureTypeStyles().add(getStyleFactory().featureTypeStyle(
RandomStyleBuilder.createRandomPointSymbolizer()));
hackClearSelection();
}
}
public static class NewRuleAction extends ActionMenuItem{
public NewRuleAction() {
super(GeotkFX.getString(FXUserStyle.class, "newrule"),GeotkFX.ICON_NEW);
}
@Override
public MenuItem init(List<? extends TreeItem> selectedItems) {
super.init(selectedItems);
return uniqueAndType(selectedItems, MutableFeatureTypeStyle.class) ? menuItem : null;
}
@Override
protected void handle(ActionEvent event) {
final MutableFeatureTypeStyle fts = (MutableFeatureTypeStyle) items.get(0).getValue();
fts.rules().add(getStyleFactory().rule(
RandomStyleBuilder.createRandomPointSymbolizer()));
hackClearSelection();
}
}
public static class NewSymbolizerAction extends ActionMenuItem{
private final FXStyleElementController editor;
public NewSymbolizerAction(final FXStyleElementController editor) {
super(getSymbolizerName(editor.getEditedClass().getSimpleName()),GeotkFX.ICON_NEW);
this.editor = editor;
}
@Override
public MenuItem init(List<? extends TreeItem> selectedItems) {
super.init(selectedItems);
return uniqueAndType(selectedItems, MutableRule.class) ? menuItem : null;
}
@Override
protected void handle(ActionEvent event) {
final MutableRule rule = (MutableRule) items.get(0).getValue();
rule.symbolizers().add((Symbolizer)editor.newValue());
hackClearSelection();
}
}
public static class DuplicateAction extends ActionMenuItem {
public DuplicateAction() {
super(GeotkFX.getString(FXUserStyle.class, "duplicate"), GeotkFX.ICON_DUPLICATE);
}
@Override
public MenuItem init(List<? extends TreeItem> selectedItems) {
super.init(selectedItems);
return uniqueAndType(selectedItems, Object.class) ? menuItem : null;
}
@Override
protected void handle(ActionEvent event) {
for(TreeItem ti : items){
if(ti.getParent()==null) continue;
final Object child = ti.getValue();
final Object parent = ti.getParent().getValue();
if (child instanceof MutableFeatureTypeStyle) {
final MutableFeatureTypeStyle fts = StyleUtilities.copy((MutableFeatureTypeStyle) child);
final int index = ((MutableStyle)parent).featureTypeStyles().indexOf(child) + 1;
((MutableStyle) parent).featureTypeStyles().add(index, fts);
} else if (child instanceof MutableRule) {
final MutableRule rule = StyleUtilities.copy((MutableRule) child);
final int index = ((MutableFeatureTypeStyle)parent).rules().indexOf(child) + 1;
((MutableFeatureTypeStyle) parent).rules().add(index, rule);
} else if (child instanceof Symbolizer) {
//no need to copy symbolizer, they are immutable
final Symbolizer symbol = (Symbolizer) child;
final int index = ((MutableRule)parent).symbolizers().indexOf(child) + 1;
((MutableRule) parent).symbolizers().add(index, symbol);
}
}
hackClearSelection();
}
}
public static class DeleteAction extends ActionMenuItem {
public DeleteAction() {
super(GeotkFX.getString(FXUserStyle.class, "delete"),GeotkFX.ICON_DELETE);
}
@Override
public MenuItem init(List<? extends TreeItem> selectedItems) {
super.init(selectedItems);
if(selectedItems.isEmpty()) return null;
return menuItem;
}
@Override
protected void handle(ActionEvent event) {
for(TreeItem ti : items){
if(ti.getParent()==null) continue;
final Object child = ti.getValue();
final Object parent = ti.getParent().getValue();
if(parent instanceof MutableStyle){
((MutableStyle)parent).featureTypeStyles().remove(child);
}else if(parent instanceof MutableFeatureTypeStyle){
((MutableFeatureTypeStyle)parent).rules().remove(child);
}else if(parent instanceof MutableRule){
((MutableRule)parent).symbolizers().remove(child);
}
}
hackClearSelection();
}
}
public static class NameColumn extends TreeTableColumn<Object,Object>{
public NameColumn() {
setCellValueFactory(new Callback<CellDataFeatures<Object, Object>, ObservableValue<Object>>() {
@Override
public ObservableValue<Object> call(CellDataFeatures<Object, Object> param) {
return new SimpleObjectProperty<>(param.getValue());
}
});
setCellFactory((TreeTableColumn<Object, Object> param) -> new SymbolizerCell());
setPrefWidth(200);
setMinWidth(120);
}
}
private static class SymbolizerCell extends TreeTableCell<Object, Object>{
private final ImageView glyphView = new ImageView();
public SymbolizerCell() {
}
@Override
protected void updateItem(Object obj, boolean empty) {
super.updateItem(obj, empty);
textProperty().unbind();
setText("");
setGraphic(null);
if(obj instanceof TreeItem) obj = ((TreeItem)obj).getValue();
if(empty || obj==null) return;
if(obj instanceof Style){
ObservableStringValue beanProperty = (ObservableStringValue)FXUtilities.beanProperty(obj, "name", String.class);
textProperty().bind(placeholderBinding(beanProperty, GeotkFX.getString(FXStyleTree.class, "defaultStyleName")));
}else if(obj instanceof FeatureTypeStyle){
ObservableStringValue beanProperty = (ObservableStringValue)FXUtilities.beanProperty(obj, "name", String.class);
textProperty().bind(placeholderBinding(beanProperty, GeotkFX.getString(FXStyleTree.class, "defaultFTSName")));
}else if(obj instanceof Rule){
ObservableStringValue beanProperty = (ObservableStringValue)FXUtilities.beanProperty(obj, "name", String.class);
textProperty().bind(placeholderBinding(beanProperty, GeotkFX.getString(FXStyleTree.class, "defaultRuleName")));
}else if(obj instanceof Symbolizer){
final Symbolizer symb = (Symbolizer) obj;
final Dimension dim = DefaultGlyphService.glyphPreferredSize(symb, null, null);
final BufferedImage imge = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB);
DefaultGlyphService.render(symb, new Rectangle(dim),imge.createGraphics(),null);
glyphView.setImage(SwingFXUtils.toFXImage(imge, null));
setGraphic(glyphView);
setText(symb.getName());
}
}
}
public static class ShowStylePaneAction extends ActionMenuItem {
private MapLayer mapLayer;
private final FXLayerStylePane stylePane;
public ShowStylePaneAction(FXLayerStylePane stylePane, String title) {
super(title, GeotkFX.ICON_DUPLICATE);
this.stylePane = stylePane;
}
public MapLayer getMapLayer() {
return mapLayer;
}
public void setMapLayer(MapLayer mapLayer) {
this.mapLayer = mapLayer;
}
@Override
public MenuItem init(List<? extends TreeItem> selectedItems) {
super.init(selectedItems);
if(!(mapLayer instanceof FeatureMapLayer)) return null;
return uniqueAndType(selectedItems, MutableFeatureTypeStyle.class) ? menuItem : null;
}
@Override
protected void handle(ActionEvent event) {
for(TreeItem ti : items){
if(ti.getParent()==null) continue;
final Object child = ti.getValue();
if (child instanceof MutableFeatureTypeStyle) {
final MutableFeatureTypeStyle fts = (MutableFeatureTypeStyle)child;
stylePane.init(mapLayer, fts);
final DialogPane pane = new DialogPane();
pane.setContent(stylePane);
pane.getButtonTypes().addAll(ButtonType.CLOSE);
final Dialog dialog = new Dialog();
dialog.setTitle(stylePane.getTitle());
dialog.initModality(Modality.WINDOW_MODAL);
dialog.setResizable(true);
dialog.setDialogPane(pane);
dialog.resultProperty().addListener(new ChangeListener() {
@Override
public void changed(ObservableValue observable, Object oldValue, Object newValue) {
dialog.close();
}
});
dialog.show();
}
}
hackClearSelection();
}
}
private static ObservableValue<String> placeholderBinding(ObservableStringValue base, String placeholder){
return when(or(equal(base,""),isNull(base))).then(placeholder).otherwise(base);
}
private static String getSymbolizerName(String name){
String str = GeotkFX.getString(FXUserStyle.class, name);
if(str.startsWith("Missing")){
str = name.replace("Symbolizer", "");
}
return str;
}
}