[729] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
| 2 | package org.openstreetmap.josm.corrector;
|
---|
| 3 |
|
---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
| 5 |
|
---|
| 6 | import java.util.ArrayList;
|
---|
[764] | 7 | import java.util.Collection;
|
---|
| 8 | import java.util.HashMap;
|
---|
| 9 | import java.util.List;
|
---|
[8404] | 10 | import java.util.Locale;
|
---|
[764] | 11 | import java.util.Map;
|
---|
[11061] | 12 | import java.util.function.Function;
|
---|
[729] | 13 | import java.util.regex.Matcher;
|
---|
| 14 | import java.util.regex.Pattern;
|
---|
| 15 |
|
---|
[1000] | 16 | import org.openstreetmap.josm.command.Command;
|
---|
[10125] | 17 | import org.openstreetmap.josm.data.correction.RoleCorrection;
|
---|
| 18 | import org.openstreetmap.josm.data.correction.TagCorrection;
|
---|
[11061] | 19 | import org.openstreetmap.josm.data.osm.Node;
|
---|
[764] | 20 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
[729] | 21 | import org.openstreetmap.josm.data.osm.OsmUtils;
|
---|
[1000] | 22 | import org.openstreetmap.josm.data.osm.Relation;
|
---|
| 23 | import org.openstreetmap.josm.data.osm.RelationMember;
|
---|
[5787] | 24 | import org.openstreetmap.josm.data.osm.Tag;
|
---|
| 25 | import org.openstreetmap.josm.data.osm.TagCollection;
|
---|
[9539] | 26 | import org.openstreetmap.josm.data.osm.Tagged;
|
---|
[729] | 27 | import org.openstreetmap.josm.data.osm.Way;
|
---|
[8919] | 28 | import org.openstreetmap.josm.tools.UserCancelException;
|
---|
[729] | 29 |
|
---|
[3210] | 30 | /**
|
---|
| 31 | * A ReverseWayTagCorrector handles necessary corrections of tags
|
---|
| 32 | * when a way is reversed. E.g. oneway=yes needs to be changed
|
---|
| 33 | * to oneway=-1 and vice versa.
|
---|
| 34 | *
|
---|
| 35 | * The Corrector offers the automatic resolution in an dialog
|
---|
| 36 | * for the user to confirm.
|
---|
| 37 | */
|
---|
[729] | 38 | public class ReverseWayTagCorrector extends TagCorrector<Way> {
|
---|
| 39 |
|
---|
[5787] | 40 | private static final String SEPARATOR = "[:_]";
|
---|
[729] | 41 |
|
---|
[8512] | 42 | private static Pattern getPatternFor(String s) {
|
---|
[5787] | 43 | return getPatternFor(s, false);
|
---|
| 44 | }
|
---|
[2676] | 45 |
|
---|
[8512] | 46 | private static Pattern getPatternFor(String s, boolean exactMatch) {
|
---|
[5787] | 47 | if (exactMatch) {
|
---|
| 48 | return Pattern.compile("(^)(" + s + ")($)");
|
---|
| 49 | } else {
|
---|
| 50 | return Pattern.compile("(^|.*" + SEPARATOR + ")(" + s + ")(" + SEPARATOR + ".*|$)",
|
---|
| 51 | Pattern.CASE_INSENSITIVE);
|
---|
| 52 | }
|
---|
| 53 | }
|
---|
[6069] | 54 |
|
---|
[12537] | 55 | private static final Collection<Pattern> IGNORED_KEYS = new ArrayList<>();
|
---|
[5787] | 56 | static {
|
---|
| 57 | for (String s : OsmPrimitive.getUninterestingKeys()) {
|
---|
[12537] | 58 | IGNORED_KEYS.add(getPatternFor(s));
|
---|
[5787] | 59 | }
|
---|
| 60 | for (String s : new String[]{"name", "ref", "tiger:county"}) {
|
---|
[12537] | 61 | IGNORED_KEYS.add(getPatternFor(s, false));
|
---|
[5787] | 62 | }
|
---|
| 63 | for (String s : new String[]{"tiger:county", "turn:lanes", "change:lanes", "placement"}) {
|
---|
[12537] | 64 | IGNORED_KEYS.add(getPatternFor(s, true));
|
---|
[5787] | 65 | }
|
---|
| 66 | }
|
---|
[6069] | 67 |
|
---|
[11061] | 68 | private interface IStringSwitcher extends Function<String, String> {
|
---|
[5787] | 69 |
|
---|
[11061] | 70 | static IStringSwitcher combined(IStringSwitcher... switchers) {
|
---|
| 71 | return key -> {
|
---|
| 72 | for (IStringSwitcher switcher : switchers) {
|
---|
| 73 | final String newKey = switcher.apply(key);
|
---|
| 74 | if (!key.equals(newKey)) {
|
---|
| 75 | return newKey;
|
---|
| 76 | }
|
---|
| 77 | }
|
---|
| 78 | return key;
|
---|
| 79 | };
|
---|
| 80 | }
|
---|
| 81 | }
|
---|
| 82 |
|
---|
| 83 | private static class StringSwitcher implements IStringSwitcher {
|
---|
| 84 |
|
---|
[1169] | 85 | private final String a;
|
---|
| 86 | private final String b;
|
---|
[5777] | 87 | private final Pattern pattern;
|
---|
[729] | 88 |
|
---|
[8836] | 89 | StringSwitcher(String a, String b) {
|
---|
[1000] | 90 | this.a = a;
|
---|
| 91 | this.b = b;
|
---|
[8846] | 92 | this.pattern = getPatternFor(a + '|' + b);
|
---|
[1169] | 93 | }
|
---|
[764] | 94 |
|
---|
[11061] | 95 | @Override
|
---|
[1169] | 96 | public String apply(String text) {
|
---|
[5777] | 97 | Matcher m = pattern.matcher(text);
|
---|
[764] | 98 |
|
---|
[1169] | 99 | if (m.lookingAt()) {
|
---|
[8404] | 100 | String leftRight = m.group(2).toLowerCase(Locale.ENGLISH);
|
---|
[764] | 101 |
|
---|
[1169] | 102 | StringBuilder result = new StringBuilder();
|
---|
[8379] | 103 | result.append(text.substring(0, m.start(2)))
|
---|
| 104 | .append(leftRight.equals(a) ? b : a)
|
---|
| 105 | .append(text.substring(m.end(2)));
|
---|
[764] | 106 |
|
---|
[1169] | 107 | return result.toString();
|
---|
| 108 | }
|
---|
| 109 | return text;
|
---|
| 110 | }
|
---|
| 111 | }
|
---|
[764] | 112 |
|
---|
[5787] | 113 | /**
|
---|
| 114 | * Reverses a given tag.
|
---|
| 115 | * @since 5787
|
---|
| 116 | */
|
---|
[6987] | 117 | public static final class TagSwitcher {
|
---|
[6069] | 118 |
|
---|
[6986] | 119 | private TagSwitcher() {
|
---|
| 120 | // Hide implicit public constructor for utility class
|
---|
| 121 | }
|
---|
| 122 |
|
---|
[5787] | 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 | */
|
---|
[8374] | 128 | public static Tag apply(final Tag tag) {
|
---|
[5787] | 129 | return apply(tag.getKey(), tag.getValue());
|
---|
| 130 | }
|
---|
[6069] | 131 |
|
---|
[5787] | 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 | */
|
---|
[8374] | 138 | public static Tag apply(final String key, final String value) {
|
---|
[5787] | 139 | String newKey = key;
|
---|
| 140 | String newValue = value;
|
---|
| 141 |
|
---|
| 142 | if (key.startsWith("oneway") || key.endsWith("oneway")) {
|
---|
| 143 | if (OsmUtils.isReversed(value)) {
|
---|
[12187] | 144 | newValue = OsmUtils.TRUE_VALUE;
|
---|
[5787] | 145 | } else if (OsmUtils.isTrue(value)) {
|
---|
[12187] | 146 | newValue = OsmUtils.REVERSE_VALUE;
|
---|
[5787] | 147 | }
|
---|
[11061] | 148 | newKey = COMBINED_SWITCHERS.apply(key);
|
---|
| 149 | } else if (key.startsWith("incline") || key.endsWith("incline")) {
|
---|
[5787] | 150 | newValue = UP_DOWN.apply(value);
|
---|
| 151 | if (newValue.equals(value)) {
|
---|
| 152 | newValue = invertNumber(value);
|
---|
| 153 | }
|
---|
[11061] | 154 | } else if (key.startsWith("direction") || key.endsWith("direction")) {
|
---|
| 155 | newValue = COMBINED_SWITCHERS.apply(value);
|
---|
[5787] | 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)) {
|
---|
[11061] | 160 | newKey = COMBINED_SWITCHERS.apply(key);
|
---|
| 161 | newValue = COMBINED_SWITCHERS.apply(value);
|
---|
[5787] | 162 | }
|
---|
| 163 | return new Tag(newKey, newValue);
|
---|
| 164 | }
|
---|
| 165 | }
|
---|
[6069] | 166 |
|
---|
[5787] | 167 | private static final StringSwitcher FORWARD_BACKWARD = new StringSwitcher("forward", "backward");
|
---|
| 168 | private static final StringSwitcher UP_DOWN = new StringSwitcher("up", "down");
|
---|
[11061] | 169 | private static final IStringSwitcher COMBINED_SWITCHERS = IStringSwitcher.combined(
|
---|
[5787] | 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
|
---|
[11061] | 175 | );
|
---|
[764] | 176 |
|
---|
[5599] | 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.
|
---|
[8470] | 180 | * @param way way to test
|
---|
[5599] | 181 | * @return false if tags should be changed to keep semantic, true otherwise.
|
---|
| 182 | */
|
---|
[2573] | 183 | public static boolean isReversible(Way way) {
|
---|
[5787] | 184 | for (Tag tag : TagCollection.from(way)) {
|
---|
| 185 | if (!tag.equals(TagSwitcher.apply(tag))) {
|
---|
| 186 | return false;
|
---|
[5599] | 187 | }
|
---|
[2573] | 188 | }
|
---|
| 189 | return true;
|
---|
| 190 | }
|
---|
| 191 |
|
---|
[13158] | 192 | /**
|
---|
| 193 | * Returns the subset of irreversible ways.
|
---|
| 194 | * @param ways all ways
|
---|
| 195 | * @return the subset of irreversible ways
|
---|
| 196 | * @see #isReversible(Way)
|
---|
| 197 | */
|
---|
[2573] | 198 | public static List<Way> irreversibleWays(List<Way> ways) {
|
---|
[7005] | 199 | List<Way> newWays = new ArrayList<>(ways);
|
---|
[2573] | 200 | for (Way way : ways) {
|
---|
| 201 | if (isReversible(way)) {
|
---|
| 202 | newWays.remove(way);
|
---|
| 203 | }
|
---|
| 204 | }
|
---|
| 205 | return newWays;
|
---|
| 206 | }
|
---|
| 207 |
|
---|
[13158] | 208 | /**
|
---|
| 209 | * Inverts sign of a numeric value.
|
---|
| 210 | * @param value numeric value
|
---|
| 211 | * @return opposite numeric value
|
---|
| 212 | */
|
---|
[5787] | 213 | public static String invertNumber(String value) {
|
---|
[2683] | 214 | Pattern pattern = Pattern.compile("^([+-]?)(\\d.*)$", Pattern.CASE_INSENSITIVE);
|
---|
| 215 | Matcher matcher = pattern.matcher(value);
|
---|
| 216 | if (!matcher.matches()) return value;
|
---|
| 217 | String sign = matcher.group(1);
|
---|
| 218 | String rest = matcher.group(2);
|
---|
[6990] | 219 | sign = "-".equals(sign) ? "" : "-";
|
---|
[2683] | 220 | return sign + rest;
|
---|
| 221 | }
|
---|
| 222 |
|
---|
[9539] | 223 | static List<TagCorrection> getTagCorrections(Tagged way) {
|
---|
[7005] | 224 | List<TagCorrection> tagCorrections = new ArrayList<>();
|
---|
[11061] | 225 | for (Map.Entry<String, String> entry : way.getKeys().entrySet()) {
|
---|
| 226 | final String key = entry.getKey();
|
---|
| 227 | final String value = entry.getValue();
|
---|
[5787] | 228 | Tag newTag = TagSwitcher.apply(key, value);
|
---|
| 229 | String newKey = newTag.getKey();
|
---|
| 230 | String newValue = newTag.getValue();
|
---|
[764] | 231 |
|
---|
[3413] | 232 | boolean needsCorrection = !key.equals(newKey);
|
---|
| 233 | if (way.get(newKey) != null && way.get(newKey).equals(newValue)) {
|
---|
| 234 | needsCorrection = false;
|
---|
| 235 | }
|
---|
| 236 | if (!value.equals(newValue)) {
|
---|
| 237 | needsCorrection = true;
|
---|
| 238 | }
|
---|
| 239 |
|
---|
| 240 | if (needsCorrection) {
|
---|
[2683] | 241 | tagCorrections.add(new TagCorrection(key, value, newKey, newValue));
|
---|
[1169] | 242 | }
|
---|
| 243 | }
|
---|
[9539] | 244 | return tagCorrections;
|
---|
| 245 | }
|
---|
[1000] | 246 |
|
---|
[9539] | 247 | static List<RoleCorrection> getRoleCorrections(Way oldway) {
|
---|
[7005] | 248 | List<RoleCorrection> roleCorrections = new ArrayList<>();
|
---|
[1000] | 249 |
|
---|
[2611] | 250 | Collection<OsmPrimitive> referrers = oldway.getReferrers();
|
---|
| 251 | for (OsmPrimitive referrer: referrers) {
|
---|
[8443] | 252 | if (!(referrer instanceof Relation)) {
|
---|
[2611] | 253 | continue;
|
---|
| 254 | }
|
---|
[8510] | 255 | Relation relation = (Relation) referrer;
|
---|
[1614] | 256 | int position = 0;
|
---|
[1925] | 257 | for (RelationMember member : relation.getMembers()) {
|
---|
[1938] | 258 | if (!member.getMember().hasEqualSemanticAttributes(oldway)
|
---|
[1930] | 259 | || !member.hasRole()) {
|
---|
[1614] | 260 | position++;
|
---|
[1169] | 261 | continue;
|
---|
[1614] | 262 | }
|
---|
[1000] | 263 |
|
---|
[11061] | 264 | final String newRole = COMBINED_SWITCHERS.apply(member.getRole());
|
---|
| 265 | if (!member.getRole().equals(newRole)) {
|
---|
[2683] | 266 | roleCorrections.add(new RoleCorrection(relation, position, member, newRole));
|
---|
[1762] | 267 | }
|
---|
[1614] | 268 |
|
---|
| 269 | position++;
|
---|
[1169] | 270 | }
|
---|
| 271 | }
|
---|
[9539] | 272 | return roleCorrections;
|
---|
| 273 | }
|
---|
| 274 |
|
---|
[11061] | 275 | static Map<OsmPrimitive, List<TagCorrection>> getTagCorrectionsMap(Way way) {
|
---|
[9539] | 276 | Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = new HashMap<>();
|
---|
[12187] | 277 | List<TagCorrection> tagCorrections = getTagCorrections(way);
|
---|
[9539] | 278 | if (!tagCorrections.isEmpty()) {
|
---|
| 279 | tagCorrectionsMap.put(way, tagCorrections);
|
---|
| 280 | }
|
---|
[11061] | 281 | for (Node node : way.getNodes()) {
|
---|
| 282 | final List<TagCorrection> corrections = getTagCorrections(node);
|
---|
| 283 | if (!corrections.isEmpty()) {
|
---|
| 284 | tagCorrectionsMap.put(node, corrections);
|
---|
| 285 | }
|
---|
| 286 | }
|
---|
| 287 | return tagCorrectionsMap;
|
---|
| 288 | }
|
---|
[9539] | 289 |
|
---|
[11061] | 290 | @Override
|
---|
| 291 | public Collection<Command> execute(Way oldway, Way way) throws UserCancelException {
|
---|
| 292 | Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = getTagCorrectionsMap(way);
|
---|
| 293 |
|
---|
[9539] | 294 | Map<OsmPrimitive, List<RoleCorrection>> roleCorrectionMap = new HashMap<>();
|
---|
| 295 | List<RoleCorrection> roleCorrections = getRoleCorrections(oldway);
|
---|
[2683] | 296 | if (!roleCorrections.isEmpty()) {
|
---|
| 297 | roleCorrectionMap.put(way, roleCorrections);
|
---|
| 298 | }
|
---|
[1169] | 299 |
|
---|
[12412] | 300 | return applyCorrections(oldway.getDataSet(), tagCorrectionsMap, roleCorrectionMap,
|
---|
[6326] | 301 | tr("When reversing this way, the following changes are suggested in order to maintain data consistency."));
|
---|
[1169] | 302 | }
|
---|
[4345] | 303 |
|
---|
[5787] | 304 | private static boolean ignoreKeyForCorrection(String key) {
|
---|
[12537] | 305 | for (Pattern ignoredKey : IGNORED_KEYS) {
|
---|
[5787] | 306 | if (ignoredKey.matcher(key).matches()) {
|
---|
| 307 | return true;
|
---|
| 308 | }
|
---|
| 309 | }
|
---|
| 310 | return false;
|
---|
[4345] | 311 | }
|
---|
[729] | 312 | }
|
---|