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<>(); 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 final class TagSwitcher { 095 096 private TagSwitcher() { 097 // Hide implicit public constructor for utility class 098 } 099 100 /** 101 * Reverses a given tag. 102 * @param tag The tag to reverse 103 * @return The reversed tag (is equal to <code>tag</code> if no change is needed) 104 */ 105 public static final Tag apply(final Tag tag) { 106 return apply(tag.getKey(), tag.getValue()); 107 } 108 109 /** 110 * Reverses a given tag (key=value). 111 * @param key The tag key 112 * @param value The tag value 113 * @return The reversed tag (is equal to <code>key=value</code> if no change is needed) 114 */ 115 public static final Tag apply(final String key, final String value) { 116 String newKey = key; 117 String newValue = value; 118 119 if (key.startsWith("oneway") || key.endsWith("oneway")) { 120 if (OsmUtils.isReversed(value)) { 121 newValue = OsmUtils.trueval; 122 } else if (OsmUtils.isTrue(value)) { 123 newValue = OsmUtils.reverseval; 124 } 125 } else if (key.startsWith("incline") || key.endsWith("incline") 126 || key.startsWith("direction") || key.endsWith("direction")) { 127 newValue = UP_DOWN.apply(value); 128 if (newValue.equals(value)) { 129 newValue = invertNumber(value); 130 } 131 } else if (key.endsWith(":forward") || key.endsWith(":backward")) { 132 // Change key but not left/right value (fix #8518) 133 newKey = FORWARD_BACKWARD.apply(key); 134 135 } else if (!ignoreKeyForCorrection(key)) { 136 for (StringSwitcher prefixSuffixSwitcher : stringSwitchers) { 137 newKey = prefixSuffixSwitcher.apply(key); 138 if (!key.equals(newKey)) { 139 break; 140 } 141 newValue = prefixSuffixSwitcher.apply(value); 142 if (!value.equals(newValue)) { 143 break; 144 } 145 } 146 } 147 return new Tag(newKey, newValue); 148 } 149 } 150 151 private static final StringSwitcher FORWARD_BACKWARD = new StringSwitcher("forward", "backward"); 152 private static final StringSwitcher UP_DOWN = new StringSwitcher("up", "down"); 153 154 private static final StringSwitcher[] stringSwitchers = new StringSwitcher[] { 155 new StringSwitcher("left", "right"), 156 new StringSwitcher("forwards", "backwards"), 157 new StringSwitcher("east", "west"), 158 new StringSwitcher("north", "south"), 159 FORWARD_BACKWARD, UP_DOWN 160 }; 161 162 /** 163 * Tests whether way can be reversed without semantic change, i.e., whether tags have to be changed. 164 * Looks for keys like oneway, oneway:bicycle, cycleway:right:oneway, left/right. 165 * @param way 166 * @return false if tags should be changed to keep semantic, true otherwise. 167 */ 168 public static boolean isReversible(Way way) { 169 for (Tag tag : TagCollection.from(way)) { 170 if (!tag.equals(TagSwitcher.apply(tag))) { 171 return false; 172 } 173 } 174 return true; 175 } 176 177 public static List<Way> irreversibleWays(List<Way> ways) { 178 List<Way> newWays = new ArrayList<>(ways); 179 for (Way way : ways) { 180 if (isReversible(way)) { 181 newWays.remove(way); 182 } 183 } 184 return newWays; 185 } 186 187 public static String invertNumber(String value) { 188 Pattern pattern = Pattern.compile("^([+-]?)(\\d.*)$", Pattern.CASE_INSENSITIVE); 189 Matcher matcher = pattern.matcher(value); 190 if (!matcher.matches()) return value; 191 String sign = matcher.group(1); 192 String rest = matcher.group(2); 193 sign = "-".equals(sign) ? "" : "-"; 194 return sign + rest; 195 } 196 197 @Override 198 public Collection<Command> execute(Way oldway, Way way) throws UserCancelException { 199 Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = new HashMap<>(); 200 201 List<TagCorrection> tagCorrections = new ArrayList<>(); 202 for (String key : way.keySet()) { 203 String value = way.get(key); 204 Tag newTag = TagSwitcher.apply(key, value); 205 String newKey = newTag.getKey(); 206 String newValue = newTag.getValue(); 207 208 boolean needsCorrection = !key.equals(newKey); 209 if (way.get(newKey) != null && way.get(newKey).equals(newValue)) { 210 needsCorrection = false; 211 } 212 if (!value.equals(newValue)) { 213 needsCorrection = true; 214 } 215 216 if (needsCorrection) { 217 tagCorrections.add(new TagCorrection(key, value, newKey, newValue)); 218 } 219 } 220 if (!tagCorrections.isEmpty()) { 221 tagCorrectionsMap.put(way, tagCorrections); 222 } 223 224 Map<OsmPrimitive, List<RoleCorrection>> roleCorrectionMap = new HashMap<>(); 225 List<RoleCorrection> roleCorrections = new ArrayList<>(); 226 227 Collection<OsmPrimitive> referrers = oldway.getReferrers(); 228 for (OsmPrimitive referrer: referrers) { 229 if (! (referrer instanceof Relation)) { 230 continue; 231 } 232 Relation relation = (Relation)referrer; 233 int position = 0; 234 for (RelationMember member : relation.getMembers()) { 235 if (!member.getMember().hasEqualSemanticAttributes(oldway) 236 || !member.hasRole()) { 237 position++; 238 continue; 239 } 240 241 boolean found = false; 242 String newRole = null; 243 for (StringSwitcher prefixSuffixSwitcher : stringSwitchers) { 244 newRole = prefixSuffixSwitcher.apply(member.getRole()); 245 if (!newRole.equals(member.getRole())) { 246 found = true; 247 break; 248 } 249 } 250 251 if (found) { 252 roleCorrections.add(new RoleCorrection(relation, position, member, newRole)); 253 } 254 255 position++; 256 } 257 } 258 if (!roleCorrections.isEmpty()) { 259 roleCorrectionMap.put(way, roleCorrections); 260 } 261 262 return applyCorrections(tagCorrectionsMap, roleCorrectionMap, 263 tr("When reversing this way, the following changes are suggested in order to maintain data consistency.")); 264 } 265 266 private static boolean ignoreKeyForCorrection(String key) { 267 for (Pattern ignoredKey : ignoredKeys) { 268 if (ignoredKey.matcher(key).matches()) { 269 return true; 270 } 271 } 272 return false; 273 } 274}