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> IGNORED_KEYS = new ArrayList<>();
056    static {
057        for (String s : OsmPrimitive.getUninterestingKeys()) {
058            IGNORED_KEYS.add(getPatternFor(s));
059        }
060        for (String s : new String[]{"name", "ref", "tiger:county"}) {
061            IGNORED_KEYS.add(getPatternFor(s, false));
062        }
063        for (String s : new String[]{"tiger:county", "turn:lanes", "change:lanes", "placement"}) {
064            IGNORED_KEYS.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.TRUE_VALUE;
145                } else if (OsmUtils.isTrue(value)) {
146                    newValue = OsmUtils.REVERSE_VALUE;
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    /**
193     * Returns the subset of irreversible ways.
194     * @param ways all ways
195     * @return the subset of irreversible ways
196     * @see #isReversible(Way)
197     */
198    public static List<Way> irreversibleWays(List<Way> ways) {
199        List<Way> newWays = new ArrayList<>(ways);
200        for (Way way : ways) {
201            if (isReversible(way)) {
202                newWays.remove(way);
203            }
204        }
205        return newWays;
206    }
207
208    /**
209     * Inverts sign of a numeric value.
210     * @param value numeric value
211     * @return opposite numeric value
212     */
213    public static String invertNumber(String value) {
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);
219        sign = "-".equals(sign) ? "" : "-";
220        return sign + rest;
221    }
222
223    static List<TagCorrection> getTagCorrections(Tagged way) {
224        List<TagCorrection> tagCorrections = new ArrayList<>();
225        for (Map.Entry<String, String> entry : way.getKeys().entrySet()) {
226            final String key = entry.getKey();
227            final String value = entry.getValue();
228            Tag newTag = TagSwitcher.apply(key, value);
229            String newKey = newTag.getKey();
230            String newValue = newTag.getValue();
231
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) {
241                tagCorrections.add(new TagCorrection(key, value, newKey, newValue));
242            }
243        }
244        return tagCorrections;
245    }
246
247    static List<RoleCorrection> getRoleCorrections(Way oldway) {
248        List<RoleCorrection> roleCorrections = new ArrayList<>();
249
250        Collection<OsmPrimitive> referrers = oldway.getReferrers();
251        for (OsmPrimitive referrer: referrers) {
252            if (!(referrer instanceof Relation)) {
253                continue;
254            }
255            Relation relation = (Relation) referrer;
256            int position = 0;
257            for (RelationMember member : relation.getMembers()) {
258                if (!member.getMember().hasEqualSemanticAttributes(oldway)
259                        || !member.hasRole()) {
260                    position++;
261                    continue;
262                }
263
264                final String newRole = COMBINED_SWITCHERS.apply(member.getRole());
265                if (!member.getRole().equals(newRole)) {
266                    roleCorrections.add(new RoleCorrection(relation, position, member, newRole));
267                }
268
269                position++;
270            }
271        }
272        return roleCorrections;
273    }
274
275    static Map<OsmPrimitive, List<TagCorrection>> getTagCorrectionsMap(Way way) {
276        Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = new HashMap<>();
277        List<TagCorrection> tagCorrections = getTagCorrections(way);
278        if (!tagCorrections.isEmpty()) {
279            tagCorrectionsMap.put(way, tagCorrections);
280        }
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    }
289
290    @Override
291    public Collection<Command> execute(Way oldway, Way way) throws UserCancelException {
292        Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = getTagCorrectionsMap(way);
293
294        Map<OsmPrimitive, List<RoleCorrection>> roleCorrectionMap = new HashMap<>();
295        List<RoleCorrection> roleCorrections = getRoleCorrections(oldway);
296        if (!roleCorrections.isEmpty()) {
297            roleCorrectionMap.put(way, roleCorrections);
298        }
299
300        return applyCorrections(oldway.getDataSet(), tagCorrectionsMap, roleCorrectionMap,
301                tr("When reversing this way, the following changes are suggested in order to maintain data consistency."));
302    }
303
304    private static boolean ignoreKeyForCorrection(String key) {
305        for (Pattern ignoredKey : IGNORED_KEYS) {
306            if (ignoredKey.matcher(key).matches()) {
307                return true;
308            }
309        }
310        return false;
311    }
312}