+ settingsPane.add(bigPanel, BorderLayout.CENTER);
+ settingsPane.add(buttonPanel, BorderLayout.SOUTH);
+
+ initFiltersTab();
+ }
+
+ /**
+ * Populates initial layout of the feature attribute filters panel
+ */
+ protected void initFiltersTab()
+ {
+ filters = new ArrayList<>();
+
+ /*
+ * choose feature type
+ */
+ JPanel chooseTypePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ chooseTypePanel.setBackground(Color.white);
+ JvSwingUtils.createItalicTitledBorder(chooseTypePanel,
+ MessageManager.getString("label.feature_type"), true);
+ filteredFeatureChoice = new JComboBox<>();
+ filteredFeatureChoice.addItemListener(new ItemListener()
+ {
+ @Override
+ public void itemStateChanged(ItemEvent e)
+ {
+ refreshFiltersDisplay();
+ }
+ });
+ chooseTypePanel.add(new JLabel(MessageManager
+ .getString("label.feature_to_filter")));
+ chooseTypePanel.add(filteredFeatureChoice);
+ populateFilterableFeatures();
+
+ /*
+ * the panel with the filters for the selected feature type
+ */
+ JPanel filtersPanel = new JPanel();
+ filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
+ filtersPanel.setBackground(Color.white);
+ JvSwingUtils.createItalicTitledBorder(filtersPanel,
+ MessageManager.getString("label.filters"), true);
+
+ /*
+ * add AND or OR radio buttons
+ */
+ JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ andOrPanel.setBackground(Color.white);
+ andOrPanel.setBorder(BorderFactory.createLineBorder(debugBorderColour));
+ andFilters = new JRadioButton("And");
+ orFilters = new JRadioButton("Or");
+ ActionListener actionListener = new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ filtersChanged();
+ }
+ };
+ andFilters.addActionListener(actionListener);
+ orFilters.addActionListener(actionListener);
+ ButtonGroup andOr = new ButtonGroup();
+ andOr.add(andFilters);
+ andOr.add(orFilters);
+ andFilters.setSelected(true);
+ andOrPanel.add(new JLabel(MessageManager
+ .getString("label.join_conditions")));
+ andOrPanel.add(andFilters);
+ andOrPanel.add(orFilters);
+ filtersPanel.add(andOrPanel);
+
+ /*
+ * panel with filters - populated by refreshFiltersDisplay
+ */
+ chooseFiltersPanel = new JPanel();
+ LayoutManager box = new BoxLayout(chooseFiltersPanel,
+ BoxLayout.Y_AXIS);
+ chooseFiltersPanel.setLayout(box);
+ filtersPanel.add(chooseFiltersPanel);
+
+ /*
+ * a read-only text view of the current filters
+ */
+ JPanel showFiltersPanel = new JPanel(new BorderLayout(5, 5));
+ showFiltersPanel.setBackground(Color.white);
+ JvSwingUtils.createItalicTitledBorder(showFiltersPanel,
+ MessageManager.getString("label.match_condition"), true);
+ filtersAsText = new JTextArea();
+ filtersAsText.setLineWrap(true);
+ filtersAsText.setWrapStyleWord(true);
+ showFiltersPanel.add(filtersAsText);
+
+ filtersPane.setLayout(new BorderLayout());
+ filtersPane.add(chooseTypePanel, BorderLayout.NORTH);
+ filtersPane.add(filtersPanel, BorderLayout.CENTER);
+ filtersPane.add(showFiltersPanel, BorderLayout.SOUTH);
+
+ /*
+ * update display for initial feature type selection
+ */
+ refreshFiltersDisplay();
+ }
+
+ /**
+ * Adds entries to the 'choose feature to filter' drop-down choice. Only
+ * feature types which have known attributes (so can be filtered) are
+ * included, so recall this method to update the list (check for newly added
+ * attributes).
+ */
+ protected void populateFilterableFeatures()
+ {
+ /*
+ * suppress action handler while updating the list
+ */
+ ItemListener listener = filteredFeatureChoice.getItemListeners()[0];
+ filteredFeatureChoice.removeItemListener(listener);
+
+ filteredFeatureChoice.removeAllItems();
+ ReverseListIterator<String> types = new ReverseListIterator<>(
+ fr.getRenderOrder());
+
+ boolean found = false;
+ while (types.hasNext())
+ {
+ String type = types.next();
+ if (FeatureAttributes.getInstance().hasAttributes(type))
+ {
+ filteredFeatureChoice.addItem(type);
+ found = true;
+ }
+ }
+ if (!found)
+ {
+ filteredFeatureChoice.addItem(MessageManager
+ .getString("label.no_feature_attributes"));
+ filteredFeatureChoice.setEnabled(false);
+ }
+
+ filteredFeatureChoice.addItemListener(listener);
+ }
+
+ /**
+ * Refreshes the display to show any filters currently configured for the
+ * selected feature type (editable, with 'remove' option), plus one extra row
+ * for adding a condition. This should be called on change of selected feature
+ * type, or after a filter has been removed, added or amended.
+ */
+ protected void refreshFiltersDisplay()
+ {
+ /*
+ * clear the panel and list of filter conditions
+ */
+ chooseFiltersPanel.removeAll();
+ filters.clear();
+
+ /*
+ * look up attributes known for feature type
+ */
+ String selectedType = (String) filteredFeatureChoice.getSelectedItem();
+ List<String[]> attNames = FeatureAttributes.getInstance()
+ .getAttributes(selectedType);
+
+ /*
+ * if this feature type has filters set, load them first
+ */
+ KeyedMatcherSetI featureFilters = fr.getFeatureFilter(selectedType);
+ filtersAsText.setText("");
+ if (featureFilters != null)
+ {
+ filtersAsText.setText(featureFilters.toString());
+ if (!featureFilters.isAnded())
+ {
+ orFilters.setSelected(true);
+ }
+ featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
+ }
+
+ /*
+ * and an empty filter for the user to populate (add)
+ */
+ KeyedMatcherI noFilter = new KeyedMatcher(Condition.values()[0], "",
+ (String) null);
+ filters.add(noFilter);
+
+ /*
+ * render the conditions in rows, each in its own JPanel
+ */
+ int filterIndex = 0;
+ for (KeyedMatcherI filter : filters)
+ {
+ String[] attName = filter.getKey();
+ Condition condition = filter.getMatcher()
+ .getCondition();
+ String pattern = filter.getMatcher().getPattern();
+ JPanel row = addFilter(attName, attNames, condition, pattern, filterIndex);
+ row.setBorder(BorderFactory.createLineBorder(debugBorderColour));
+ chooseFiltersPanel.add(row);
+ filterIndex++;
+ }
+ // chooseFiltersPanel.add(Box.createVerticalGlue());
+
+ filtersPane.validate();
+ filtersPane.repaint();
+ }
+
+ /**
+ * A helper method that constructs a panel with one filter condition:
+ * <ul>
+ * <li>a drop-down list of attribute names to choose from</li>
+ * <li>a drop-down list of conditions to choose from</li>
+ * <li>a text field for input of a match pattern</li>
+ * <li>optionally, a 'remove' button</li>
+ * </ul>
+ * If attribute, condition or pattern are not null, they are set as defaults for
+ * the input fields. The 'remove' button is added unless the pattern is null or
+ * empty (incomplete filter condition).
+ *
+ * @param attName
+ * @param attNames
+ * @param cond
+ * @param pattern
+ * @param filterIndex
+ * @return
+ */
+ protected JPanel addFilter(String[] attName, List<String[]> attNames,
+ Condition cond, String pattern, int filterIndex)
+ {
+ JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ filterRow.setBackground(Color.white);
+
+ /*
+ * drop-down choice of attribute, with description as a tooltip
+ * if we can obtain it
+ */
+ String featureType = (String) filteredFeatureChoice.getSelectedItem();
+ final JComboBox<String> attCombo = populateAttributesDropdown(
+ featureType, attNames);
+ JComboBox<Condition> condCombo = new JComboBox<>();
+ JTextField patternField = new JTextField(8);
+
+ /*
+ * action handlers that validate and (if valid) apply changes
+ */
+ ActionListener actionListener = new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ if (attCombo.getSelectedItem() != null)
+ {
+ if (validateFilter(patternField, condCombo))
+ {
+ updateFilter(attCombo, condCombo, patternField, filterIndex);
+ filtersChanged();
+ }
+ }
+ }
+ };
+ ItemListener itemListener = new ItemListener()
+ {
+ @Override
+ public void itemStateChanged(ItemEvent e)
+ {
+ actionListener.actionPerformed(null);
+ }
+ };
+
+ if (attName == null) // the 'add a condition' row
+ {
+ attCombo.setSelectedItem(null);
+ }
+ else
+ {
+ attCombo.setSelectedItem(String.join(COLON, attName));
+ }
+ attCombo.addItemListener(itemListener);
+
+ filterRow.add(attCombo);
+
+ /*
+ * drop-down choice of test condition
+ */
+ for (Condition c : Condition.values())
+ {
+ condCombo.addItem(c);
+ }
+ if (cond != null)
+ {
+ condCombo.setSelectedItem(cond);
+ }
+ condCombo.addItemListener(itemListener);
+ filterRow.add(condCombo);
+
+ /*
+ * pattern to match against
+ */
+ patternField.setText(pattern);
+ patternField.addActionListener(actionListener);
+ patternField.addFocusListener(new FocusAdapter()
+ {
+ @Override
+ public void focusLost(FocusEvent e)
+ {
+ actionListener.actionPerformed(null);
+ }
+ });
+ filterRow.add(patternField);
+
+ /*
+ * add remove button if filter is populated (non-empty pattern)
+ */
+ if (pattern != null && pattern.trim().length() > 0)
+ {
+ // todo: gif for button drawing '-' or 'x'
+ JButton removeCondition = new BasicArrowButton(SwingConstants.WEST);
+ removeCondition.setToolTipText(MessageManager
+ .getString("label.delete_row"));
+ removeCondition.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ filters.remove(filterIndex);
+ filtersChanged();
+ }
+ });
+ filterRow.add(removeCondition);
+ }
+
+ return filterRow;
+ }
+
+ /**
+ * A helper method to build the drop-down choice of attributes for a feature.
+ * Where metadata is available with a description for an attribute, that is
+ * added as a tooltip.
+ *
+ * @param featureType
+ * @param attNames
+ */
+ protected JComboBox<String> populateAttributesDropdown(
+ String featureType, List<String[]> attNames)
+ {
+ List<String> displayNames = new ArrayList<>();
+ List<String> tooltips = new ArrayList<>();
+ FeatureAttributes fa = FeatureAttributes.getInstance();
+ for (String[] attName : attNames)
+ {
+ String desc = fa.getDescription(featureType, attName);
+ if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
+ {
+ desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
+ }
+ displayNames.add(String.join(COLON, attName));
+ tooltips.add(desc == null ? "" : desc);
+ }
+
+ JComboBox<String> attCombo = JvSwingUtils.buildComboWithTooltips(
+ displayNames, tooltips);
+ if (attNames.isEmpty())
+ {
+ attCombo.setToolTipText(MessageManager
+ .getString("label.no_attributes"));
+ }
+ return attCombo;
+ }
+
+ /**
+ * Action on any change to feature filtering, namely
+ * <ul>
+ * <li>change of selected attribute</li>
+ * <li>change of selected condition</li>
+ * <li>change of match pattern</li>
+ * <li>removal of a condition</li>
+ * </ul>
+ * The action should be to
+ * <ul>
+ * <li>parse and validate the filters</li>
+ * <li>if valid, update the filter text box</li>
+ * <li>and apply the filters to the viewport</li>
+ * </ul>
+ */
+ protected void filtersChanged()
+ {
+ /*
+ * update the filter conditions for the feature type
+ */
+ String featureType = (String) filteredFeatureChoice.getSelectedItem();
+ boolean anded = andFilters.isSelected();
+ KeyedMatcherSetI combined = new KeyedMatcherSet();
+
+ for (KeyedMatcherI filter : filters)
+ {
+ String pattern = filter.getMatcher().getPattern();
+ if (pattern.trim().length() > 0)
+ {
+ if (anded)
+ {
+ combined.and(filter);
+ }
+ else
+ {
+ combined.or(filter);
+ }
+ }
+ }
+
+ /*
+ * save the filter conditions in the FeatureRenderer
+ * (note this might now be an empty filter with no conditions)
+ */
+ fr.setFeatureFilter(featureType, combined);
+
+ filtersAsText.setText(combined.toString());
+
+ refreshFiltersDisplay();
+
+ af.alignPanel.paintAlignment(true, true);
+ }
+
+ /**
+ * Constructs a filter condition from the given input fields, and replaces the
+ * condition at filterIndex with the new one
+ *
+ * @param attCombo
+ * @param condCombo
+ * @param valueField
+ * @param filterIndex
+ */
+ protected void updateFilter(JComboBox<String> attCombo,
+ JComboBox<Condition> condCombo, JTextField valueField,
+ int filterIndex)
+ {
+ String attName = (String) attCombo.getSelectedItem();
+ Condition cond = (Condition) condCombo.getSelectedItem();
+ String pattern = valueField.getText();
+ KeyedMatcherI km = new KeyedMatcher(cond, pattern,
+ attName.split(COLON));
+
+ filters.set(filterIndex, km);