/* * 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 com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Polygon; import java.awt.Color; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; 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.collections.ObservableList; 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.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.MenuItem; import javafx.scene.control.SplitMenuButton; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextInputDialog; import javafx.scene.control.Tooltip; import javafx.scene.control.cell.TextFieldTableCell; import javafx.scene.image.ImageView; import javafx.util.Callback; import org.apache.sis.feature.FeatureExt; import org.apache.sis.storage.DataStoreException; import org.apache.sis.util.iso.SimpleInternationalString; import org.geotoolkit.cql.CQL; import org.geotoolkit.cql.CQLException; import org.geotoolkit.data.FeatureIterator; import org.geotoolkit.data.FeatureStoreRuntimeException; import org.geotoolkit.data.query.Query; import org.geotoolkit.data.query.QueryBuilder; import org.geotoolkit.display2d.GO2Utilities; import org.geotoolkit.display2d.service.DefaultGlyphService; import org.opengis.feature.Feature; 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.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.MutableStyleFactory; import org.geotoolkit.style.StyleConstants; import org.geotoolkit.style.interval.DefaultIntervalPalette; import org.geotoolkit.style.interval.DefaultRandomPalette; import org.geotoolkit.style.interval.IntervalStyleBuilder; import org.geotoolkit.style.interval.Palette; import org.geotoolkit.style.interval.RandomPalette; import org.opengis.feature.AttributeType; import org.opengis.feature.FeatureType; import org.opengis.feature.PropertyType; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.filter.PropertyIsEqualTo; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; import org.opengis.filter.expression.PropertyName; import org.opengis.style.Description; import org.opengis.style.Fill; import org.opengis.style.Graphic; import org.opengis.style.GraphicalSymbol; import org.opengis.style.LineSymbolizer; import org.opengis.style.Mark; import org.opengis.style.PointSymbolizer; import org.opengis.style.PolygonSymbolizer; import org.opengis.style.Rule; import org.opengis.style.Stroke; import org.opengis.style.Symbolizer; /** * * @author Johann Sorel (Geomatys) */ public class FXStyleClassifSinglePane extends FXLayerStylePane { private static final Dimension GLYPH_DIMENSION = new Dimension(30, 20); private static final RandomPalette RANDOM_PALETTE = new DefaultRandomPalette(); private static final PaletteFactory PF = PaletteFactory.getDefault(); private static final List<Palette> PALETTES; static{ PALETTES = new ArrayList<>(); final Set<String> paletteNames = PF.getAvailableNames(); PALETTES.add(RANDOM_PALETTE); for (String palName : paletteNames) { if("pastel".equals(palName) || "bright".equals(palName) || "dark".equals(palName)) 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 CheckBox uiOther; @FXML private TableView<MutableRule> uiTable; @FXML private ComboBox<Palette> uiPalette; @FXML private SplitMenuButton uiTemplate; @FXML private Button uiCombineFilter; private FeatureMapLayer layer; private Symbolizer template; //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; public FXStyleClassifSinglePane() { GeotkFX.loadJRXML(this,FXStyleClassifSinglePane.class); } @FXML private void editTemplate(ActionEvent event) { template = FXPropertyPane.showSymbolizerDialog(this, template, layer); updateTemplateGlyph(); } @FXML private void generate(ActionEvent event) { final ObservableList<MutableRule> rules = create(uiProperty.getSelectionModel().getSelectedItem(), uiOther.isSelected()); uiTable.setItems(rules); } @FXML private void addValue(ActionEvent event) { final TextInputDialog dialog = new TextInputDialog(); dialog.setHeaderText(GeotkFX.getString(FXStyleClassifSinglePane.class,"newvalue")); final Optional<String> str = dialog.showAndWait(); if(str.get()==null)return; final String val = str.get(); final MutableRule r = createRule(uiProperty.getSelectionModel().getSelectedItem(), val,uiTable.getItems().size()); uiTable.getItems().add(r); } @FXML private void editCombineFilter(ActionEvent event) { try { final 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 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(); 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(FXStyleClassifSinglePane.class,"title"); } @Override public String getCategory() { return GeotkFX.getString(FXStyleClassifSinglePane.class,"category"); } private Class<? extends Symbolizer> getExpectedType(){ if(template instanceof PointSymbolizer){ return PointSymbolizer.class; }else if(template instanceof LineSymbolizer){ return LineSymbolizer.class; }else if(template instanceof PolygonSymbolizer){ return PolygonSymbolizer.class; }else{ return PointSymbolizer.class; } } /** * Called by FXMLLoader after creating controller. */ public void initialize(){ uiPalette.setItems(FXCollections.observableArrayList(PALETTES)); uiPalette.setCellFactory((ListView<Palette> param) -> new FXPaletteCell(false)); uiPalette.setButtonCell((new FXPaletteCell(false))); uiPalette.setEditable(false); uiProperty.setCellFactory((ListView<PropertyName> param) -> new FXPropertyCell()); uiProperty.setButtonCell((new FXPropertyCell())); uiProperty.setEditable(false); 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(FXStyleClassifSinglePane.class, "pointTemplate")); miPoint.setOnAction((ActionEvent event) -> { template = IntervalStyleBuilder.createPointTemplate(); updateTemplateGlyph(); }); final MenuItem miLine = new MenuItem(GeotkFX.getString(FXStyleClassifSinglePane.class, "lineTemplate")); miLine.setOnAction((ActionEvent event) -> { template = IntervalStyleBuilder.createLineTemplate(); updateTemplateGlyph(); }); final MenuItem miPolygon = new MenuItem(GeotkFX.getString(FXStyleClassifSinglePane.class, "polygonTemplate")); miPolygon.setOnAction((ActionEvent event) -> { template = 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; uiOther.setSelected(false); uiProperty.setItems(listProperties(layer)); template = generateTemplate(layer); uiProperty.getSelectionModel().selectFirst(); uiPalette.getSelectionModel().selectFirst(); updateTemplateGlyph(); if(styleElement instanceof MutableStyle){ tryRebuildExisting((MutableStyle)styleElement); }else if(styleElement instanceof MutableFeatureTypeStyle){ tryRebuildExisting((MutableFeatureTypeStyle)styleElement); } return true; } private void updateTemplateGlyph(){ if(template==null){ uiTemplate.setText("..."); uiTemplate.setGraphic(null); }else{ final BufferedImage img = new BufferedImage(30, 20, BufferedImage.TYPE_INT_ARGB); DefaultGlyphService.render(template, new Rectangle(GLYPH_DIMENSION), img.createGraphics(),null); uiTemplate.setText(""); uiTemplate.setGraphic(new ImageView(SwingFXUtils.toFXImage(img, null))); } } @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 ObservableList listProperties(FeatureMapLayer layer){ final ObservableList properties = FXCollections.observableArrayList(); if(layer != null){ final FeatureType schema = layer.getCollection().getFeatureType(); for(PropertyType desc : schema.getProperties(true)){ if(desc instanceof AttributeType){ final Class<?> type = ((AttributeType)desc).getValueClass(); if(!Geometry.class.isAssignableFrom(type)){ properties.add(GeotkFX.getFilterFactory().property(desc.getName().tip().toString())); } } } } return properties; } private Symbolizer generateTemplate(FeatureMapLayer layer){ Symbolizer template = null; if(layer != null){ final FeatureType schema = layer.getCollection().getFeatureType(); //find the geometry class for template final AttributeType<?> geo = FeatureExt.getDefaultGeometryAttribute(schema); final Class<?> geoClass = (geo!=null)?geo.getValueClass():null; final MutableStyleFactory sf = GeotkFX.getStyleFactory(); final FilterFactory ff = GeotkFX.getFilterFactory(); if(geoClass!=null && (Polygon.class.isAssignableFrom(geoClass) || MultiPolygon.class.isAssignableFrom(geoClass))){ final Stroke stroke = sf.stroke(Color.BLACK, 1); final Fill fill = sf.fill(Color.BLUE); template = sf.polygonSymbolizer(stroke,fill,null); }else if(geoClass!=null && (LineString.class.isAssignableFrom(geoClass) || MultiLineString.class.isAssignableFrom(geoClass))){ final Stroke stroke = sf.stroke(Color.BLUE, 2); template = sf.lineSymbolizer(stroke,null); }else{ final Stroke stroke = sf.stroke(Color.BLACK, 1); final Fill fill = sf.fill(Color.BLUE); final List<GraphicalSymbol> symbols = new ArrayList<GraphicalSymbol>(); symbols.add(sf.mark(StyleConstants.MARK_CIRCLE, fill, stroke)); final Graphic gra = sf.graphic(symbols, ff.literal(1), ff.literal(12), ff.literal(0), sf.anchorPoint(), sf.displacement()); template = sf.pointSymbolizer(gra, null); } } return template; } private void tryRebuildExisting(MutableStyle style){ //try to rebuild the previous analyze if it was one final List<MutableFeatureTypeStyle> ftss = style.featureTypeStyles(); if(ftss.size() == 1){ tryRebuildExisting(ftss.get(0)); }else{ tryRebuildExisting((MutableFeatureTypeStyle)null); } } private void tryRebuildExisting(MutableFeatureTypeStyle fts){ final ObservableList<MutableRule> candidates = FXCollections.observableArrayList(); boolean hasOther = false; PropertyName currentProperty = null; //try to rebuild the previous analyze if it was one final Class<? extends Symbolizer> expectedType = getExpectedType(); final List<PropertyName> properties = uiProperty.getItems(); if(fts != null){ //defensive copy avoid synchronization final List<MutableRule> candidateRules = new ArrayList<>(fts.rules()); for(Rule r : candidateRules){ //defensive copy avoid synchronization final List<? extends Symbolizer> candidateSymbols = new ArrayList<>(r.symbolizers()); if(candidateSymbols.size() != 1) return; final Symbolizer symbol = candidateSymbols.get(0); if(expectedType.isInstance(symbol)){ if(r.isElseFilter()){ //it looks like it's a valid classification "other" rule candidates.add((MutableRule) r); template = symbol; hasOther = true; }else{ Filter f = r.getFilter(); if(f != null && f instanceof PropertyIsEqualTo){ PropertyIsEqualTo equal = (PropertyIsEqualTo) f; Expression exp1 = equal.getExpression1(); Expression exp2 = equal.getExpression2(); if(exp1 instanceof PropertyName && exp2 instanceof Literal){ if(properties.contains(exp1)){ //it looks like it's a valid classification property rule candidates.add((MutableRule) r); template = symbol; currentProperty = (PropertyName) exp1; }else{ //property is not in the schema return; } }else if(exp2 instanceof PropertyName && exp1 instanceof Literal){ if(properties.contains(exp2)){ //it looks like it's a valid classification property rule candidates.add((MutableRule) r); template = symbol; currentProperty = (PropertyName) exp2; }else{ //property is not in the schema return; } }else{ //mismatch analyze structure return; } } } }else{ return; } } } uiOther.setSelected(hasOther); uiProperty.getSelectionModel().select(currentProperty); uiTable.getItems().setAll(candidates); } private ObservableList<MutableRule> create(PropertyName property, boolean other){ //search the different values final Set<Object> differentValues = new HashSet<Object>(); final QueryBuilder builder = new QueryBuilder(); builder.setTypeName(layer.getCollection().getFeatureType().getName()); builder.setProperties(new String[]{property.getPropertyName()}); final Query query = builder.buildQuery(); FeatureIterator features = null; try{ features = layer.getCollection().subCollection(query).iterator(); while(features.hasNext()){ final Feature feature = features.next(); differentValues.add(property.evaluate(feature)); } }catch(DataStoreException ex){ ex.printStackTrace(); }catch(FeatureStoreRuntimeException ex){ ex.printStackTrace(); }finally{ if(features != null){ features.close(); } } final MutableStyleFactory sf = GeotkFX.getStyleFactory(); final ObservableList<MutableRule> rules = FXCollections.observableArrayList(); int idx = 0; for(Object obj : differentValues){ MutableRule rule = createRule(property, obj,idx); if(combineFilter!=null && !Filter.INCLUDE.equals(combineFilter)){ rule.setFilter(GO2Utilities.FILTER_FACTORY.and(combineFilter, rule.getFilter())); } rules.add(rule); idx++; } //generate the other rule if asked if(other){ MutableRule r = sf.rule(createSymbolizer(idx)); r.setElseFilter(true); r.setDescription(sf.description("other", "other")); rules.add(r); } return rules; } protected Symbolizer createSymbolizer(int idx){ final Palette palette = uiPalette.getValue(); if(palette instanceof DefaultIntervalPalette){ final DefaultIntervalPalette ip = (DefaultIntervalPalette) palette; final int[] argb = ip.getARGB(); idx %= argb.length; return derivateSymbolizer(template, new Color(argb[idx])); } return derivateSymbolizer(template, RANDOM_PALETTE.next()); } /** * Derivate a symbolizer with a new color. * @param symbol original symbolizer * @param color new color * @return derivate symbolizer */ protected Symbolizer derivateSymbolizer(final Symbolizer symbol, final Color color){ final MutableStyleFactory sf = GeotkFX.getStyleFactory(); if(symbol instanceof PolygonSymbolizer){ PolygonSymbolizer ps = (PolygonSymbolizer)symbol; Fill fill = sf.fill(sf.literal(color),ps.getFill().getOpacity()); return sf.polygonSymbolizer(ps.getName(), ps.getGeometryPropertyName(), ps.getDescription(), ps.getUnitOfMeasure(), ps.getStroke(),fill,ps.getDisplacement(),ps.getPerpendicularOffset()); }else if(symbol instanceof LineSymbolizer){ LineSymbolizer ls = (LineSymbolizer) symbol; Stroke oldStroke = ls.getStroke(); Stroke stroke = sf.stroke(sf.literal(color),oldStroke.getOpacity(),oldStroke.getWidth(), oldStroke.getLineJoin(),oldStroke.getLineCap(),oldStroke.getDashArray(),oldStroke.getDashOffset()); return sf.lineSymbolizer(ls.getName(), ls.getGeometryPropertyName(), ls.getDescription(), ls.getUnitOfMeasure(), stroke, ls.getPerpendicularOffset()); }else if(symbol instanceof PointSymbolizer){ PointSymbolizer ps = (PointSymbolizer) symbol; Graphic oldGraphic = ps.getGraphic(); Mark oldMark = (Mark) oldGraphic.graphicalSymbols().get(0); Fill fill = sf.fill(sf.literal(color),oldMark.getFill().getOpacity()); List<GraphicalSymbol> symbols = new ArrayList<GraphicalSymbol>(); symbols.add(sf.mark(oldMark.getWellKnownName(), fill, oldMark.getStroke())); Graphic graphic = sf.graphic(symbols, oldGraphic.getOpacity(),oldGraphic.getSize(), oldGraphic.getRotation(),oldGraphic.getAnchorPoint(),oldGraphic.getDisplacement()); return sf.pointSymbolizer(graphic,ps.getGeometryPropertyName()); }else{ throw new IllegalArgumentException("unexpected symbolizer type : " + symbol); } } /** * Create a rule for given property name and object value. * * @param property rule filter property * @param obj rule filter property value * @param idx color index * @return rule created rule */ protected MutableRule createRule(final PropertyName property, final Object obj, int idx){ final MutableStyleFactory sf = GeotkFX.getStyleFactory(); final FilterFactory ff = GeotkFX.getFilterFactory(); final MutableRule r = sf.rule(createSymbolizer(idx)); r.setFilter(ff.equals(property, ff.literal(obj))); r.setDescription(sf.description(obj.toString(), obj.toString())); r.setName(obj.toString()); return r; } 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<CellDataFeatures<MutableRule, Symbolizer>, ObservableValue<Symbolizer>>() { @Override public ObservableValue<Symbolizer> call(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 CellEditEvent<MutableRule, Symbolizer> event = (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<CellDataFeatures<MutableRule, String>, ObservableValue<String>>() { @Override public ObservableValue<String> call(CellDataFeatures<MutableRule, String> param) { final Description desc = param.getValue().getDescription(); return new SimpleStringProperty(String.valueOf(desc.getTitle())); } }); setCellFactory(TextFieldTableCell.forTableColumn()); setOnEditCommit(new EventHandler<CellEditEvent<MutableRule, String>>() { @Override public void handle(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<CellDataFeatures<MutableRule, Filter>, ObservableValue<Filter>>() { @Override public ObservableValue<Filter> call(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((CellEditEvent<MutableRule, Filter> event) -> { event.getRowValue().setFilter(event.getNewValue()); }); } } }