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