// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.autofilter;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openstreetmap.josm.actions.mapmode.MapMode;
import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
import org.openstreetmap.josm.data.osm.BBox;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Filter;
import org.openstreetmap.josm.data.osm.FilterModel;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
import org.openstreetmap.josm.data.osm.event.DataSetListener;
import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
import org.openstreetmap.josm.data.preferences.BooleanProperty;
import org.openstreetmap.josm.data.preferences.StringProperty;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.MapFrame;
import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.NavigatableComponent;
import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
import org.openstreetmap.josm.gui.widgets.OSDLabel;
import org.openstreetmap.josm.spi.preferences.Config;
import org.openstreetmap.josm.tools.Logging;
/**
* The auto filter manager keeps track of registered auto filter rules and applies the active one on the fly,
* when the map contents, location or zoom changes.
* @since 12400
*/
public final class AutoFilterManager
implements ZoomChangeListener, MapModeChangeListener, DataSetListener, PreferenceChangedListener, LayerChangeListener {
/**
* Property to determines if the auto filter feature is enabled.
*/
public static final BooleanProperty PROP_AUTO_FILTER_ENABLED = new BooleanProperty("auto.filter.enabled", true);
/**
* Property to determine the current auto filter rule.
*/
public static final StringProperty PROP_AUTO_FILTER_RULE = new StringProperty("auto.filter.rule", "level");
/**
* The unique instance.
*/
private static volatile AutoFilterManager instance;
/**
* The buttons currently displayed in map view.
*/
private final Map buttons = new TreeMap<>();
/**
* The list of registered auto filter rules.
*/
private final List rules = new ArrayList<>();
/**
* A helper for {@link #drawOSDText(Graphics2D)}.
*/
private final OSDLabel lblOSD = new OSDLabel("");
/**
* The filter model.
*/
private final FilterModel model = new FilterModel();
/**
* The currently enabled rule, if any.
*/
private AutoFilterRule enabledRule;
/**
* The currently selected auto filter, if any.
*/
private AutoFilter currentAutoFilter;
/**
* Returns the unique instance.
* @return the unique instance
*/
public static AutoFilterManager getInstance() {
if (instance == null) {
instance = new AutoFilterManager();
}
return instance;
}
private AutoFilterManager() {
MapFrame.addMapModeChangeListener(this);
Config.getPref().addPreferenceChangeListener(this);
NavigatableComponent.addZoomChangeListener(this);
MainApplication.getLayerManager().addLayerChangeListener(this);
DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT_CONSOLIDATED);
registerAutoFilterRules(AutoFilterRule.defaultRules());
}
private synchronized void updateButtons() {
MapFrame map = MainApplication.getMap();
if (enabledRule != null && map != null
&& enabledRule.getMinZoomLevel() <= Selector.GeneralSelector.scale2level(map.mapView.getDist100Pixel())) {
// Retrieve the values from current rule visible on screen
NavigableSet values = getNumericValues(enabledRule.getKey(), enabledRule.getValueComparator());
// Make sure current auto filter button remains visible even if no data is found, to allow user to disable it
if (currentAutoFilter != null) {
values.add(currentAutoFilter.getFilter().text.split("=")[1]);
}
if (!values.equals(buttons.keySet())) {
removeAllButtons();
addNewButtons(values);
}
}
}
private synchronized void addNewButtons(NavigableSet values) {
int i = 0;
int maxWidth = 16;
MapView mapView = MainApplication.getMap().mapView;
for (final String value : values.descendingSet()) {
Filter filter = new Filter();
filter.enable = true;
filter.inverted = true;
filter.text = enabledRule.getKey() + "=" + value;
String label = enabledRule.getValueFormatter().apply(value);
AutoFilter autoFilter = new AutoFilter(label, filter.text, filter);
AutoFilterButton button = new AutoFilterButton(autoFilter);
if (autoFilter.equals(currentAutoFilter)) {
button.getModel().setPressed(true);
}
buttons.put(value, button);
maxWidth = Math.max(maxWidth, button.getPreferredSize().width);
mapView.add(button).setLocation(3, 60 + 22*i++);
}
for (AutoFilterButton b : buttons.values()) {
b.setSize(maxWidth, 20);
}
mapView.validate();
}
private void removeAllButtons() {
for (Iterator it = buttons.keySet().iterator(); it.hasNext();) {
MainApplication.getMap().mapView.remove(buttons.get(it.next()));
it.remove();
}
}
private static NavigableSet getNumericValues(String key, Comparator comparator) {
NavigableSet values = new TreeSet<>(comparator);
for (String s : getTagValues(key)) {
try {
Integer.parseInt(s);
values.add(s);
} catch (NumberFormatException e) {
Logging.trace(e);
}
}
return values;
}
private static Set getTagValues(String key) {
DataSet ds = MainApplication.getLayerManager().getEditDataSet();
Set values = new TreeSet<>();
if (ds != null) {
BBox bbox = MainApplication.getMap().mapView.getState().getViewArea().getLatLonBoundsBox().toBBox();
Consumer consumer = getTagValuesConsumer(key, values);
ds.searchNodes(bbox).forEach(consumer);
ds.searchWays(bbox).forEach(consumer);
ds.searchRelations(bbox).forEach(consumer);
}
return values;
}
static Consumer getTagValuesConsumer(String key, Set values) {
return o -> {
String value = o.get(key);
if (value != null) {
Pattern p = Pattern.compile("(-?[0-9]+)-(-?[0-9]+)");
for (String v : value.split(";")) {
Matcher m = p.matcher(v);
if (m.matches()) {
int a = Integer.parseInt(m.group(1));
int b = Integer.parseInt(m.group(2));
for (int i = Math.min(a, b); i <= Math.max(a, b); i++) {
values.add(Integer.toString(i));
}
} else {
values.add(v);
}
}
}
};
}
@Override
public void zoomChanged() {
updateButtons();
}
@Override
public void dataChanged(DataChangedEvent event) {
updateFiltersFull();
}
@Override
public void nodeMoved(NodeMovedEvent event) {
updateFiltersFull();
}
@Override
public void otherDatasetChange(AbstractDatasetChangedEvent event) {
updateFiltersFull();
}
@Override
public void primitivesAdded(PrimitivesAddedEvent event) {
updateFiltersEvent(event, false);
updateButtons();
}
@Override
public void primitivesRemoved(PrimitivesRemovedEvent event) {
updateFiltersFull();
updateButtons();
}
@Override
public void relationMembersChanged(RelationMembersChangedEvent event) {
updateFiltersEvent(event, true);
}
@Override
public void tagsChanged(TagsChangedEvent event) {
updateFiltersEvent(event, true);
updateButtons();
}
@Override
public void wayNodesChanged(WayNodesChangedEvent event) {
updateFiltersEvent(event, true);
}
@Override
public void mapModeChange(MapMode oldMapMode, MapMode newMapMode) {
updateFiltersFull();
}
private synchronized void updateFiltersFull() {
if (currentAutoFilter != null) {
model.executeFilters();
}
}
private synchronized void updateFiltersEvent(AbstractDatasetChangedEvent event, boolean affectedOnly) {
if (currentAutoFilter != null) {
Collection extends OsmPrimitive> prims = event.getPrimitives();
model.executeFilters(affectedOnly ? FilterModel.getAffectedPrimitives(prims) : prims);
}
}
/**
* Registers new auto filter rule(s).
* @param filterRules new auto filter rules. Must not be null
* @return {@code true} if the list changed as a result of the call
* @throws NullPointerException if {@code filterRules} is null
*/
public synchronized boolean registerAutoFilterRules(AutoFilterRule... filterRules) {
return rules.addAll(Arrays.asList(filterRules));
}
/**
* Unregisters an auto filter rule.
* @param rule auto filter rule to remove. Must not be null
* @return {@code true} if the list contained the specified rule
* @throws NullPointerException if {@code rule} is null
*/
public synchronized boolean unregisterAutoFilterRule(AutoFilterRule rule) {
return rules.remove(Objects.requireNonNull(rule, "rule"));
}
/**
* Returns the list of registered auto filter rules.
* @return the list of registered rules
*/
public synchronized List getAutoFilterRules() {
return new ArrayList<>(rules);
}
/**
* Returns the auto filter rule defined for the given OSM key.
* @param key OSM key used to identify rule. Can't be null.
* @return the auto filter rule defined for the given OSM key, or null
* @throws NullPointerException if key is null
*/
public synchronized AutoFilterRule getAutoFilterRule(String key) {
for (AutoFilterRule r : rules) {
if (key.equals(r.getKey())) {
return r;
}
}
return null;
}
/**
* Sets the currently enabled auto filter rule to the one defined for the given OSM key.
* @param key OSM key used to identify new rule to enable. Null to disable the auto filter feature.
*/
public synchronized void enableAutoFilterRule(String key) {
enableAutoFilterRule(key == null ? null : getAutoFilterRule(key));
}
/**
* Sets the currently enabled auto filter rule.
* @param rule new rule to enable. Null to disable the auto filter feature.
*/
public synchronized void enableAutoFilterRule(AutoFilterRule rule) {
enabledRule = rule;
}
/**
* Returns the currently selected auto filter, if any.
* @return the currently selected auto filter, or null
*/
public synchronized AutoFilter getCurrentAutoFilter() {
return currentAutoFilter;
}
/**
* Sets the currently selected auto filter, if any.
* @param autoFilter the currently selected auto filter, or null
*/
public synchronized void setCurrentAutoFilter(AutoFilter autoFilter) {
model.clearFilters();
currentAutoFilter = autoFilter;
if (autoFilter != null) {
model.addFilter(autoFilter.getFilter());
model.executeFilters();
if (model.isChanged()) {
OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
if (editLayer != null) {
editLayer.invalidate();
}
}
}
}
/**
* Draws a text on the map display that indicates that filters are active.
* @param g The graphics to draw that text on.
*/
public synchronized void drawOSDText(Graphics2D g) {
model.drawOSDText(g, lblOSD,
tr("Filter active: {0}
", currentAutoFilter.getFilter().text),
tr("
Click again on filter button to see all objects.