/*
* 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.layer.style;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.SplitMenuButton;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.image.ImageView;
import javafx.util.Callback;
import org.apache.sis.util.iso.SimpleInternationalString;
import org.geotoolkit.cql.CQL;
import org.geotoolkit.cql.CQLException;
import org.geotoolkit.display2d.service.DefaultGlyphService;
import org.geotoolkit.gui.javafx.filter.FXCQLEditor;
import org.geotoolkit.gui.javafx.layer.FXLayerStylePane;
import org.geotoolkit.gui.javafx.layer.FXPropertyPane;
import org.geotoolkit.gui.javafx.style.FXPaletteCell;
import org.geotoolkit.gui.javafx.util.ButtonTableCell;
import org.geotoolkit.gui.javafx.util.FXDeleteTableColumn;
import org.geotoolkit.gui.javafx.util.FXNumberSpinner;
import org.geotoolkit.image.palette.PaletteFactory;
import org.geotoolkit.internal.GeotkFX;
import org.geotoolkit.internal.Loggers;
import org.geotoolkit.map.FeatureMapLayer;
import org.geotoolkit.map.MapLayer;
import org.geotoolkit.style.MutableFeatureTypeStyle;
import org.geotoolkit.style.MutableRule;
import org.geotoolkit.style.MutableStyle;
import org.geotoolkit.style.interval.DefaultIntervalPalette;
import org.geotoolkit.style.interval.IntervalPalette;
import org.geotoolkit.style.interval.IntervalStyleBuilder;
import org.opengis.filter.Filter;
import org.opengis.filter.expression.PropertyName;
import org.opengis.style.Description;
import org.opengis.style.Symbolizer;
/**
*
* @author Johann Sorel (Geomatys)
*/
public class FXStyleClassifRangePane extends FXLayerStylePane {
private static final PaletteFactory PF = PaletteFactory.getDefault();
private static final List<IntervalPalette> PALETTES;
private static final Dimension GLYPH_DIMENSION = new Dimension(30, 20);
static{
PALETTES = new ArrayList<>();
final Set<String> paletteNames = PF.getAvailableNames();
for (String palName : paletteNames) {
try {
PALETTES.add(new DefaultIntervalPalette(PF.getColors(palName)));
} catch (IOException ex) {
Loggers.JAVAFX.log(Level.WARNING, ex.getMessage(), ex);
}
}
}
@FXML private ComboBox<PropertyName> uiProperty;
@FXML private ComboBox<IntervalStyleBuilder.METHOD> uiMethod;
@FXML private ComboBox<PropertyName> uiNormalize;
@FXML private SplitMenuButton uiTemplate;
@FXML private FXNumberSpinner uiClasses;
@FXML private ComboBox<Object> uiPalette;
@FXML private TableView<MutableRule> uiTable;
@FXML private Button uiCombineFilter;
//this is the target style element where we must generate the rules
//it can be a MutableStyle or a MutableFeatureTypeStyle
private Object targetStyleElement;
private Filter combineFilter = Filter.INCLUDE;
private final IntervalStyleBuilder analyze = new IntervalStyleBuilder();
private FeatureMapLayer layer;
public FXStyleClassifRangePane() {
GeotkFX.loadJRXML(this,FXStyleClassifRangePane.class);
}
@FXML
private void propertyChange(ActionEvent event) {
analyze.setClassification(uiProperty.getSelectionModel().getSelectedItem());
updateNormalizeList();
}
@FXML
private void normalizeChange(ActionEvent event) {
final PropertyName prop = uiNormalize.getSelectionModel().getSelectedItem();
analyze.setNormalize(prop);
}
@FXML
private void methodChange(ActionEvent event) {
analyze.setMethod(uiMethod.getSelectionModel().getSelectedItem());
}
@FXML
private void editTemplate(ActionEvent event) {
final Symbolizer template = FXPropertyPane.showSymbolizerDialog(this, analyze.getTemplate(), layer);
analyze.setTemplate(template);
updateTemplateGlyph();
}
@FXML
private void generate(ActionEvent event) {
analyze.setClassification(uiProperty.getValue());
analyze.setMethod(uiMethod.getValue());
analyze.setNbClasses(uiClasses.valueProperty().get().intValue());
analyze.setNormalize(uiNormalize.getValue());
uiTable.getItems().setAll(analyze.generateRules((IntervalPalette) uiPalette.getSelectionModel().getSelectedItem(),combineFilter));
}
@FXML
private void editCombineFilter(ActionEvent event) {
try {
Filter f = FXCQLEditor.showFilterDialog(this, layer, combineFilter);
if(f!=null){
combineFilter = f;
uiCombineFilter.setTooltip(new Tooltip(CQL.write(combineFilter)));
}
} catch (CQLException ex) {
Loggers.JAVAFX.log(Level.INFO, ex.getMessage(),ex);
}
}
@FXML
private void invertValues(ActionEvent event) {
final List<MutableRule> rules = new ArrayList<>(uiTable.getItems());
final Symbolizer[] symbols = new Symbolizer[rules.size()];
for(int i=0;i<rules.size();i++){
symbols[rules.size()-1-i] = rules.get(i).symbolizers().get(0);
}
for(int i=0;i<rules.size();i++){
rules.get(i).symbolizers().clear();
rules.get(i).symbolizers().add(symbols[i]);
}
uiTable.getItems().clear();
uiTable.getItems().setAll(rules);
}
@FXML
private void removeAll(ActionEvent event) {
uiTable.getItems().clear();
}
@FXML
private void apply(ActionEvent event) {
if(layer==null) return;
if(targetStyleElement instanceof MutableStyle){
final List<MutableFeatureTypeStyle> ftss = ((MutableStyle)targetStyleElement).featureTypeStyles();
final MutableFeatureTypeStyle fts;
if(ftss.isEmpty()){
fts = GeotkFX.getStyleFactory().featureTypeStyle();
layer.getStyle().featureTypeStyles().add(fts);
}else{
fts = ftss.get(0);
}
fts.rules().clear();
fts.rules().addAll(uiTable.getItems());
}else if(targetStyleElement instanceof MutableFeatureTypeStyle){
final MutableFeatureTypeStyle fts = (MutableFeatureTypeStyle) targetStyleElement;
fts.rules().clear();
fts.rules().addAll(uiTable.getItems());
}
}
@Override
public String getTitle() {
return GeotkFX.getString(this,"title");
}
@Override
public String getCategory() {
return GeotkFX.getString(this,"category");
}
/**
* Called by FXMLLoader after creating controller.
*/
public void initialize(){
uiPalette.setItems(FXCollections.observableArrayList(PALETTES));
uiPalette.setCellFactory((ListView<Object> param) -> new FXPaletteCell());
uiPalette.setButtonCell((new FXPaletteCell()));
uiPalette.setEditable(false);
uiPalette.getSelectionModel().selectFirst();
uiProperty.setCellFactory((ListView<PropertyName> param) -> new FXPropertyCell());
uiProperty.setButtonCell((new FXPropertyCell()));
uiProperty.setEditable(false);
uiNormalize.setCellFactory((ListView<PropertyName> param) -> new FXPropertyCell());
uiNormalize.setButtonCell((new FXPropertyCell()));
uiNormalize.setEditable(false);
uiMethod.setEditable(false);
uiMethod.getItems().clear();
uiMethod.getItems().add(IntervalStyleBuilder.METHOD.EL);
uiMethod.getItems().add(IntervalStyleBuilder.METHOD.QANTILE);
uiMethod.getItems().add(IntervalStyleBuilder.METHOD.MANUAL);
uiClasses.getSpinner().setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, Integer.MAX_VALUE, 0, 1));
uiClasses.valueProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
analyze.setNbClasses(uiClasses.valueProperty().get().intValue());
});
uiTable.setItems(FXCollections.observableArrayList());
uiTable.getColumns().add(new GlyphColumn());
uiTable.getColumns().add(new NameColumn());
uiTable.getColumns().add(new FilterColumn());
uiTable.getColumns().add(new FXDeleteTableColumn(false));
//this will cause the column width to fit the view area
uiTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
final MenuItem miPoint = new MenuItem(GeotkFX.getString(FXStyleClassifRangePane.class, "pointTemplate"));
miPoint.setOnAction((ActionEvent event) -> {
analyze.setTemplate(IntervalStyleBuilder.createPointTemplate());
updateTemplateGlyph();
});
final MenuItem miLine = new MenuItem(GeotkFX.getString(FXStyleClassifRangePane.class, "lineTemplate"));
miLine.setOnAction((ActionEvent event) -> {
analyze.setTemplate(IntervalStyleBuilder.createLineTemplate());
updateTemplateGlyph();
});
final MenuItem miPolygon = new MenuItem(GeotkFX.getString(FXStyleClassifRangePane.class, "polygonTemplate"));
miPolygon.setOnAction((ActionEvent event) -> {
analyze.setTemplate(IntervalStyleBuilder.createPolygonTemplate());
updateTemplateGlyph();
});
uiTemplate.getItems().clear();
uiTemplate.getItems().add(miPoint);
uiTemplate.getItems().add(miLine);
uiTemplate.getItems().add(miPolygon);
uiCombineFilter.setGraphic(new ImageView(GeotkFX.ICON_FILTER));
}
@Override
public boolean init(MapLayer candidate, Object styleElement) {
if(!(candidate instanceof FeatureMapLayer)) return false;
if(styleElement==null) styleElement = candidate.getStyle();
this.targetStyleElement = styleElement;
this.layer = (FeatureMapLayer) candidate;
analyze.setLayer(layer);
uiTable.getItems().clear();
if(styleElement instanceof MutableStyle){
if(analyze.isIntervalStyle((MutableStyle)styleElement)){
uiTable.getItems().addAll(layer.getStyle().featureTypeStyles().get(0).rules());
}
}else if(styleElement instanceof MutableFeatureTypeStyle){
if(analyze.isIntervalStyle((MutableFeatureTypeStyle)styleElement)){
uiTable.getItems().addAll( ((MutableFeatureTypeStyle)styleElement).rules());
}
}
final List<PropertyName> props = analyze.getProperties();
uiProperty.setItems(FXCollections.observableArrayList(props));
uiProperty.getSelectionModel().selectFirst();
updateNormalizeList();
updateTemplateGlyph();
uiMethod.getSelectionModel().select(analyze.getMethod());
uiClasses.valueProperty().set(analyze.getNbClasses());
return true;
}
private void updateNormalizeList(){
final PropertyName oldSelected = uiNormalize.getSelectionModel().getSelectedItem();
final List<PropertyName> lstnormalize = new ArrayList<>();
lstnormalize.add(analyze.noValue);
lstnormalize.addAll(analyze.getProperties());
lstnormalize.remove(uiProperty.getSelectionModel().getSelectedItem());
uiNormalize.setItems(FXCollections.observableList(lstnormalize));
if(oldSelected != null){
uiNormalize.getSelectionModel().select(oldSelected);
}
if(uiNormalize.getSelectionModel().getSelectedItem() == null){
uiNormalize.getSelectionModel().select(analyze.noValue);
}
}
private void updateTemplateGlyph(){
final Symbolizer template = analyze.getTemplate();
if(template == null){
uiTemplate.setGraphic(null);
uiTemplate.setText("...");
}else{
final BufferedImage img = new BufferedImage(30, 20, BufferedImage.TYPE_INT_ARGB);
DefaultGlyphService.render(template, new Rectangle(GLYPH_DIMENSION), img.createGraphics(),null);
uiTemplate.setGraphic(new ImageView(SwingFXUtils.toFXImage(img, null)));
uiTemplate.setText("");
}
}
@Override
public MutableStyle getMutableStyle() {
final MutableStyle style = GeotkFX.getStyleFactory().style();
final MutableFeatureTypeStyle fts = GeotkFX.getStyleFactory().featureTypeStyle();
style.featureTypeStyles().add(fts);
fts.rules().addAll(uiTable.getItems());
return style;
}
private static final class FXPropertyCell extends ListCell<PropertyName>{
@Override
protected void updateItem(PropertyName item, boolean empty) {
super.updateItem(item, empty);
if(item instanceof PropertyName){
setText(item.getPropertyName());
}else{
setText("");
}
}
}
private final class GlyphColumn extends TableColumn<MutableRule,Symbolizer>{
public GlyphColumn() {
super();
setMinWidth(36);
setMaxWidth(36);
setEditable(true);
setCellValueFactory(new Callback<TableColumn.CellDataFeatures<MutableRule, Symbolizer>, ObservableValue<Symbolizer>>() {
@Override
public ObservableValue<Symbolizer> call(TableColumn.CellDataFeatures<MutableRule, Symbolizer> param) {
return new SimpleObjectProperty<>(param.getValue().symbolizers().get(0));
}
});
setCellFactory(new Callback<TableColumn<MutableRule, Symbolizer>, TableCell<MutableRule, Symbolizer>>() {
@Override
public TableCell<MutableRule, Symbolizer> call(TableColumn<MutableRule, Symbolizer> param) {
return new ButtonTableCell<MutableRule,Symbolizer>(false,null,null,new Function<Symbolizer,Symbolizer>() {
@Override
public Symbolizer apply(Symbolizer t) {
return FXPropertyPane.showSymbolizerDialog(null, t, layer);
}
}){
@Override
protected void updateItem(Symbolizer item, boolean empty) {
super.updateItem(item, empty);
if(item!=null){
button.setText(null);
final BufferedImage img = new BufferedImage(30, 20, BufferedImage.TYPE_INT_ARGB);
DefaultGlyphService.render(item, new Rectangle(GLYPH_DIMENSION), img.createGraphics(),null);
button.setGraphic(new ImageView(SwingFXUtils.toFXImage(img, null)));
}
}
};
}
});
addEventHandler((EventType)TableColumn.editCommitEvent(), new EventHandler<Event>() {
@Override
public void handle(Event evt) {
final TableColumn.CellEditEvent<MutableRule, Symbolizer> event = (TableColumn.CellEditEvent<MutableRule, Symbolizer>) evt;
//BUG : next line raise a nullpointer, bug in javafx ?
final MutableRule rule = event.getRowValue();
// final MutableRule rule = uiTable.getItems().get(event.getTablePosition().getRow());
rule.symbolizers().set(0, event.getNewValue());
}
});
}
}
private static final class NameColumn extends TableColumn<MutableRule,String>{
public NameColumn() {
super(GeotkFX.getString(FXStyleClassifSinglePane.class, "name"));
setMinWidth(80);
setEditable(true);
setCellValueFactory(new Callback<TableColumn.CellDataFeatures<MutableRule, String>, ObservableValue<String>>() {
@Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<MutableRule, String> param) {
final Description desc = param.getValue().getDescription();
return new SimpleStringProperty(String.valueOf(desc.getTitle()));
}
});
setCellFactory(TextFieldTableCell.forTableColumn());
setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<MutableRule, String>>() {
@Override
public void handle(TableColumn.CellEditEvent<MutableRule, String> event) {
final MutableRule rule = event.getRowValue();
final Description desc = GeotkFX.getStyleFactory().description(
new SimpleInternationalString(event.getNewValue()),
rule.getDescription().getAbstract());
rule.setDescription(desc);
}
});
}
}
private final class FilterColumn extends TableColumn<MutableRule,Filter>{
public FilterColumn() {
super(GeotkFX.getString(FXStyleClassifSinglePane.class, "filter"));
setMinWidth(80);
setEditable(true);
setCellValueFactory(new Callback<TableColumn.CellDataFeatures<MutableRule, Filter>, ObservableValue<Filter>>() {
@Override
public ObservableValue<Filter> call(TableColumn.CellDataFeatures<MutableRule, Filter> param) {
return new SimpleObjectProperty<>(param.getValue().getFilter());
}
});
setCellFactory(new Callback<TableColumn<MutableRule, Filter>, TableCell<MutableRule, Filter>>() {
@Override
public TableCell<MutableRule, Filter> call(TableColumn<MutableRule, Filter> param) {
return new ButtonTableCell<MutableRule,Filter>(false,null,null,new Function<Filter,Filter>() {
@Override
public Filter apply(Filter t) {
try{
return FXCQLEditor.showFilterDialog(null, layer, t);
}catch(CQLException ex){
ex.printStackTrace();
}
return t;
}
}){
@Override
protected void updateItem(Filter item, boolean empty) {
super.updateItem(item, empty);
if(item!=null){
button.setText(CQL.write(item));
}
}
};
}
});
setOnEditCommit((TableColumn.CellEditEvent<MutableRule, Filter> event) -> {
event.getRowValue().setFilter(event.getNewValue());
});
}
}
}