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