001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.corrector; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.util.ArrayList; 007import java.util.Arrays; 008import java.util.Collection; 009import java.util.HashMap; 010import java.util.List; 011import java.util.Locale; 012import java.util.Map; 013import java.util.function.Function; 014import java.util.regex.Matcher; 015import java.util.regex.Pattern; 016import java.util.stream.Collectors; 017 018import org.openstreetmap.josm.command.Command; 019import org.openstreetmap.josm.data.correction.RoleCorrection; 020import org.openstreetmap.josm.data.correction.TagCorrection; 021import org.openstreetmap.josm.data.osm.AbstractPrimitive; 022import org.openstreetmap.josm.data.osm.Node; 023import org.openstreetmap.josm.data.osm.OsmPrimitive; 024import org.openstreetmap.josm.data.osm.OsmUtils; 025import org.openstreetmap.josm.data.osm.Relation; 026import org.openstreetmap.josm.data.osm.RelationMember; 027import org.openstreetmap.josm.data.osm.Tag; 028import org.openstreetmap.josm.data.osm.Tagged; 029import org.openstreetmap.josm.data.osm.Way; 030import org.openstreetmap.josm.tools.Logging; 031import org.openstreetmap.josm.tools.UserCancelException; 032 033/** 034 * A ReverseWayTagCorrector handles necessary corrections of tags 035 * when a way is reversed. E.g. oneway=yes needs to be changed 036 * to oneway=-1 and vice versa. 037 * 038 * The Corrector offers the automatic resolution in an dialog 039 * for the user to confirm. 040 */ 041public class ReverseWayTagCorrector extends TagCorrector<Way> { 042 043 private static final String SEPARATOR = "[:_]"; 044 045 private static Pattern getPatternFor(String s) { 046 return getPatternFor(s, false); 047 } 048 049 private static Pattern getPatternFor(String s, boolean exactMatch) { 050 if (exactMatch) { 051 return Pattern.compile("(^)(" + s + ")($)"); 052 } else { 053 return Pattern.compile("(^|.*" + SEPARATOR + ")(" + s + ")(" + SEPARATOR + ".*|$)", 054 Pattern.CASE_INSENSITIVE); 055 } 056 } 057 058 private static final Collection<Pattern> IGNORED_KEYS = new ArrayList<>(); 059 static { 060 for (String s : AbstractPrimitive.getUninterestingKeys()) { 061 IGNORED_KEYS.add(getPatternFor(s)); 062 } 063 for (String s : new String[]{"name", "ref", "tiger:county"}) { 064 IGNORED_KEYS.add(getPatternFor(s, false)); 065 } 066 for (String s : new String[]{"tiger:county", "turn:lanes", "change:lanes", "placement"}) { 067 IGNORED_KEYS.add(getPatternFor(s, true)); 068 } 069 } 070 071 private interface IStringSwitcher extends Function<String, String> { 072 073 static IStringSwitcher combined(IStringSwitcher... switchers) { 074 return key -> Arrays.stream(switchers) 075 .map(switcher -> switcher.apply(key)) 076 .filter(newKey -> !key.equals(newKey)) 077 .findFirst().orElse(key); 078 } 079 } 080 081 private static class StringSwitcher implements IStringSwitcher { 082 083 private final String a; 084 private final String b; 085 private final Pattern pattern; 086 087 StringSwitcher(String a, String b) { 088 this.a = a; 089 this.b = b; 090 this.pattern = getPatternFor(a + '|' + b); 091 } 092 093 @Override 094 public String apply(String text) { 095 Matcher m = pattern.matcher(text); 096 097 if (m.lookingAt()) { 098 String leftRight = m.group(2).toLowerCase(Locale.ENGLISH); 099 100 StringBuilder result = new StringBuilder(); 101 result.append(text.substring(0, m.start(2))) 102 .append(leftRight.equals(a) ? b : a) 103 .append(text.substring(m.end(2))); 104 105 return result.toString(); 106 } 107 return text; 108 } 109 } 110 111 /** 112 * Reverses a given tag. 113 * @since 5787 114 */ 115 public static final class TagSwitcher { 116 117 private TagSwitcher() { 118 // Hide implicit public constructor for utility class 119 } 120 121 /** 122 * Reverses a given tag. 123 * @param tag The tag to reverse 124 * @return The reversed tag (is equal to <code>tag</code> if no change is needed) 125 */ 126 public static Tag apply(final Tag tag) { 127 return apply(tag.getKey(), tag.getValue()); 128 } 129 130 /** 131 * Reverses a given tag (key=value). 132 * @param key The tag key 133 * @param value The tag value 134 * @return The reversed tag (is equal to <code>key=value</code> if no change is needed) 135 */ 136 public static Tag apply(final String key, final String value) { 137 String newKey = key; 138 String newValue = value; 139 140 if (key.startsWith("oneway") || key.endsWith("oneway")) { 141 if (OsmUtils.isReversed(value)) { 142 newValue = OsmUtils.TRUE_VALUE; 143 } else if (OsmUtils.isTrue(value)) { 144 newValue = OsmUtils.REVERSE_VALUE; 145 } 146 newKey = COMBINED_SWITCHERS.apply(key); 147 } else if (key.startsWith("incline") || key.endsWith("incline")) { 148 newValue = UP_DOWN.apply(value); 149 if (newValue.equals(value)) { 150 newValue = invertNumber(value); 151 } 152 } else if (key.startsWith("direction") || key.endsWith("direction")) { 153 newValue = COMBINED_SWITCHERS.apply(value); 154 } else if (key.endsWith(":forward") || key.endsWith(":backward")) { 155 // Change key but not left/right value (fix #8518) 156 newKey = FORWARD_BACKWARD.apply(key); 157 } else if (!ignoreKeyForCorrection(key)) { 158 newKey = COMBINED_SWITCHERS.apply(key); 159 newValue = COMBINED_SWITCHERS.apply(value); 160 } 161 return new Tag(newKey, newValue); 162 } 163 } 164 165 private static final StringSwitcher FORWARD_BACKWARD = new StringSwitcher("forward", "backward"); 166 private static final StringSwitcher UP_DOWN = new StringSwitcher("up", "down"); 167 private static final IStringSwitcher COMBINED_SWITCHERS = IStringSwitcher.combined( 168 new StringSwitcher("left", "right"), 169 new StringSwitcher("forwards", "backwards"), 170 FORWARD_BACKWARD, UP_DOWN 171 ); 172 173 /** 174 * Tests whether way can be reversed without semantic change, i.e., whether tags have to be changed. 175 * Looks for keys like oneway, oneway:bicycle, cycleway:right:oneway, left/right. 176 * Also tests the nodes, e.g. a highway=stop with direction, see #20013. 177 * @param way way to test 178 * @return false if tags should be changed to keep semantic, true otherwise. 179 */ 180 public static boolean isReversible(Way way) { 181 return getTagCorrectionsMap(way).isEmpty(); 182 } 183 184 /** 185 * Returns the subset of irreversible ways. 186 * @param ways all ways 187 * @return the subset of irreversible ways 188 * @see #isReversible(Way) 189 */ 190 public static List<Way> irreversibleWays(List<Way> ways) { 191 return ways.stream().filter(w -> !isReversible(w)).collect(Collectors.toList()); 192 } 193 194 /** 195 * Inverts sign of a numeric value and converts decimal number to use decimal point. 196 * Also removes sign from null value. 197 * @param value numeric value 198 * @return opposite numeric value 199 */ 200 public static String invertNumber(String value) { 201 Pattern pattern = Pattern.compile("^([+-]?)(\\d*[,.]?\\d*)(.*)$", Pattern.CASE_INSENSITIVE); 202 Matcher matcher = pattern.matcher(value); 203 if (!matcher.matches()) return value; 204 String sign = matcher.group(1); 205 String number = matcher.group(2); 206 String symbol = matcher.group(3); 207 sign = "-".equals(sign) ? "" : "-"; 208 209 if (!number.isEmpty()) { 210 String fixedNum = number.replace(",", "."); 211 try { 212 double parsed = Double.parseDouble(fixedNum); 213 if (parsed != 0) { 214 return sign + fixedNum + symbol; 215 } else { 216 return fixedNum + symbol; 217 } 218 } catch (NumberFormatException e) { 219 Logging.trace(e); 220 return value; 221 } 222 } 223 224 return value; 225 } 226 227 static List<TagCorrection> getTagCorrections(Tagged way) { 228 List<TagCorrection> tagCorrections = new ArrayList<>(); 229 for (Map.Entry<String, String> entry : way.getKeys().entrySet()) { 230 final String key = entry.getKey(); 231 final String value = entry.getValue(); 232 Tag newTag = TagSwitcher.apply(key, value); 233 String newKey = newTag.getKey(); 234 String newValue = newTag.getValue(); 235 236 boolean needsCorrection = !key.equals(newKey); 237 if (way.get(newKey) != null && way.get(newKey).equals(newValue)) { 238 needsCorrection = false; 239 } 240 if (!value.equals(newValue)) { 241 needsCorrection = true; 242 } 243 244 if (needsCorrection) { 245 tagCorrections.add(new TagCorrection(key, value, newKey, newValue)); 246 } 247 } 248 return tagCorrections; 249 } 250 251 static List<RoleCorrection> getRoleCorrections(Way oldway) { 252 List<RoleCorrection> roleCorrections = new ArrayList<>(); 253 254 Collection<OsmPrimitive> referrers = oldway.getReferrers(); 255 for (OsmPrimitive referrer: referrers) { 256 if (!(referrer instanceof Relation)) { 257 continue; 258 } 259 Relation relation = (Relation) referrer; 260 int position = 0; 261 for (RelationMember member : relation.getMembers()) { 262 if (!member.getMember().hasEqualSemanticAttributes(oldway) 263 || !member.hasRole()) { 264 position++; 265 continue; 266 } 267 268 final String newRole = COMBINED_SWITCHERS.apply(member.getRole()); 269 if (!member.getRole().equals(newRole)) { 270 roleCorrections.add(new RoleCorrection(relation, position, member, newRole)); 271 } 272 273 position++; 274 } 275 } 276 return roleCorrections; 277 } 278 279 static Map<OsmPrimitive, List<TagCorrection>> getTagCorrectionsMap(Way way) { 280 Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = new HashMap<>(); 281 List<TagCorrection> tagCorrections = getTagCorrections(way); 282 if (!tagCorrections.isEmpty()) { 283 tagCorrectionsMap.put(way, tagCorrections); 284 } 285 for (Node node : way.getNodes()) { 286 final List<TagCorrection> corrections = getTagCorrections(node); 287 if (!corrections.isEmpty()) { 288 tagCorrectionsMap.put(node, corrections); 289 } 290 } 291 return tagCorrectionsMap; 292 } 293 294 @Override 295 public Collection<Command> execute(Way oldway, Way way) throws UserCancelException { 296 Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = getTagCorrectionsMap(way); 297 298 Map<OsmPrimitive, List<RoleCorrection>> roleCorrectionMap = new HashMap<>(); 299 List<RoleCorrection> roleCorrections = getRoleCorrections(oldway); 300 if (!roleCorrections.isEmpty()) { 301 roleCorrectionMap.put(way, roleCorrections); 302 } 303 304 return applyCorrections(oldway.getDataSet(), tagCorrectionsMap, roleCorrectionMap, 305 tr("When reversing this way, the following changes are suggested in order to maintain data consistency.")); 306 } 307 308 private static boolean ignoreKeyForCorrection(String key) { 309 return IGNORED_KEYS.stream() 310 .anyMatch(ignoredKey -> ignoredKey.matcher(key).matches()); 311 } 312}