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