001// License: GPL. See LICENSE file for details.
002package org.openstreetmap.josm.data.validation;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.BufferedReader;
007import java.io.File;
008import java.io.FileNotFoundException;
009import java.io.FileReader;
010import java.io.FileWriter;
011import java.io.IOException;
012import java.io.PrintWriter;
013import java.util.ArrayList;
014import java.util.Arrays;
015import java.util.Collection;
016import java.util.HashMap;
017import java.util.Map;
018import java.util.TreeSet;
019import java.util.regex.Matcher;
020import java.util.regex.Pattern;
021
022import javax.swing.JOptionPane;
023
024import org.openstreetmap.josm.Main;
025import org.openstreetmap.josm.actions.ValidateAction;
026import org.openstreetmap.josm.data.validation.tests.Addresses;
027import org.openstreetmap.josm.data.validation.tests.BarriersEntrances;
028import org.openstreetmap.josm.data.validation.tests.BuildingInBuilding;
029import org.openstreetmap.josm.data.validation.tests.Coastlines;
030import org.openstreetmap.josm.data.validation.tests.CrossingWays;
031import org.openstreetmap.josm.data.validation.tests.DeprecatedTags;
032import org.openstreetmap.josm.data.validation.tests.DuplicateNode;
033import org.openstreetmap.josm.data.validation.tests.DuplicateRelation;
034import org.openstreetmap.josm.data.validation.tests.DuplicateWay;
035import org.openstreetmap.josm.data.validation.tests.DuplicatedWayNodes;
036import org.openstreetmap.josm.data.validation.tests.Highways;
037import org.openstreetmap.josm.data.validation.tests.MultipolygonTest;
038import org.openstreetmap.josm.data.validation.tests.NameMismatch;
039import org.openstreetmap.josm.data.validation.tests.NodesDuplicatingWayTags;
040import org.openstreetmap.josm.data.validation.tests.NodesWithSameName;
041import org.openstreetmap.josm.data.validation.tests.OpeningHourTest;
042import org.openstreetmap.josm.data.validation.tests.OverlappingAreas;
043import org.openstreetmap.josm.data.validation.tests.OverlappingWays;
044import org.openstreetmap.josm.data.validation.tests.PowerLines;
045import org.openstreetmap.josm.data.validation.tests.RelationChecker;
046import org.openstreetmap.josm.data.validation.tests.SelfIntersectingWay;
047import org.openstreetmap.josm.data.validation.tests.SimilarNamedWays;
048import org.openstreetmap.josm.data.validation.tests.TagChecker;
049import org.openstreetmap.josm.data.validation.tests.TurnrestrictionTest;
050import org.openstreetmap.josm.data.validation.tests.UnclosedWays;
051import org.openstreetmap.josm.data.validation.tests.UnconnectedWays;
052import org.openstreetmap.josm.data.validation.tests.UntaggedNode;
053import org.openstreetmap.josm.data.validation.tests.UntaggedWay;
054import org.openstreetmap.josm.data.validation.tests.WayConnectedToArea;
055import org.openstreetmap.josm.data.validation.tests.WronglyOrderedWays;
056import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
057import org.openstreetmap.josm.gui.layer.Layer;
058import org.openstreetmap.josm.gui.layer.OsmDataLayer;
059import org.openstreetmap.josm.gui.layer.ValidatorLayer;
060import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
061import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
062import org.openstreetmap.josm.tools.Utils;
063
064/**
065 * A OSM data validator.
066 *
067 * @author Francisco R. Santos <frsantos@gmail.com>
068 */
069public class OsmValidator implements LayerChangeListener {
070
071    public static ValidatorLayer errorLayer = null;
072
073    /** The validate action */
074    public ValidateAction validateAction = new ValidateAction();
075
076    /** Grid detail, multiplier of east,north values for valuable cell sizing */
077    public static double griddetail;
078
079    public static final Collection<String> ignoredErrors = new TreeSet<String>();
080
081    /**
082     * All available tests
083     * TODO: is there any way to find out automatically all available tests?
084     */
085    @SuppressWarnings("unchecked")
086    private static final Class<Test>[] allAvailableTests = new Class[] {
087        DuplicateNode.class, // ID    1 ..   99
088        OverlappingWays.class, // ID  101 ..  199
089        UntaggedNode.class, // ID  201 ..  299
090        UntaggedWay.class, // ID  301 ..  399
091        SelfIntersectingWay.class, // ID  401 ..  499
092        DuplicatedWayNodes.class, // ID  501 ..  599
093        CrossingWays.class, // ID  601 ..  699
094        SimilarNamedWays.class, // ID  701 ..  799
095        NodesWithSameName.class, // ID  801 ..  899
096        Coastlines.class, // ID  901 ..  999
097        WronglyOrderedWays.class, // ID 1001 .. 1099
098        UnclosedWays.class, // ID 1101 .. 1199
099        TagChecker.class, // ID 1201 .. 1299
100        UnconnectedWays.class, // ID 1301 .. 1399
101        DuplicateWay.class, // ID 1401 .. 1499
102        NameMismatch.class, // ID  1501 ..  1599
103        MultipolygonTest.class, // ID  1601 ..  1699
104        RelationChecker.class, // ID  1701 ..  1799
105        TurnrestrictionTest.class, // ID  1801 ..  1899
106        DuplicateRelation.class, // ID 1901 .. 1999
107        BuildingInBuilding.class, // ID 2001 .. 2099
108        DeprecatedTags.class, // ID 2101 .. 2199
109        OverlappingAreas.class, // ID 2201 .. 2299
110        WayConnectedToArea.class, // ID 2301 .. 2399
111        NodesDuplicatingWayTags.class, // ID 2401 .. 2499
112        PowerLines.class, // ID 2501 .. 2599
113        Addresses.class, // ID 2601 .. 2699
114        Highways.class, // ID 2701 .. 2799
115        BarriersEntrances.class, // ID 2801 .. 2899
116        OpeningHourTest.class // 2901 .. 2999
117    };
118    
119    private static Map<String, Test> allTestsMap;
120    static {
121        allTestsMap = new HashMap<String, Test>();
122        for (Class<Test> testClass : allAvailableTests) {
123            try {
124                allTestsMap.put(testClass.getSimpleName(), testClass.newInstance());
125            } catch (Exception e) {
126                Main.error(e);
127                continue;
128            }
129        }
130    }
131
132    /**
133     * Constructs a new {@code OsmValidator}.
134     */
135    public OsmValidator() {
136        checkValidatorDir();
137        initializeGridDetail();
138        initializeTests(getTests());
139        loadIgnoredErrors(); //FIXME: load only when needed
140    }
141
142    /**
143     * Returns the plugin's directory of the plugin
144     *
145     * @return The directory of the plugin
146     */
147    public static String getValidatorDir() {
148        return Main.pref.getPreferencesDir() + "validator/";
149    }
150
151    /**
152     * Check if plugin directory exists (store ignored errors file)
153     */
154    private void checkValidatorDir() {
155        try {
156            File pathDir = new File(getValidatorDir());
157            if (!pathDir.exists()) {
158                pathDir.mkdirs();
159            }
160        } catch (Exception e){
161            e.printStackTrace();
162        }
163    }
164
165    private void loadIgnoredErrors() {
166        ignoredErrors.clear();
167        if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
168            BufferedReader in = null;
169            try {
170                in = new BufferedReader(new FileReader(getValidatorDir() + "ignorederrors"));
171                for (String line = in.readLine(); line != null; line = in.readLine()) {
172                    ignoredErrors.add(line);
173                }
174            } catch (final FileNotFoundException e) {
175                // Ignore
176            } catch (final IOException e) {
177                e.printStackTrace();
178            } finally {
179                Utils.close(in);
180            }
181        }
182    }
183
184    public static void addIgnoredError(String s) {
185        ignoredErrors.add(s);
186    }
187
188    public static boolean hasIgnoredError(String s) {
189        return ignoredErrors.contains(s);
190    }
191
192    public static void saveIgnoredErrors() {
193        PrintWriter out = null;
194        try {
195            out = new PrintWriter(new FileWriter(getValidatorDir() + "ignorederrors"), false);
196            for (String e : ignoredErrors) {
197                out.println(e);
198            }
199        } catch (IOException e) {
200            e.printStackTrace();
201        } finally {
202            Utils.close(out);
203        }
204    }
205
206    public static void initializeErrorLayer() {
207        if (!Main.pref.getBoolean(ValidatorPreference.PREF_LAYER, true))
208            return;
209        if (errorLayer == null) {
210            errorLayer = new ValidatorLayer();
211            Main.main.addLayer(errorLayer);
212        }
213    }
214
215    /** 
216     * Gets a map from simple names to all tests. 
217     * @return A map of all tests, indexed by the simple name of their Java class 
218     */
219    public static Map<String, Test> getAllTestsMap() {
220        applyPrefs(allTestsMap, false);
221        applyPrefs(allTestsMap, true);
222        return new HashMap<String, Test>(allTestsMap);
223    }
224
225    private static void applyPrefs(Map<String, Test> tests, boolean beforeUpload) {
226        Pattern regexp = Pattern.compile("(\\w+)=(true|false),?");
227        Matcher m = regexp.matcher(Main.pref.get(beforeUpload ? ValidatorPreference.PREF_TESTS_BEFORE_UPLOAD
228                : ValidatorPreference.PREF_TESTS));
229        int pos = 0;
230        while (m.find(pos)) {
231            String testName = m.group(1);
232            Test test = tests.get(testName);
233            if (test != null) {
234                boolean enabled = Boolean.valueOf(m.group(2));
235                if (beforeUpload) {
236                    test.testBeforeUpload = enabled;
237                } else {
238                    test.enabled = enabled;
239                }
240            }
241            pos = m.end();
242        }
243    }
244
245    public static Collection<Test> getTests() {
246        return getAllTestsMap().values();
247    }
248
249    public static Collection<Test> getEnabledTests(boolean beforeUpload) {
250        Collection<Test> enabledTests = getTests();
251        for (Test t : new ArrayList<Test>(enabledTests)) {
252            if (beforeUpload ? t.testBeforeUpload : t.enabled) {
253                continue;
254            }
255            enabledTests.remove(t);
256        }
257        return enabledTests;
258    }
259
260    /**
261     * Gets the list of all available test classes
262     *
263     * @return An array of the test classes
264     */
265    public static Class<Test>[] getAllAvailableTests() {
266        return Arrays.copyOf(allAvailableTests, allAvailableTests.length);
267    }
268
269    /**
270     * Initialize grid details based on current projection system. Values based on
271     * the original value fixed for EPSG:4326 (10000) using heuristics (that is, test&error
272     * until most bugs were discovered while keeping the processing time reasonable)
273     */
274    public void initializeGridDetail() {
275        String code = Main.getProjection().toCode();
276        if (Arrays.asList(ProjectionPreference.wgs84.allCodes()).contains(code)) {
277            OsmValidator.griddetail = 10000;
278        } else if (Arrays.asList(ProjectionPreference.mercator.allCodes()).contains(code)) {
279            OsmValidator.griddetail = 0.01;
280        } else if (Arrays.asList(ProjectionPreference.lambert.allCodes()).contains(code)) {
281            OsmValidator.griddetail = 0.1;
282        } else {
283            OsmValidator.griddetail = 1.0;
284        }
285    }
286
287    /**
288     * Initializes all tests
289     * @param allTests The tests to initialize
290     */
291    public static void initializeTests(Collection<Test> allTests) {
292        for (Test test : allTests) {
293            try {
294                if (test.enabled) {
295                    test.initialize();
296                }
297            } catch (Exception e) {
298                e.printStackTrace();
299                JOptionPane.showMessageDialog(Main.parent,
300                        tr("Error initializing test {0}:\n {1}", test.getClass()
301                                .getSimpleName(), e),
302                                tr("Error"),
303                                JOptionPane.ERROR_MESSAGE);
304            }
305        }
306    }
307
308    /* -------------------------------------------------------------------------- */
309    /* interface LayerChangeListener                                              */
310    /* -------------------------------------------------------------------------- */
311    @Override
312    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
313    }
314
315    @Override
316    public void layerAdded(Layer newLayer) {
317    }
318
319    @Override
320    public void layerRemoved(Layer oldLayer) {
321        if (oldLayer == errorLayer) {
322            errorLayer = null;
323            return;
324        }
325        if (Main.map.mapView.getLayersOfType(OsmDataLayer.class).isEmpty()) {
326            if (errorLayer != null) {
327                Main.main.removeLayer(errorLayer);
328            }
329        }
330    }
331}