// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.corrector; import static org.openstreetmap.josm.tools.I18n.tr; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.data.correction.RoleCorrection; import org.openstreetmap.josm.data.correction.TagCorrection; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.OsmUtils; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.data.osm.Tag; import org.openstreetmap.josm.data.osm.TagCollection; import org.openstreetmap.josm.data.osm.Tagged; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.tools.UserCancelException; /** * A ReverseWayTagCorrector handles necessary corrections of tags * when a way is reversed. E.g. oneway=yes needs to be changed * to oneway=-1 and vice versa. * * The Corrector offers the automatic resolution in an dialog * for the user to confirm. */ public class ReverseWayTagCorrector extends TagCorrector<Way> { private static final String SEPARATOR = "[:_]"; private static Pattern getPatternFor(String s) { return getPatternFor(s, false); } private static Pattern getPatternFor(String s, boolean exactMatch) { if (exactMatch) { return Pattern.compile("(^)(" + s + ")($)"); } else { return Pattern.compile("(^|.*" + SEPARATOR + ")(" + s + ")(" + SEPARATOR + ".*|$)", Pattern.CASE_INSENSITIVE); } } private static final Collection<Pattern> ignoredKeys = new ArrayList<>(); static { for (String s : OsmPrimitive.getUninterestingKeys()) { ignoredKeys.add(getPatternFor(s)); } for (String s : new String[]{"name", "ref", "tiger:county"}) { ignoredKeys.add(getPatternFor(s, false)); } for (String s : new String[]{"tiger:county", "turn:lanes", "change:lanes", "placement"}) { ignoredKeys.add(getPatternFor(s, true)); } } private interface IStringSwitcher extends Function<String, String> { static IStringSwitcher combined(IStringSwitcher... switchers) { return key -> { for (IStringSwitcher switcher : switchers) { final String newKey = switcher.apply(key); if (!key.equals(newKey)) { return newKey; } } return key; }; } } private static class StringSwitcher implements IStringSwitcher { private final String a; private final String b; private final Pattern pattern; StringSwitcher(String a, String b) { this.a = a; this.b = b; this.pattern = getPatternFor(a + '|' + b); } @Override public String apply(String text) { Matcher m = pattern.matcher(text); if (m.lookingAt()) { String leftRight = m.group(2).toLowerCase(Locale.ENGLISH); StringBuilder result = new StringBuilder(); result.append(text.substring(0, m.start(2))) .append(leftRight.equals(a) ? b : a) .append(text.substring(m.end(2))); return result.toString(); } return text; } } /** * Reverses a given tag. * @since 5787 */ public static final class TagSwitcher { private TagSwitcher() { // Hide implicit public constructor for utility class } /** * Reverses a given tag. * @param tag The tag to reverse * @return The reversed tag (is equal to <code>tag</code> if no change is needed) */ public static Tag apply(final Tag tag) { return apply(tag.getKey(), tag.getValue()); } /** * Reverses a given tag (key=value). * @param key The tag key * @param value The tag value * @return The reversed tag (is equal to <code>key=value</code> if no change is needed) */ public static Tag apply(final String key, final String value) { String newKey = key; String newValue = value; if (key.startsWith("oneway") || key.endsWith("oneway")) { if (OsmUtils.isReversed(value)) { newValue = OsmUtils.trueval; } else if (OsmUtils.isTrue(value)) { newValue = OsmUtils.reverseval; } newKey = COMBINED_SWITCHERS.apply(key); } else if (key.startsWith("incline") || key.endsWith("incline")) { newValue = UP_DOWN.apply(value); if (newValue.equals(value)) { newValue = invertNumber(value); } } else if (key.startsWith("direction") || key.endsWith("direction")) { newValue = COMBINED_SWITCHERS.apply(value); } else if (key.endsWith(":forward") || key.endsWith(":backward")) { // Change key but not left/right value (fix #8518) newKey = FORWARD_BACKWARD.apply(key); } else if (!ignoreKeyForCorrection(key)) { newKey = COMBINED_SWITCHERS.apply(key); newValue = COMBINED_SWITCHERS.apply(value); } return new Tag(newKey, newValue); } } private static final StringSwitcher FORWARD_BACKWARD = new StringSwitcher("forward", "backward"); private static final StringSwitcher UP_DOWN = new StringSwitcher("up", "down"); private static final IStringSwitcher COMBINED_SWITCHERS = IStringSwitcher.combined( new StringSwitcher("left", "right"), new StringSwitcher("forwards", "backwards"), new StringSwitcher("east", "west"), new StringSwitcher("north", "south"), FORWARD_BACKWARD, UP_DOWN ); /** * Tests whether way can be reversed without semantic change, i.e., whether tags have to be changed. * Looks for keys like oneway, oneway:bicycle, cycleway:right:oneway, left/right. * @param way way to test * @return false if tags should be changed to keep semantic, true otherwise. */ public static boolean isReversible(Way way) { for (Tag tag : TagCollection.from(way)) { if (!tag.equals(TagSwitcher.apply(tag))) { return false; } } return true; } public static List<Way> irreversibleWays(List<Way> ways) { List<Way> newWays = new ArrayList<>(ways); for (Way way : ways) { if (isReversible(way)) { newWays.remove(way); } } return newWays; } public static String invertNumber(String value) { Pattern pattern = Pattern.compile("^([+-]?)(\\d.*)$", Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(value); if (!matcher.matches()) return value; String sign = matcher.group(1); String rest = matcher.group(2); sign = "-".equals(sign) ? "" : "-"; return sign + rest; } static List<TagCorrection> getTagCorrections(Tagged way) { List<TagCorrection> tagCorrections = new ArrayList<>(); for (Map.Entry<String, String> entry : way.getKeys().entrySet()) { final String key = entry.getKey(); final String value = entry.getValue(); Tag newTag = TagSwitcher.apply(key, value); String newKey = newTag.getKey(); String newValue = newTag.getValue(); boolean needsCorrection = !key.equals(newKey); if (way.get(newKey) != null && way.get(newKey).equals(newValue)) { needsCorrection = false; } if (!value.equals(newValue)) { needsCorrection = true; } if (needsCorrection) { tagCorrections.add(new TagCorrection(key, value, newKey, newValue)); } } return tagCorrections; } static List<RoleCorrection> getRoleCorrections(Way oldway) { List<RoleCorrection> roleCorrections = new ArrayList<>(); Collection<OsmPrimitive> referrers = oldway.getReferrers(); for (OsmPrimitive referrer: referrers) { if (!(referrer instanceof Relation)) { continue; } Relation relation = (Relation) referrer; int position = 0; for (RelationMember member : relation.getMembers()) { if (!member.getMember().hasEqualSemanticAttributes(oldway) || !member.hasRole()) { position++; continue; } final String newRole = COMBINED_SWITCHERS.apply(member.getRole()); if (!member.getRole().equals(newRole)) { roleCorrections.add(new RoleCorrection(relation, position, member, newRole)); } position++; } } return roleCorrections; } static Map<OsmPrimitive, List<TagCorrection>> getTagCorrectionsMap(Way way) { Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = new HashMap<>(); List<TagCorrection> tagCorrections = getTagCorrections((Tagged) way); if (!tagCorrections.isEmpty()) { tagCorrectionsMap.put(way, tagCorrections); } for (Node node : way.getNodes()) { final List<TagCorrection> corrections = getTagCorrections(node); if (!corrections.isEmpty()) { tagCorrectionsMap.put(node, corrections); } } return tagCorrectionsMap; } @Override public Collection<Command> execute(Way oldway, Way way) throws UserCancelException { Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = getTagCorrectionsMap(way); Map<OsmPrimitive, List<RoleCorrection>> roleCorrectionMap = new HashMap<>(); List<RoleCorrection> roleCorrections = getRoleCorrections(oldway); if (!roleCorrections.isEmpty()) { roleCorrectionMap.put(way, roleCorrections); } return applyCorrections(tagCorrectionsMap, roleCorrectionMap, tr("When reversing this way, the following changes are suggested in order to maintain data consistency.")); } private static boolean ignoreKeyForCorrection(String key) { for (Pattern ignoredKey : ignoredKeys) { if (ignoredKey.matcher(key).matches()) { return true; } } return false; } }