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