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