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