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}