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