001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.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 FORWARD_BACKWARD, UP_DOWN 174 ); 175 176 /** 177 * Tests whether way can be reversed without semantic change, i.e., whether tags have to be changed. 178 * Looks for keys like oneway, oneway:bicycle, cycleway:right:oneway, left/right. 179 * @param way way to test 180 * @return false if tags should be changed to keep semantic, true otherwise. 181 */ 182 public static boolean isReversible(Way way) { 183 for (Tag tag : TagCollection.from(way)) { 184 if (!tag.equals(TagSwitcher.apply(tag))) { 185 return false; 186 } 187 } 188 return true; 189 } 190 191 /** 192 * Returns the subset of irreversible ways. 193 * @param ways all ways 194 * @return the subset of irreversible ways 195 * @see #isReversible(Way) 196 */ 197 public static List<Way> irreversibleWays(List<Way> ways) { 198 List<Way> newWays = new ArrayList<>(ways); 199 for (Way way : ways) { 200 if (isReversible(way)) { 201 newWays.remove(way); 202 } 203 } 204 return newWays; 205 } 206 207 /** 208 * Inverts sign of a numeric value. 209 * @param value numeric value 210 * @return opposite numeric value 211 */ 212 public static String invertNumber(String value) { 213 Pattern pattern = Pattern.compile("^([+-]?)(\\d.*)$", Pattern.CASE_INSENSITIVE); 214 Matcher matcher = pattern.matcher(value); 215 if (!matcher.matches()) return value; 216 String sign = matcher.group(1); 217 String rest = matcher.group(2); 218 sign = "-".equals(sign) ? "" : "-"; 219 return sign + rest; 220 } 221 222 static List<TagCorrection> getTagCorrections(Tagged way) { 223 List<TagCorrection> tagCorrections = new ArrayList<>(); 224 for (Map.Entry<String, String> entry : way.getKeys().entrySet()) { 225 final String key = entry.getKey(); 226 final String value = entry.getValue(); 227 Tag newTag = TagSwitcher.apply(key, value); 228 String newKey = newTag.getKey(); 229 String newValue = newTag.getValue(); 230 231 boolean needsCorrection = !key.equals(newKey); 232 if (way.get(newKey) != null && way.get(newKey).equals(newValue)) { 233 needsCorrection = false; 234 } 235 if (!value.equals(newValue)) { 236 needsCorrection = true; 237 } 238 239 if (needsCorrection) { 240 tagCorrections.add(new TagCorrection(key, value, newKey, newValue)); 241 } 242 } 243 return tagCorrections; 244 } 245 246 static List<RoleCorrection> getRoleCorrections(Way oldway) { 247 List<RoleCorrection> roleCorrections = new ArrayList<>(); 248 249 Collection<OsmPrimitive> referrers = oldway.getReferrers(); 250 for (OsmPrimitive referrer: referrers) { 251 if (!(referrer instanceof Relation)) { 252 continue; 253 } 254 Relation relation = (Relation) referrer; 255 int position = 0; 256 for (RelationMember member : relation.getMembers()) { 257 if (!member.getMember().hasEqualSemanticAttributes(oldway) 258 || !member.hasRole()) { 259 position++; 260 continue; 261 } 262 263 final String newRole = COMBINED_SWITCHERS.apply(member.getRole()); 264 if (!member.getRole().equals(newRole)) { 265 roleCorrections.add(new RoleCorrection(relation, position, member, newRole)); 266 } 267 268 position++; 269 } 270 } 271 return roleCorrections; 272 } 273 274 static Map<OsmPrimitive, List<TagCorrection>> getTagCorrectionsMap(Way way) { 275 Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = new HashMap<>(); 276 List<TagCorrection> tagCorrections = getTagCorrections(way); 277 if (!tagCorrections.isEmpty()) { 278 tagCorrectionsMap.put(way, tagCorrections); 279 } 280 for (Node node : way.getNodes()) { 281 final List<TagCorrection> corrections = getTagCorrections(node); 282 if (!corrections.isEmpty()) { 283 tagCorrectionsMap.put(node, corrections); 284 } 285 } 286 return tagCorrectionsMap; 287 } 288 289 @Override 290 public Collection<Command> execute(Way oldway, Way way) throws UserCancelException { 291 Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = getTagCorrectionsMap(way); 292 293 Map<OsmPrimitive, List<RoleCorrection>> roleCorrectionMap = new HashMap<>(); 294 List<RoleCorrection> roleCorrections = getRoleCorrections(oldway); 295 if (!roleCorrections.isEmpty()) { 296 roleCorrectionMap.put(way, roleCorrections); 297 } 298 299 return applyCorrections(oldway.getDataSet(), tagCorrectionsMap, roleCorrectionMap, 300 tr("When reversing this way, the following changes are suggested in order to maintain data consistency.")); 301 } 302 303 private static boolean ignoreKeyForCorrection(String key) { 304 for (Pattern ignoredKey : IGNORED_KEYS) { 305 if (ignoredKey.matcher(key).matches()) { 306 return true; 307 } 308 } 309 return false; 310 } 311}