001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions.upload;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.Collection;
007import java.util.Collections;
008import java.util.HashMap;
009import java.util.LinkedList;
010import java.util.List;
011import java.util.Map;
012import java.util.Map.Entry;
013
014import org.openstreetmap.josm.Main;
015import org.openstreetmap.josm.command.ChangePropertyCommand;
016import org.openstreetmap.josm.command.Command;
017import org.openstreetmap.josm.command.SequenceCommand;
018import org.openstreetmap.josm.data.APIDataSet;
019import org.openstreetmap.josm.data.osm.OsmPrimitive;
020import org.openstreetmap.josm.data.osm.Relation;
021import org.openstreetmap.josm.data.osm.Tag;
022
023/**
024 * Fixes defective data entries for all modified objects before upload
025 * @since 5621
026 */
027public class FixDataHook implements UploadHook {
028
029    /**
030     * List of checks to run on data
031     */
032    private List<FixData> deprecated = new LinkedList<>();
033
034    /**
035     * Constructor for data initialization
036     */
037    public FixDataHook () {
038        deprecated.add(new FixDataSpace());
039        deprecated.add(new FixDataKey("color",            "colour"));
040        deprecated.add(new FixDataTag("highway", "ford",  "ford",    "yes"));
041        deprecated.add(new FixDataTag("oneway",  "false", "oneway",  "no"));
042        deprecated.add(new FixDataTag("oneway",  "0",     "oneway",  "no"));
043        deprecated.add(new FixDataTag("oneway",  "true",  "oneway",  "yes"));
044        deprecated.add(new FixDataTag("oneway",  "1",     "oneway",  "yes"));
045        deprecated.add(new FixDataTag("highway", "stile", "barrier", "stile"));
046        deprecated.add(new FixData() {
047            @Override
048            public boolean fixKeys(Map<String, String> keys, OsmPrimitive osm) {
049                if(osm instanceof Relation && "multipolygon".equals(keys.get("type")) && "administrative".equals(keys.get("boundary"))) {
050                    keys.put("type", "boundary");
051                    return true;
052                }
053                return false;
054            }
055        });
056    }
057
058    /**
059     * Common set of commands for data fixing
060     */
061    public interface FixData {
062        /**
063         * Checks if data needs to be fixed and change keys
064         *
065         * @param keys list of keys to be modified
066         * @param osm the object for type validation, don't use keys of it!
067         * @return <code>true</code> if keys have been modified
068         */
069        public boolean fixKeys(Map<String, String> keys, OsmPrimitive osm);
070    }
071
072    /**
073     * Data fix to remove spaces at begin or end of tags
074     */
075    public static class FixDataSpace implements FixData {
076        @Override
077        public boolean fixKeys(Map<String, String> keys, OsmPrimitive osm) {
078            Map<String, String> newKeys = new HashMap<>(keys);
079            for (Entry<String, String> e : keys.entrySet()) {
080                String v = Tag.removeWhiteSpaces(e.getValue());
081                String k = Tag.removeWhiteSpaces(e.getKey());
082                boolean drop = k.isEmpty() || v.isEmpty();
083                if(!e.getKey().equals(k)) {
084                    if(drop || !keys.containsKey(k)) {
085                        newKeys.remove(e.getKey());
086                        if(!drop)
087                            newKeys.put(k, v);
088                    }
089                } else if(!e.getValue().equals(v)) {
090                    if(v.isEmpty())
091                        newKeys.remove(k);
092                    else
093                        newKeys.put(k, v);
094                } else if (drop) {
095                    newKeys.remove(e.getKey());
096                }
097            }
098            boolean changed = !keys.equals(newKeys);
099            if (changed) {
100                keys.clear();
101                keys.putAll(newKeys);
102            }
103            return changed;
104        }
105    }
106
107    /**
108     * Data fix to cleanup wrong spelled keys
109     */
110    public static class FixDataKey implements FixData {
111        /** key of wrong data */
112        String oldKey;
113        /** key of correct data */
114        String newKey;
115
116        /**
117         * Setup key check for wrong spelled keys
118         *
119         * @param oldKey wrong spelled key
120         * @param newKey correct replacement
121         */
122        public FixDataKey(String oldKey, String newKey) {
123            this.oldKey = oldKey;
124            this.newKey = newKey;
125        }
126
127        @Override
128        public boolean fixKeys(Map<String, String> keys, OsmPrimitive osm) {
129            if(keys.containsKey(oldKey) && !keys.containsKey(newKey)) {
130                keys.put(newKey, keys.get(oldKey));
131                keys.remove(oldKey);
132                return true;
133            }
134            return false;
135        }
136    }
137
138    /**
139     * Data fix to cleanup wrong spelled tags
140     */
141    public static class FixDataTag implements FixData {
142        /** key of wrong data */
143        String oldKey;
144        /** value of wrong data */
145        String oldValue;
146        /** key of correct data */
147        String newKey;
148        /** value of correct data */
149        String newValue;
150
151        /**
152         * Setup key check for wrong spelled keys
153         *
154         * @param oldKey wrong or old key
155         * @param oldValue wrong or old value
156         * @param newKey correct key replacement
157         * @param newValue correct value replacement
158         */
159        public FixDataTag(String oldKey, String oldValue, String newKey, String newValue) {
160            this.oldKey = oldKey;
161            this.oldValue = oldValue;
162            this.newKey = newKey;
163            this.newValue = newValue;
164        }
165
166        @Override
167        public boolean fixKeys(Map<String, String> keys, OsmPrimitive osm) {
168            if(oldValue.equals(keys.get(oldKey)) && (newKey.equals(oldKey) || !keys.containsKey(newKey))) {
169                keys.put(newKey, newValue);
170                if(!newKey.equals(oldKey))
171                    keys.remove(oldKey);
172                return true;
173            }
174            return false;
175        }
176    }
177
178    /**
179     * Checks the upload for deprecated or wrong tags.
180     * @param apiDataSet the data to upload
181     */
182    @Override
183    public boolean checkUpload(APIDataSet apiDataSet) {
184        if(!Main.pref.getBoolean("fix.data.on.upload", true))
185            return true;
186
187        List<OsmPrimitive> objectsToUpload = apiDataSet.getPrimitives();
188        Collection<Command> cmds = new LinkedList<>();
189
190        for (OsmPrimitive osm : objectsToUpload) {
191            Map<String, String> keys = osm.getKeys();
192            if(!keys.isEmpty()) {
193                boolean modified = false;
194                for (FixData fix : deprecated) {
195                    if(fix.fixKeys(keys, osm))
196                        modified = true;
197                }
198                if(modified)
199                    cmds.add(new ChangePropertyCommand(Collections.singleton(osm), new HashMap<>(keys)));
200            }
201        }
202
203        if(!cmds.isEmpty())
204            Main.main.undoRedo.add(new SequenceCommand(tr("Fix deprecated tags"), cmds));
205        return true;
206    }
207}