source: josm/trunk/src/org/openstreetmap/josm/gui/autofilter/AutoFilterManager.java@ 12407

Last change on this file since 12407 was 12407, checked in by Don-vip, 7 years ago

see #14929 - add support for numeric ranges + unit test

File size: 13.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.autofilter;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Graphics2D;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.Collection;
10import java.util.Comparator;
11import java.util.Iterator;
12import java.util.List;
13import java.util.Map;
14import java.util.NavigableSet;
15import java.util.Objects;
16import java.util.Set;
17import java.util.TreeMap;
18import java.util.TreeSet;
19import java.util.function.Consumer;
20import java.util.regex.Matcher;
21import java.util.regex.Pattern;
22
23import org.openstreetmap.josm.Main;
24import org.openstreetmap.josm.actions.mapmode.MapMode;
25import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
26import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
27import org.openstreetmap.josm.data.osm.BBox;
28import org.openstreetmap.josm.data.osm.DataSet;
29import org.openstreetmap.josm.data.osm.Filter;
30import org.openstreetmap.josm.data.osm.FilterModel;
31import org.openstreetmap.josm.data.osm.OsmPrimitive;
32import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
33import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
34import org.openstreetmap.josm.data.osm.event.DataSetListener;
35import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
36import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
37import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
38import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
39import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
40import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
41import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
42import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
43import org.openstreetmap.josm.data.preferences.BooleanProperty;
44import org.openstreetmap.josm.data.preferences.StringProperty;
45import org.openstreetmap.josm.gui.MapFrame;
46import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener;
47import org.openstreetmap.josm.gui.NavigatableComponent;
48import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
49import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
50import org.openstreetmap.josm.gui.widgets.OSDLabel;
51
52/**
53 * The auto filter manager keeps track of registered auto filter rules and applies the active one on the fly,
54 * when the map contents, location or zoom changes.
55 * @since 12400
56 */
57public final class AutoFilterManager implements ZoomChangeListener, MapModeChangeListener, DataSetListener, PreferenceChangedListener {
58
59 /**
60 * Property to determines if the auto filter feature is enabled.
61 */
62 public static final BooleanProperty PROP_AUTO_FILTER_ENABLED = new BooleanProperty("auto.filter.enabled", Boolean.TRUE);
63
64 /**
65 * Property to determine the current auto filter rule.
66 */
67 public static final StringProperty PROP_AUTO_FILTER_RULE = new StringProperty("auto.filter.rule", "level");
68
69 /**
70 * The unique instance.
71 */
72 private static volatile AutoFilterManager instance;
73
74 /**
75 * The buttons currently displayed in map view.
76 */
77 private final Map<String, AutoFilterButton> buttons = new TreeMap<>();
78
79 /**
80 * The list of registered auto filter rules.
81 */
82 private final List<AutoFilterRule> rules = new ArrayList<>();
83
84 /**
85 * A helper for {@link #drawOSDText(Graphics2D)}.
86 */
87 private final OSDLabel lblOSD = new OSDLabel("");
88
89 /**
90 * The filter model.
91 */
92 private final FilterModel model = new FilterModel();
93
94 /**
95 * The currently enabled rule, if any.
96 */
97 private AutoFilterRule enabledRule;
98
99 /**
100 * The currently selected auto filter, if any.
101 */
102 private AutoFilter currentAutoFilter;
103
104 /**
105 * Returns the unique instance.
106 * @return the unique instance
107 */
108 public static AutoFilterManager getInstance() {
109 if (instance == null) {
110 instance = new AutoFilterManager();
111 }
112 return instance;
113 }
114
115 private AutoFilterManager() {
116 MapFrame.addMapModeChangeListener(this);
117 Main.pref.addPreferenceChangeListener(this);
118 NavigatableComponent.addZoomChangeListener(this);
119 DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT_CONSOLIDATED);
120 registerAutoFilterRules(AutoFilterRule.defaultRules());
121 }
122
123 private synchronized void updateButtons() {
124 if (enabledRule != null && Main.map != null
125 && enabledRule.getMinZoomLevel() <= Selector.GeneralSelector.scale2level(Main.map.mapView.getDist100Pixel())) {
126 NavigableSet<String> values = getNumericValues(enabledRule.getKey(), enabledRule.getValueComparator());
127 if (!values.equals(buttons.keySet())) {
128 removeAllButtons();
129 addNewButtons(values);
130 }
131 }
132 }
133
134 private void addNewButtons(NavigableSet<String> values) {
135 int i = 0;
136 int maxWidth = 16;
137 for (final String value : values.descendingSet()) {
138 Filter filter = new Filter();
139 filter.enable = true;
140 filter.inverted = true;
141 filter.text = enabledRule.getKey() + "=" + value;
142 String label = enabledRule.getValueFormatter().apply(value);
143 AutoFilterButton button = new AutoFilterButton(new AutoFilter(label, filter.text, filter));
144 buttons.put(value, button);
145 maxWidth = Math.max(maxWidth, button.getPreferredSize().width);
146 Main.map.mapView.add(button).setLocation(3, 60 + 22*i++);
147 }
148 for (AutoFilterButton b : buttons.values()) {
149 b.setSize(maxWidth, 20);
150 }
151 Main.map.mapView.validate();
152 }
153
154 private void removeAllButtons() {
155 for (Iterator<String> it = buttons.keySet().iterator(); it.hasNext();) {
156 Main.map.mapView.remove(buttons.get(it.next()));
157 it.remove();
158 }
159 }
160
161 private static NavigableSet<String> getNumericValues(String key, Comparator<String> comparator) {
162 NavigableSet<String> values = new TreeSet<>(comparator);
163 for (String s : getTagValues(key)) {
164 try {
165 Integer.parseInt(s);
166 values.add(s);
167 } catch (NumberFormatException e) {
168 Main.trace(e);
169 }
170 }
171 return values;
172 }
173
174 private static Set<String> getTagValues(String key) {
175 DataSet ds = Main.getLayerManager().getEditDataSet();
176 Set<String> values = new TreeSet<>();
177 if (ds != null) {
178 BBox bbox = Main.map.mapView.getState().getViewArea().getLatLonBoundsBox().toBBox();
179 Consumer<OsmPrimitive> consumer = getTagValuesConsumer(key, values);
180 ds.searchNodes(bbox).forEach(consumer);
181 ds.searchWays(bbox).forEach(consumer);
182 ds.searchRelations(bbox).forEach(consumer);
183 }
184 return values;
185 }
186
187 static Consumer<OsmPrimitive> getTagValuesConsumer(String key, Set<String> values) {
188 return o -> {
189 String value = o.get(key);
190 if (value != null) {
191 Pattern p = Pattern.compile("(-?[0-9]+)-(-?[0-9]+)");
192 for (String v : value.split(";")) {
193 Matcher m = p.matcher(v);
194 if (m.matches()) {
195 int a = Integer.parseInt(m.group(1));
196 int b = Integer.parseInt(m.group(2));
197 for (int i = Math.min(a, b); i <= Math.max(a, b); i++) {
198 values.add(Integer.toString(i));
199 }
200 } else {
201 values.add(v);
202 }
203 }
204 }
205 };
206 }
207
208 @Override
209 public void zoomChanged() {
210 updateButtons();
211 }
212
213 @Override
214 public void dataChanged(DataChangedEvent event) {
215 updateFiltersFull();
216 }
217
218 @Override
219 public void nodeMoved(NodeMovedEvent event) {
220 updateFiltersFull();
221 }
222
223 @Override
224 public void otherDatasetChange(AbstractDatasetChangedEvent event) {
225 updateFiltersFull();
226 }
227
228 @Override
229 public void primitivesAdded(PrimitivesAddedEvent event) {
230 updateFiltersEvent(event, false);
231 updateButtons();
232 }
233
234 @Override
235 public void primitivesRemoved(PrimitivesRemovedEvent event) {
236 updateFiltersFull();
237 updateButtons();
238 }
239
240 @Override
241 public void relationMembersChanged(RelationMembersChangedEvent event) {
242 updateFiltersEvent(event, true);
243 }
244
245 @Override
246 public void tagsChanged(TagsChangedEvent event) {
247 updateFiltersEvent(event, true);
248 updateButtons();
249 }
250
251 @Override
252 public void wayNodesChanged(WayNodesChangedEvent event) {
253 updateFiltersEvent(event, true);
254 }
255
256 @Override
257 public void mapModeChange(MapMode oldMapMode, MapMode newMapMode) {
258 updateFiltersFull();
259 }
260
261 private void updateFiltersFull() {
262 if (currentAutoFilter != null) {
263 model.executeFilters();
264 }
265 }
266
267 private void updateFiltersEvent(AbstractDatasetChangedEvent event, boolean affectedOnly) {
268 if (currentAutoFilter != null) {
269 Collection<? extends OsmPrimitive> prims = event.getPrimitives();
270 model.executeFilters(affectedOnly ? FilterModel.getAffectedPrimitives(prims) : prims);
271 }
272 }
273
274 /**
275 * Registers new auto filter rule(s).
276 * @param filterRules new auto filter rules. Must not be null
277 * @return {@code true} if the list changed as a result of the call
278 * @throws NullPointerException if {@code filterRules} is null
279 */
280 public synchronized boolean registerAutoFilterRules(AutoFilterRule... filterRules) {
281 return rules.addAll(Arrays.asList(filterRules));
282 }
283
284 /**
285 * Unregisters an auto filter rule.
286 * @param rule auto filter rule to remove. Must not be null
287 * @return {@code true} if the list contained the specified rule
288 * @throws NullPointerException if {@code rule} is null
289 */
290 public synchronized boolean unregisterAutoFilterRule(AutoFilterRule rule) {
291 return rules.remove(Objects.requireNonNull(rule, "rule"));
292 }
293
294 /**
295 * Returns the list of registered auto filter rules.
296 * @return the list of registered rules
297 */
298 public synchronized List<AutoFilterRule> getAutoFilterRules() {
299 return new ArrayList<>(rules);
300 }
301
302 /**
303 * Returns the auto filter rule defined for the given OSM key.
304 * @param key OSM key used to identify rule. Can't be null.
305 * @return the auto filter rule defined for the given OSM key, or null
306 * @throws NullPointerException if key is null
307 */
308 public synchronized AutoFilterRule getAutoFilterRule(String key) {
309 for (AutoFilterRule r : rules) {
310 if (key.equals(r.getKey())) {
311 return r;
312 }
313 }
314 return null;
315 }
316
317 /**
318 * Sets the currently enabled auto filter rule to the one defined for the given OSM key.
319 * @param key OSM key used to identify new rule to enable. Null to disable the auto filter feature.
320 */
321 public synchronized void enableAutoFilterRule(String key) {
322 enableAutoFilterRule(key == null ? null : getAutoFilterRule(key));
323 }
324
325 /**
326 * Sets the currently enabled auto filter rule.
327 * @param rule new rule to enable. Null to disable the auto filter feature.
328 */
329 public synchronized void enableAutoFilterRule(AutoFilterRule rule) {
330 enabledRule = rule;
331 }
332
333 /**
334 * Returns the currently selected auto filter, if any.
335 * @return the currently selected auto filter, or null
336 */
337 public synchronized AutoFilter getCurrentAutoFilter() {
338 return currentAutoFilter;
339 }
340
341 /**
342 * Sets the currently selected auto filter, if any.
343 * @param autoFilter the currently selected auto filter, or null
344 */
345 public synchronized void setCurrentAutoFilter(AutoFilter autoFilter) {
346 model.clearFilters();
347 currentAutoFilter = autoFilter;
348 if (autoFilter != null) {
349 model.addFilter(autoFilter.getFilter());
350 model.executeFilters();
351 if (model.isChanged()) {
352 Main.getLayerManager().getEditLayer().invalidate();
353 }
354 }
355 }
356
357 /**
358 * Draws a text on the map display that indicates that filters are active.
359 * @param g The graphics to draw that text on.
360 */
361 public void drawOSDText(Graphics2D g) {
362 model.drawOSDText(g, lblOSD,
363 tr("<h2>Filter active: {0}</h2>", currentAutoFilter.getFilter().text),
364 tr("</p><p>Click again on filter button to see all objects.</p></html>"));
365 }
366
367 private void resetCurrentAutoFilter() {
368 setCurrentAutoFilter(null);
369 removeAllButtons();
370 if (Main.map != null) {
371 Main.map.filterDialog.getFilterModel().executeFilters();
372 }
373 }
374
375 @Override
376 public void preferenceChanged(PreferenceChangeEvent e) {
377 if (e.getKey().equals(PROP_AUTO_FILTER_ENABLED.getKey())) {
378 if (PROP_AUTO_FILTER_ENABLED.get()) {
379 enableAutoFilterRule(PROP_AUTO_FILTER_RULE.get());
380 updateButtons();
381 } else {
382 enableAutoFilterRule((AutoFilterRule) null);
383 resetCurrentAutoFilter();
384 }
385 } else if (e.getKey().equals(PROP_AUTO_FILTER_RULE.getKey())) {
386 enableAutoFilterRule(PROP_AUTO_FILTER_RULE.get());
387 resetCurrentAutoFilter();
388 updateButtons();
389 }
390 }
391}
Note: See TracBrowser for help on using the repository browser.