/*
* 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.filter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TextArea;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.fxmisc.richtext.CodeArea;
import org.geotoolkit.cql.CQL;
import org.geotoolkit.cql.CQLException;
import org.geotoolkit.cql.CQLLexer;
import org.geotoolkit.cql.CQLParser;
import org.geotoolkit.data.FeatureCollection;
import org.geotoolkit.filter.function.FunctionFactory;
import org.geotoolkit.filter.function.Functions;
import org.geotoolkit.gui.javafx.util.FXOptionDialog;
import org.geotoolkit.internal.GeotkFX;
import org.geotoolkit.map.FeatureMapLayer;
import org.geotoolkit.map.MapLayer;
import org.opengis.feature.FeatureType;
import org.opengis.feature.PropertyType;
import org.opengis.filter.Filter;
import org.opengis.filter.expression.Expression;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
/**
* CQL editor
*
* @author Johann Sorel (Geomatys)
*/
public class FXCQLEditor extends BorderPane {
private static final Collection STYLE_DEFAULT = Collections.singleton("default");
private static final Collection STYLE_COMMENT = Collections.singleton("comment");
private static final Collection STYLE_LITERAL = Collections.singleton("literal");
private static final Collection STYLE_FUNCTION = Collections.singleton("function");
private static final Collection STYLE_PARENTHESE = Collections.singleton("parenthese");
private static final Collection STYLE_OPERATOR = Collections.singleton("operator");
private static final Collection STYLE_BINARY = Collections.singleton("binary");
private static final Collection STYLE_PROPERTY = Collections.singleton("property");
private static final Collection STYLE_ERROR = Collections.singleton("error");
@FXML private ListView<String> uiProperties;
@FXML private TreeView<Object> uiFunctions;
@FXML private TextArea uiDetail;
private final CodeArea codeArea = new CodeArea();
private final boolean filterMode;
/**
*
* @param forFilter set to true if this editor is for filter expressions
*/
public FXCQLEditor(boolean forFilter){
GeotkFX.loadJRXML(this,FXCQLEditor.class);
this.filterMode = forFilter;
setCenter(codeArea);
codeArea.setWrapText(true);
codeArea.getStylesheets().add(FXCQLEditor.class.getResource("cql.css").toExternalForm());
codeArea.textProperty().addListener((obs, oldText, newText) -> {
updateHightLight();
//codeArea.setStyleSpans(0, computeHighlight(newText));
});
uiProperties.setCellFactory((ListView<String> param) -> new ClickCell());
uiProperties.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
uiProperties.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
Platform.runLater(uiProperties.getSelectionModel()::clearSelection);
}
});
uiFunctions.setShowRoot(false);
uiFunctions.setCellFactory((TreeView<Object> param) -> new ClickFCell());
uiFunctions.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
uiFunctions.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<TreeItem<Object>>() {
@Override
public void changed(ObservableValue<? extends TreeItem<Object>> observable, TreeItem<Object> oldValue, TreeItem<Object> newValue) {
Platform.runLater(uiFunctions.getSelectionModel()::clearSelection);
}
});
final TreeItem<Object> root = new TreeItem<>("root");
//sort factory by name
final List<FunctionFactory> factories = new ArrayList<>(Functions.getFactories());
Collections.sort(factories, new Comparator<FunctionFactory>() {
@Override
public int compare(FunctionFactory o1, FunctionFactory o2) {
return o1.getIdentifier().compareTo(o2.getIdentifier());
}
});
for(FunctionFactory ff : factories){
final TreeItem fnode = new TreeItem(ff.getIdentifier());
String[] names = ff.getNames();
Arrays.sort(names);
for(String str : names){
final ParameterDescriptorGroup desc = ff.describeFunction(str);
final TreeItem enode = new TreeItem(desc);
fnode.getChildren().add(enode);
}
root.getChildren().add(fnode);
}
uiFunctions.setRoot(root);
}
@FXML
private void putShortcut(ActionEvent event) {
final Button button = (Button) event.getSource();
final String text = button.getText();
codeArea.appendText(" "+text);
}
public void setTarget(Object candidate){
FeatureType ft = null;
if(candidate instanceof FeatureType){
ft = (FeatureType) candidate;
}else if(candidate instanceof FeatureCollection) {
ft = ((FeatureCollection)candidate).getFeatureType();
}else if(candidate instanceof FeatureMapLayer){
ft = ((FeatureMapLayer)candidate).getCollection().getFeatureType();
}
final ObservableList properties = FXCollections.observableArrayList();
if(ft!=null){
for(PropertyType desc : ft.getProperties(true)){
properties.add(desc.getName().tip().toString());
}
}
uiProperties.setItems(properties);
}
public void setExpression(Expression candidate){
codeArea.replaceText(CQL.write(candidate));
}
public void setFilter(Filter candidate){
codeArea.replaceText(CQL.write(candidate));
}
public Expression getExpression() throws CQLException{
return CQL.parseExpression(codeArea.getText());
}
public Filter getFilter() throws CQLException{
return CQL.parseFilter(codeArea.getText());
}
private void updateHightLight(){
final String txt = codeArea.getText();
final ParseTree tree = CQL.compile(txt);
syntaxHighLight(tree);
}
private void syntaxHighLight(ParseTree tree){
if(tree instanceof ParserRuleContext){
final ParserRuleContext prc = (ParserRuleContext) tree;
if(prc.exception!=null){
//error nodes
final Token tokenStart = prc.getStart();
Token tokenEnd = prc.getStop();
if(tokenEnd==null) tokenEnd = tokenStart;
final int offset = tokenStart.getStartIndex();
final int end = tokenEnd.getStopIndex()+1;
if(end>offset){
codeArea.setStyle(offset, end,STYLE_ERROR);
}
return;
}
//special case for functions
if(prc instanceof CQLParser.ExpressionTermContext){
final CQLParser.ExpressionTermContext ctx = (CQLParser.ExpressionTermContext) prc;
if(ctx.NAME()!=null && ctx.LPAREN()!=null){
final int nbChild = tree.getChildCount();
for(int i=0;i<nbChild;i++){
final ParseTree pt = tree.getChild(i);
if(pt instanceof TerminalNode && ((TerminalNode)pt).getSymbol().getType() == CQLLexer.NAME){
final TerminalNode tn = (TerminalNode) pt;
// if index<0 = missing token
final Token token = tn.getSymbol();
final int offset = token.getStartIndex();
final int end = token.getStopIndex()+1;
if(end>offset){
codeArea.setStyle(offset, end,STYLE_FUNCTION);
}
}else{
syntaxHighLight(pt);
}
}
return;
}
}
}
if(tree instanceof TerminalNode){
final TerminalNode tn = (TerminalNode) tree;
// if index<0 = missing token
final Token token = tn.getSymbol();
final int offset = token.getStartIndex();
final int end = token.getStopIndex()+1;
switch(token.getType()){
case CQLLexer.COMMA :
case CQLLexer.UNARY :
case CQLLexer.MULT :
codeArea.setStyle(offset, end, STYLE_DEFAULT);
break;
// EXpressions -------------------------------------------------
case CQLLexer.TEXT :
case CQLLexer.INT :
case CQLLexer.FLOAT :
case CQLLexer.DATE :
case CQLLexer.DURATION_P :
case CQLLexer.DURATION_T :
case CQLLexer.POINT :
case CQLLexer.LINESTRING :
case CQLLexer.POLYGON :
case CQLLexer.MPOINT :
case CQLLexer.MLINESTRING :
case CQLLexer.MPOLYGON :
codeArea.setStyle(offset, end, STYLE_LITERAL);
break;
case CQLLexer.PROPERTY_NAME :
codeArea.setStyle(offset, end, STYLE_PROPERTY);
break;
case CQLLexer.NAME :
if(tree.getChildCount()==0){
//property name
codeArea.setStyle(offset, end, STYLE_PROPERTY);
}else{
//function name
codeArea.setStyle(offset, end, STYLE_FUNCTION);
}
break;
case CQLLexer.RPAREN :
case CQLLexer.LPAREN :
codeArea.setStyle(offset, end, STYLE_PARENTHESE);
break;
case CQLLexer.COMPARE :
case CQLLexer.LIKE :
case CQLLexer.IS :
case CQLLexer.BETWEEN :
case CQLLexer.IN :
codeArea.setStyle(offset, end, STYLE_OPERATOR);
break;
case CQLLexer.AND :
case CQLLexer.OR :
case CQLLexer.NOT :
codeArea.setStyle(offset, end, STYLE_BINARY);
break;
case CQLLexer.BBOX :
case CQLLexer.BEYOND :
case CQLLexer.CONTAINS :
case CQLLexer.CROSSES :
case CQLLexer.DISJOINT :
case CQLLexer.DWITHIN :
case CQLLexer.EQUALS :
case CQLLexer.INTERSECTS :
case CQLLexer.OVERLAPS :
case CQLLexer.TOUCHES :
case CQLLexer.WITHIN :
codeArea.setStyle(offset, end, STYLE_BINARY);
break;
default :
codeArea.setStyle(offset, end, STYLE_ERROR);
break;
}
}
final int nbChild = tree.getChildCount();
for(int i=0;i<nbChild;i++){
syntaxHighLight(tree.getChild(i));
}
}
public static Expression showDialog(Node parent, MapLayer layer, Expression candidate) throws CQLException {
final FXCQLEditor editor = new FXCQLEditor(false);
editor.setExpression(candidate);
editor.setTarget(layer);
if(FXOptionDialog.showOkCancel(parent, editor, "CQL Editor", true)){
return editor.getExpression();
}else{
return null;
}
}
public static Filter showFilterDialog(Node parent, MapLayer layer, Filter candidate) throws CQLException {
final FXCQLEditor editor = new FXCQLEditor(true);
editor.setFilter(candidate);
editor.setTarget(layer);
if(FXOptionDialog.showOkCancel(parent, editor, "CQL Editor", true)){
return editor.getFilter();
}else{
return null;
}
}
private class ClickCell extends ListCell<String>{
public ClickCell() {
setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
final String item = getItem();
if(item!=null){
if(codeArea.getText().endsWith(" ")){
codeArea.appendText(item);
}else{
codeArea.appendText(" "+item);
}
}
}
});
}
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(item);
}
}
private class ClickFCell extends TreeCell<Object>{
public ClickFCell() {
setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
final Object item = getItem();
if(item instanceof ParameterDescriptorGroup){
final ParameterDescriptorGroup desc = (ParameterDescriptorGroup) item;
final StringBuilder sb = new StringBuilder();
sb.append(desc.getName().getCode()).append('(');
final List<GeneralParameterDescriptor> gpds = desc.descriptors();
for(int i=0;i<gpds.size();i++){
if(i>0) sb.append(',');
sb.append(gpds.get(i).getName().getCode());
}
sb.append(')');
if(codeArea.getText().endsWith(" ")){
codeArea.appendText(sb.toString());
}else{
codeArea.appendText(" "+sb.toString());
}
//update text area
final StringBuilder sbDesc = new StringBuilder();
sbDesc.append(desc.getName().toString());
if(desc.getRemarks()!=null){
sbDesc.append(" : ");
sbDesc.append(desc.getRemarks());
}
sbDesc.append("\n\n");
for(GeneralParameterDescriptor argDesc : desc.descriptors()){
sbDesc.append(" - ");
sbDesc.append(argDesc.getName().toString());
if(argDesc.getRemarks()!=null){
sbDesc.append(" : ");
sbDesc.append(argDesc.getRemarks());
}
sbDesc.append("\n\n");
}
uiDetail.setText(sbDesc.toString());
}else{
uiDetail.setText("");
}
}
});
}
@Override
protected void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if(item instanceof String){
setText((String)item);
}else if(item instanceof ParameterDescriptorGroup){
final ParameterDescriptorGroup desc = (ParameterDescriptorGroup) item;
setText(desc.getName().getCode());
}else{
setText("");
}
}
}
}