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