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