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}