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