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