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