/* * Copyright (C) 2011 Brockmann Consult GmbH (info@brockmann-consult.de) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * any later version. * This program 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 General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see http://www.gnu.org/licenses/ */ package org.esa.snap.timeseries.ui.graph; import org.esa.snap.core.jexp.ParseException; import org.esa.snap.core.jexp.Parser; import org.esa.snap.core.jexp.Symbol; import org.esa.snap.core.jexp.Term; import org.esa.snap.core.jexp.Variable; import org.esa.snap.core.jexp.impl.DefaultNamespace; import org.esa.snap.core.jexp.impl.ParserImpl; import org.esa.snap.core.jexp.impl.SymbolFactory; import org.esa.snap.core.ui.ExpressionPane; import org.esa.snap.core.ui.ModalDialog; import org.esa.snap.rcp.SnapApp; import org.esa.snap.timeseries.core.timeseries.datamodel.AxisMapping; import org.esa.snap.util.PropertyMap; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesDataItem; import javax.swing.AbstractAction; import javax.swing.DefaultComboBoxModel; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ItemEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * @author Sabine Embacher * @author Thomas Storm */ class TimeSeriesValidator implements TimeSeriesGraphForm.ValidatorUI, TimeSeriesGraphModel.Validation { private static final String QUALIFIER_RASTER = "r."; private static final String QUALIFIER_INSITU = "i."; private final Map<Object, Map<String, String>> timeSeriesExpressionsMap = new HashMap<>(); private final Set<TimeSeriesGraphModel.ValidationListener> validationListeners = new HashSet<>(); private final Parser parser = new ParserImpl(); private Map<String, String> currentExpressionMap; private List<String> qualifiedSourceNames; private DefaultNamespace namespace; private JComboBox<String> sourceNamesDropDown; private JTextField expressionTextField; private boolean hasUI = false; @Override public JComponent createUI() { expressionTextField = new JTextField(""); expressionTextField.setEditable(false); expressionTextField.setEnabled(false); expressionTextField.setColumns(30); expressionTextField.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (expressionTextField.isEnabled()) { showExpressionEditor(); } } }); sourceNamesDropDown = new JComboBox<>(); sourceNamesDropDown.setPreferredSize(new Dimension(120, 20)); sourceNamesDropDown.addItemListener(e -> { if (ItemEvent.SELECTED == e.getStateChange()) { final String selectedSourceName = e.getItem().toString(); final String expression = getExpressionFor(selectedSourceName); expressionTextField.setText(expression == null ? "" : expression); } }); sourceNamesDropDown.setEnabled(false); final JButton editExpressionButton = new JButton("..."); editExpressionButton.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { showExpressionEditor(); } }); JPanel uiPanel = new JPanel(); uiPanel.add(new JLabel("Valid expression:")); uiPanel.add(sourceNamesDropDown); uiPanel.add(expressionTextField); uiPanel.add(editExpressionButton); hasUI = true; final JPanel stretchablePanel = new JPanel(new BorderLayout()); stretchablePanel.add(uiPanel, BorderLayout.CENTER); return stretchablePanel; } @Override public void adaptTo(Object timeSeriesKey, AxisMapping axisMapping) { if (timeSeriesExpressionsMap.containsKey(timeSeriesKey)) { currentExpressionMap = timeSeriesExpressionsMap.get(timeSeriesKey); } else { currentExpressionMap = new HashMap<>(); timeSeriesExpressionsMap.put(timeSeriesKey, currentExpressionMap); } qualifiedSourceNames = extractQualifiedSourceNames(axisMapping); namespace = new DefaultNamespace(); for (String qualifiedSourceName : qualifiedSourceNames) { namespace.registerSymbol(SymbolFactory.createVariable(qualifiedSourceName, 0.0)); } if (qualifiedSourceNames.size() > 0 && hasUI) { expressionTextField.setEnabled(true); sourceNamesDropDown.setEnabled(true); sourceNamesDropDown.setModel(new DefaultComboBoxModel<>(getSourceNames())); final String expression = getExpressionFor(getSelectedSourceName()); expressionTextField.setText(expression == null ? "" : expression); } } @Override public TimeSeries validate(TimeSeries timeSeries, String sourceName, TimeSeriesType type) throws ParseException { String qualifiedSourceName = createQualifiedSourcename(sourceName, type); final Symbol symbol = namespace.resolveSymbol(qualifiedSourceName); if (symbol == null) { throw new ParseException("No variable for identifier '" + qualifiedSourceName + "' registered."); } final String expression = getExpressionFor(qualifiedSourceName); if (expression == null || expression.trim().isEmpty()) { return timeSeries; } final Variable variable = (Variable) symbol; final Term term = parser.parse(expression, namespace); final int seriesCount = timeSeries.getItemCount(); final TimeSeries validatedSeries = new TimeSeries(timeSeries.getKey()); for (int i = 0; i < seriesCount; i++) { final TimeSeriesDataItem dataItem = timeSeries.getDataItem(i); final Number value = dataItem.getValue(); variable.assignD(null, value.doubleValue()); if (term.evalB(null)) { validatedSeries.add(dataItem); } } return validatedSeries; } @Override public void addValidationListener(TimeSeriesGraphModel.ValidationListener listener) { validationListeners.add(listener); } boolean setExpression(String qualifiedSourceName, String expression) { final Symbol symbol = namespace.resolveSymbol(qualifiedSourceName); if (symbol == null) { return false; } if (isExpressionValid(expression, qualifiedSourceName)) { currentExpressionMap.put(qualifiedSourceName, expression); fireExpressionChanged(); return true; } return false; } private void showExpressionEditor() { final MyExpressionPane expressionPane = new MyExpressionPane(); expressionPane.setEmptyExpressionAllowed(true); expressionPane.setCode(expressionTextField.getText()); final String sourceName = getSelectedSourceName(); final int status = expressionPane.showModalDialog(SnapApp.getDefault().getMainFrame(), "Valid Expression for Source '" + sourceName + "'"); if (ModalDialog.ID_OK == status) { final String expression = expressionPane.getCode(); expressionTextField.setText(expression); setExpression(sourceName, expression); } } private boolean isExpressionValid(String expression, String qualifiedSourceName) { if (expression == null || expression.trim().isEmpty()) { return true; } if (expression.trim().equals(qualifiedSourceName.trim())) { return false; } try { final DefaultNamespace expressionValidationNamespace = new DefaultNamespace(); expressionValidationNamespace.registerSymbol(SymbolFactory.createVariable(qualifiedSourceName, 0.0)); final Term term = parser.parse(expression, expressionValidationNamespace); return term != null && term.isB(); } catch (ParseException ignored) { return false; } } private void fireExpressionChanged() { validationListeners.forEach(TimeSeriesGraphModel.ValidationListener::expressionChanged); } private String getSelectedSourceName() { return sourceNamesDropDown.getSelectedItem().toString(); } private String[] getSourceNames() { return qualifiedSourceNames.toArray(new String[qualifiedSourceNames.size()]); } private void collectSourceNames(ArrayList<String> names, List<String> sourceNames, String qualifier) { for (String sourceName : sourceNames) { final String qualifiedSourceName = qualifier + sourceName; names.add(qualifiedSourceName); } } private List<String> extractQualifiedSourceNames(AxisMapping axisMapping) { final ArrayList<String> names = new ArrayList<>(); for (String alias : axisMapping.getAliasNames()) { collectSourceNames(names, axisMapping.getInsituNames(alias), QUALIFIER_INSITU); collectSourceNames(names, axisMapping.getRasterNames(alias), QUALIFIER_RASTER); } return names; } private String createQualifiedSourcename(String sourceName, TimeSeriesType type) { String qualifiedSourceName; if (TimeSeriesType.INSITU.equals(type)) { qualifiedSourceName = QUALIFIER_INSITU + sourceName; } else { qualifiedSourceName = QUALIFIER_RASTER + sourceName; } return qualifiedSourceName; } private String getExpressionFor(String qualifiedSourceName) { return currentExpressionMap.get(qualifiedSourceName); } private class MyExpressionPane extends ExpressionPane { public MyExpressionPane() { super(true, null, new PropertyMap()); initParser(); initLeftAccessory(); } private void initParser() { final String sourceName = getSelectedSourceName(); final Variable sourceVariable = SymbolFactory.createVariable(sourceName, 0.0); final DefaultNamespace namespace = new DefaultNamespace(); namespace.registerSymbol(sourceVariable); setParser(new ParserImpl(namespace, true)); } private void initLeftAccessory() { final String sourceName = getSelectedSourceName(); final JButton insertButton = createInsertButton(sourceName); final JPanel sourcePane = new JPanel(new BorderLayout()); sourcePane.add(new JLabel("Data Source:"), BorderLayout.NORTH); sourcePane.add(insertButton); final JPanel patternInsertionPane = createPatternInsertionPane(); final JPanel leftAccessory = new JPanel(new BorderLayout(4, 4)); leftAccessory.add(sourcePane, BorderLayout.NORTH); leftAccessory.add(patternInsertionPane); setLeftAccessory(leftAccessory); } } }