001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.autofilter;
003
004import java.text.DecimalFormat;
005import java.util.Arrays;
006import java.util.Locale;
007import java.util.Objects;
008import java.util.Optional;
009import java.util.function.Function;
010import java.util.function.IntFunction;
011import java.util.function.ToIntFunction;
012import java.util.regex.Matcher;
013import java.util.regex.Pattern;
014import java.util.stream.IntStream;
015
016import org.openstreetmap.josm.data.osm.OsmPrimitive;
017import org.openstreetmap.josm.data.osm.OsmUtils;
018import org.openstreetmap.josm.data.preferences.BooleanProperty;
019import org.openstreetmap.josm.tools.Logging;
020
021/**
022 * An auto filter rule determines how auto filter can be built from visible map data.
023 * Several rules can be registered, but only one rule is active at the same time.
024 * Rules are identified by the OSM key on which they apply.
025 * The dynamic values discovering operates only below a certain zoom level, for performance reasons.
026 * @since 12400
027 */
028public class AutoFilterRule {
029
030    /**
031     * Property to determine if the auto filter should assume sensible defaults for values (such as layer=1 for bridge=yes).
032     */
033    private static final BooleanProperty PROP_AUTO_FILTER_DEFAULTS = new BooleanProperty("auto.filter.defaults", true);
034
035    private final String key;
036
037    private final int minZoomLevel;
038
039    private Function<OsmPrimitive, IntStream> defaultValueSupplier = p -> IntStream.empty();
040
041    private ToIntFunction<String> valueExtractor = Integer::parseInt;
042
043    private IntFunction<String> valueFormatter = Integer::toString;
044
045    /**
046     * Constructs a new {@code AutoFilterRule}.
047     * @param key the OSM key on which the rule applies
048     * @param minZoomLevel the minimum zoom level at which the rule applies
049     */
050    public AutoFilterRule(String key, int minZoomLevel) {
051        this.key = key;
052        this.minZoomLevel = minZoomLevel;
053    }
054
055    /**
056     * Returns the OSM key on which the rule applies.
057     * @return the OSM key on which the rule applies
058     */
059    public String getKey() {
060        return key;
061    }
062
063    /**
064     * Returns the minimum zoom level at which the rule applies.
065     * @return the minimum zoom level at which the rule applies
066     */
067    public int getMinZoomLevel() {
068        return minZoomLevel;
069    }
070
071    /**
072     * Formats the numeric value
073     * @param value the numeric value to format
074     * @return the formatted value
075     */
076    public String formatValue(int value) {
077        return valueFormatter.apply(value);
078    }
079
080    /**
081     * Sets a OSM value formatter that defines the associated button label.
082     * @param valueFormatter OSM value formatter. Cannot be null
083     * @return {@code this}
084     * @throws NullPointerException if {@code valueFormatter} is null
085     */
086    public AutoFilterRule setValueFormatter(IntFunction<String> valueFormatter) {
087        this.valueFormatter = Objects.requireNonNull(valueFormatter);
088        return this;
089    }
090
091    /**
092     * Sets the function which yields default values for the given OSM primitive.
093     * This function is invoked if the primitive does not have this {@linkplain #getKey() key}.
094     * @param defaultValueSupplier the function which yields default values for the given OSM primitive
095     * @return {@code this}
096     * @throws NullPointerException if {@code defaultValueSupplier} is null
097     */
098    public AutoFilterRule setDefaultValueSupplier(Function<OsmPrimitive, IntStream> defaultValueSupplier) {
099        this.defaultValueSupplier = Objects.requireNonNull(defaultValueSupplier);
100        return this;
101    }
102
103    /**
104     * Sets the function which extracts a numeric value from an OSM value
105     * @param valueExtractor the function which extracts a numeric value from an OSM value
106     * @return {@code this}
107     * @throws NullPointerException if {@code valueExtractor} is null
108     */
109    public AutoFilterRule setValueExtractor(ToIntFunction<String> valueExtractor) {
110        this.valueExtractor = Objects.requireNonNull(valueExtractor);
111        return this;
112    }
113
114    /**
115     * Returns the numeric values for the given OSM primitive
116     * @param osm the primitive
117     * @return a stream of numeric values
118     */
119    public IntStream getTagValuesForPrimitive(OsmPrimitive osm) {
120        String value = osm.get(key);
121        if (value != null) {
122            Pattern p = Pattern.compile("(-?[0-9]+)-(-?[0-9]+)");
123            return OsmUtils.splitMultipleValues(value).flatMapToInt(v -> {
124                Matcher m = p.matcher(v);
125                if (m.matches()) {
126                    int a = valueExtractor.applyAsInt(m.group(1));
127                    int b = valueExtractor.applyAsInt(m.group(2));
128                    return IntStream.rangeClosed(Math.min(a, b), Math.max(a, b));
129                } else {
130                    try {
131                        return IntStream.of(valueExtractor.applyAsInt(v));
132                    } catch (NumberFormatException e) {
133                        Logging.trace(e);
134                        return IntStream.empty();
135                    }
136                }
137            });
138        }
139        return Boolean.TRUE.equals(PROP_AUTO_FILTER_DEFAULTS.get()) ? defaultValueSupplier.apply(osm) : IntStream.empty();
140    }
141
142    /**
143     * Returns the default list of auto filter rules. Plugins can extend the list by registering additional rules.
144     * @return the default list of auto filter rules
145     */
146    public static AutoFilterRule[] defaultRules() {
147        return new AutoFilterRule[]{
148            new AutoFilterRule("admin_level", 11),
149            new AutoFilterRule("building:levels", 17),
150            new AutoFilterRule("frequency", 5),
151            new AutoFilterRule("gauge", 5),
152            new AutoFilterRule("incline", 13)
153                    .setValueExtractor(s -> Integer.parseInt(s.replaceAll("%$", "")))
154                    .setValueFormatter(v -> v + "\u2009%"),
155            new AutoFilterRule("lanes", 13),
156            new AutoFilterRule("layer", 16)
157                    .setDefaultValueSupplier(AutoFilterRule::defaultLayer),
158            new AutoFilterRule("level", 17)
159                // #17109, support values like 0.5 or 1.5 - level values are multiplied by 2 when parsing, values are divided by 2 for formatting
160                .setValueExtractor(s -> (int) (Double.parseDouble(s) * 2.))
161                .setValueFormatter(v -> DecimalFormat.getInstance(Locale.ROOT).format(v / 2.)),
162            new AutoFilterRule("maxspeed", 16)
163                    .setValueExtractor(s -> Integer.parseInt(s.replace(" mph", ""))),
164            new AutoFilterRule("voltage", 5)
165                    .setValueFormatter(s -> s % 1000 == 0 ? (s / 1000) + "kV" : s + "V")
166        };
167    }
168
169    /**
170     * Returns the default auto filter rule for the given key
171     * @param key the OSM key
172     * @return default auto filter rule for the given key
173     */
174    static Optional<AutoFilterRule> getDefaultRule(String key) {
175        return Arrays.stream(AutoFilterRule.defaultRules())
176                .filter(r -> key.equals(r.getKey()))
177                .findFirst();
178    }
179
180    private static IntStream defaultLayer(OsmPrimitive osm) {
181        // assume sensible defaults, see #17496
182        if (osm.hasTag("bridge") || osm.hasTag("power", "line") || osm.hasTag("location", "overhead")) {
183            return IntStream.of(1);
184        } else if (osm.isKeyTrue("tunnel") || osm.hasTag("tunnel", "culvert") || osm.hasTag("location", "underground")) {
185            return IntStream.of(-1);
186        } else if (osm.hasTag("tunnel", "building_passage") || osm.hasKey("highway", "railway", "waterway")) {
187            return IntStream.of(0);
188        } else {
189            return IntStream.empty();
190        }
191    }
192
193    @Override
194    public boolean equals(Object o) {
195        if (this == o) return true;
196        if (o == null || getClass() != o.getClass()) return false;
197        AutoFilterRule that = (AutoFilterRule) o;
198        return Objects.equals(key, that.key);
199    }
200
201    @Override
202    public int hashCode() {
203        return Objects.hash(key);
204    }
205
206    @Override
207    public String toString() {
208        return key + " [" + minZoomLevel + ']';
209    }
210}