001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.File;
007import java.io.FileNotFoundException;
008import java.io.FileOutputStream;
009import java.io.IOException;
010import java.io.OutputStreamWriter;
011import java.io.PrintWriter;
012import java.nio.charset.StandardCharsets;
013import java.nio.file.Files;
014import java.nio.file.Path;
015import java.nio.file.Paths;
016import java.util.ArrayList;
017import java.util.Arrays;
018import java.util.Collection;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.SortedMap;
023import java.util.TreeMap;
024import java.util.TreeSet;
025
026import javax.swing.JOptionPane;
027
028import org.openstreetmap.josm.Main;
029import org.openstreetmap.josm.actions.ValidateAction;
030import org.openstreetmap.josm.data.validation.tests.Addresses;
031import org.openstreetmap.josm.data.validation.tests.ApiCapabilitiesTest;
032import org.openstreetmap.josm.data.validation.tests.BarriersEntrances;
033import org.openstreetmap.josm.data.validation.tests.Coastlines;
034import org.openstreetmap.josm.data.validation.tests.ConditionalKeys;
035import org.openstreetmap.josm.data.validation.tests.CrossingWays;
036import org.openstreetmap.josm.data.validation.tests.DuplicateNode;
037import org.openstreetmap.josm.data.validation.tests.DuplicateRelation;
038import org.openstreetmap.josm.data.validation.tests.DuplicateWay;
039import org.openstreetmap.josm.data.validation.tests.DuplicatedWayNodes;
040import org.openstreetmap.josm.data.validation.tests.Highways;
041import org.openstreetmap.josm.data.validation.tests.InternetTags;
042import org.openstreetmap.josm.data.validation.tests.Lanes;
043import org.openstreetmap.josm.data.validation.tests.LongSegment;
044import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker;
045import org.openstreetmap.josm.data.validation.tests.MultipolygonTest;
046import org.openstreetmap.josm.data.validation.tests.NameMismatch;
047import org.openstreetmap.josm.data.validation.tests.OpeningHourTest;
048import org.openstreetmap.josm.data.validation.tests.OverlappingWays;
049import org.openstreetmap.josm.data.validation.tests.PowerLines;
050import org.openstreetmap.josm.data.validation.tests.PublicTransportRouteTest;
051import org.openstreetmap.josm.data.validation.tests.RelationChecker;
052import org.openstreetmap.josm.data.validation.tests.SelfIntersectingWay;
053import org.openstreetmap.josm.data.validation.tests.SimilarNamedWays;
054import org.openstreetmap.josm.data.validation.tests.TagChecker;
055import org.openstreetmap.josm.data.validation.tests.TurnrestrictionTest;
056import org.openstreetmap.josm.data.validation.tests.UnclosedWays;
057import org.openstreetmap.josm.data.validation.tests.UnconnectedWays;
058import org.openstreetmap.josm.data.validation.tests.UntaggedNode;
059import org.openstreetmap.josm.data.validation.tests.UntaggedWay;
060import org.openstreetmap.josm.data.validation.tests.WayConnectedToArea;
061import org.openstreetmap.josm.data.validation.tests.WronglyOrderedWays;
062import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
063import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
064import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
065import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
066import org.openstreetmap.josm.gui.layer.OsmDataLayer;
067import org.openstreetmap.josm.gui.layer.ValidatorLayer;
068import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
069import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
070import org.openstreetmap.josm.tools.Utils;
071
072/**
073 * A OSM data validator.
074 *
075 * @author Francisco R. Santos <frsantos@gmail.com>
076 */
077public class OsmValidator implements LayerChangeListener {
078
079    public static volatile ValidatorLayer errorLayer;
080
081    /** The validate action */
082    public ValidateAction validateAction = new ValidateAction();
083
084    /** Grid detail, multiplier of east,north values for valuable cell sizing */
085    public static double griddetail;
086
087    private static final Collection<String> ignoredErrors = new TreeSet<>();
088
089    /**
090     * All registered tests
091     */
092    private static final Collection<Class<? extends Test>> allTests = new ArrayList<>();
093    private static final Map<String, Test> allTestsMap = new HashMap<>();
094
095    /**
096     * All available tests in core
097     */
098    @SuppressWarnings("unchecked")
099    private static final Class<Test>[] CORE_TEST_CLASSES = new Class[] {
100        /* FIXME - unique error numbers for tests aren't properly unique - ignoring will not work as expected */
101        DuplicateNode.class, // ID    1 ..   99
102        OverlappingWays.class, // ID  101 ..  199
103        UntaggedNode.class, // ID  201 ..  299
104        UntaggedWay.class, // ID  301 ..  399
105        SelfIntersectingWay.class, // ID  401 ..  499
106        DuplicatedWayNodes.class, // ID  501 ..  599
107        CrossingWays.Ways.class, // ID  601 ..  699
108        CrossingWays.Boundaries.class, // ID  601 ..  699
109        CrossingWays.Barrier.class, // ID  601 ..  699
110        SimilarNamedWays.class, // ID  701 ..  799
111        Coastlines.class, // ID  901 ..  999
112        WronglyOrderedWays.class, // ID 1001 .. 1099
113        UnclosedWays.class, // ID 1101 .. 1199
114        TagChecker.class, // ID 1201 .. 1299
115        UnconnectedWays.UnconnectedHighways.class, // ID 1301 .. 1399
116        UnconnectedWays.UnconnectedRailways.class, // ID 1301 .. 1399
117        UnconnectedWays.UnconnectedWaterways.class, // ID 1301 .. 1399
118        UnconnectedWays.UnconnectedNaturalOrLanduse.class, // ID 1301 .. 1399
119        UnconnectedWays.UnconnectedPower.class, // ID 1301 .. 1399
120        DuplicateWay.class, // ID 1401 .. 1499
121        NameMismatch.class, // ID  1501 ..  1599
122        MultipolygonTest.class, // ID  1601 ..  1699
123        RelationChecker.class, // ID  1701 ..  1799
124        TurnrestrictionTest.class, // ID  1801 ..  1899
125        DuplicateRelation.class, // ID 1901 .. 1999
126        WayConnectedToArea.class, // ID 2301 .. 2399
127        PowerLines.class, // ID 2501 .. 2599
128        Addresses.class, // ID 2601 .. 2699
129        Highways.class, // ID 2701 .. 2799
130        BarriersEntrances.class, // ID 2801 .. 2899
131        OpeningHourTest.class, // 2901 .. 2999
132        MapCSSTagChecker.class, // 3000 .. 3099
133        Lanes.class, // 3100 .. 3199
134        ConditionalKeys.class, // 3200 .. 3299
135        InternetTags.class, // 3300 .. 3399
136        ApiCapabilitiesTest.class, // 3400 .. 3499
137        LongSegment.class, // 3500 .. 3599
138        PublicTransportRouteTest.class, // 3600 .. 3699
139    };
140
141    public static void addTest(Class<? extends Test> testClass) {
142        allTests.add(testClass);
143        try {
144            allTestsMap.put(testClass.getName(), testClass.getConstructor().newInstance());
145        } catch (ReflectiveOperationException e) {
146            Main.error(e);
147        }
148    }
149
150    static {
151        for (Class<? extends Test> testClass : CORE_TEST_CLASSES) {
152            addTest(testClass);
153        }
154    }
155
156    /**
157     * Constructs a new {@code OsmValidator}.
158     */
159    public OsmValidator() {
160        checkValidatorDir();
161        initializeGridDetail();
162        loadIgnoredErrors(); //FIXME: load only when needed
163    }
164
165    /**
166     * Returns the validator directory.
167     *
168     * @return The validator directory
169     */
170    public static String getValidatorDir() {
171        return new File(Main.pref.getUserDataDirectory(), "validator").getAbsolutePath();
172    }
173
174    /**
175     * Check if validator directory exists (store ignored errors file)
176     */
177    private static void checkValidatorDir() {
178        File pathDir = new File(getValidatorDir());
179        if (!pathDir.exists()) {
180            Utils.mkDirs(pathDir);
181        }
182    }
183
184    private static void loadIgnoredErrors() {
185        ignoredErrors.clear();
186        if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
187            Path path = Paths.get(getValidatorDir()).resolve("ignorederrors");
188            if (Files.exists(path)) {
189                try {
190                    ignoredErrors.addAll(Files.readAllLines(path, StandardCharsets.UTF_8));
191                } catch (final FileNotFoundException e) {
192                    Main.debug(Main.getErrorMessage(e));
193                } catch (final IOException e) {
194                    Main.error(e);
195                }
196            }
197        }
198    }
199
200    public static void addIgnoredError(String s) {
201        ignoredErrors.add(s);
202    }
203
204    public static boolean hasIgnoredError(String s) {
205        return ignoredErrors.contains(s);
206    }
207
208    public static void saveIgnoredErrors() {
209        try (PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(
210                new File(getValidatorDir(), "ignorederrors")), StandardCharsets.UTF_8), false)) {
211            for (String e : ignoredErrors) {
212                out.println(e);
213            }
214        } catch (IOException e) {
215            Main.error(e);
216        }
217    }
218
219    public static synchronized void initializeErrorLayer() {
220        if (!Main.pref.getBoolean(ValidatorPreference.PREF_LAYER, true))
221            return;
222        if (errorLayer == null) {
223            errorLayer = new ValidatorLayer();
224            Main.main.addLayer(errorLayer);
225        }
226    }
227
228    /**
229     * Gets a map from simple names to all tests.
230     * @return A map of all tests, indexed and sorted by the name of their Java class
231     */
232    public static SortedMap<String, Test> getAllTestsMap() {
233        applyPrefs(allTestsMap, false);
234        applyPrefs(allTestsMap, true);
235        return new TreeMap<>(allTestsMap);
236    }
237
238    /**
239     * Returns the instance of the given test class.
240     * @param <T> testClass type
241     * @param testClass The class of test to retrieve
242     * @return the instance of the given test class, if any, or {@code null}
243     * @since 6670
244     */
245    @SuppressWarnings("unchecked")
246    public static <T extends Test> T getTest(Class<T> testClass) {
247        if (testClass == null) {
248            return null;
249        }
250        return (T) allTestsMap.get(testClass.getName());
251    }
252
253    private static void applyPrefs(Map<String, Test> tests, boolean beforeUpload) {
254        for (String testName : Main.pref.getCollection(beforeUpload
255        ? ValidatorPreference.PREF_SKIP_TESTS_BEFORE_UPLOAD : ValidatorPreference.PREF_SKIP_TESTS)) {
256            Test test = tests.get(testName);
257            if (test != null) {
258                if (beforeUpload) {
259                    test.testBeforeUpload = false;
260                } else {
261                    test.enabled = false;
262                }
263            }
264        }
265    }
266
267    public static Collection<Test> getTests() {
268        return getAllTestsMap().values();
269    }
270
271    public static Collection<Test> getEnabledTests(boolean beforeUpload) {
272        Collection<Test> enabledTests = getTests();
273        for (Test t : new ArrayList<>(enabledTests)) {
274            if (beforeUpload ? t.testBeforeUpload : t.enabled) {
275                continue;
276            }
277            enabledTests.remove(t);
278        }
279        return enabledTests;
280    }
281
282    /**
283     * Gets the list of all available test classes
284     *
285     * @return A collection of the test classes
286     */
287    public static Collection<Class<? extends Test>> getAllAvailableTestClasses() {
288        return Collections.unmodifiableCollection(allTests);
289    }
290
291    /**
292     * Initialize grid details based on current projection system. Values based on
293     * the original value fixed for EPSG:4326 (10000) using heuristics (that is, test&amp;error
294     * until most bugs were discovered while keeping the processing time reasonable)
295     */
296    public static final void initializeGridDetail() {
297        String code = Main.getProjection().toCode();
298        if (Arrays.asList(ProjectionPreference.wgs84.allCodes()).contains(code)) {
299            OsmValidator.griddetail = 10000;
300        } else if (Arrays.asList(ProjectionPreference.mercator.allCodes()).contains(code)) {
301            OsmValidator.griddetail = 0.01;
302        } else if (Arrays.asList(ProjectionPreference.lambert.allCodes()).contains(code)) {
303            OsmValidator.griddetail = 0.1;
304        } else {
305            OsmValidator.griddetail = 1.0;
306        }
307    }
308
309    private static boolean testsInitialized;
310
311    /**
312     * Initializes all tests if this operations hasn't been performed already.
313     */
314    public static synchronized void initializeTests() {
315        if (!testsInitialized) {
316            Main.debug("Initializing validator tests");
317            final long startTime = System.currentTimeMillis();
318            initializeTests(getTests());
319            testsInitialized = true;
320            if (Main.isDebugEnabled()) {
321                final long elapsedTime = System.currentTimeMillis() - startTime;
322                Main.debug("Initializing validator tests completed in " + Utils.getDurationString(elapsedTime));
323            }
324        }
325    }
326
327    /**
328     * Initializes all tests
329     * @param allTests The tests to initialize
330     */
331    public static void initializeTests(Collection<? extends Test> allTests) {
332        for (Test test : allTests) {
333            try {
334                if (test.enabled) {
335                    test.initialize();
336                }
337            } catch (Exception e) {
338                Main.error(e);
339                JOptionPane.showMessageDialog(Main.parent,
340                        tr("Error initializing test {0}:\n {1}", test.getClass()
341                                .getSimpleName(), e),
342                                tr("Error"),
343                                JOptionPane.ERROR_MESSAGE);
344            }
345        }
346    }
347
348    /* -------------------------------------------------------------------------- */
349    /* interface LayerChangeListener                                              */
350    /* -------------------------------------------------------------------------- */
351    @Override
352    public void layerAdded(LayerAddEvent e) {
353        // do nothing
354    }
355
356    @Override
357    public void layerOrderChanged(LayerOrderChangeEvent e) {
358        // do nothing
359    }
360
361    @Override
362    public void layerRemoving(LayerRemoveEvent e) {
363        if (e.getRemovedLayer() == errorLayer) {
364            errorLayer = null;
365            return;
366        }
367        if (e.getSource().getLayersOfType(OsmDataLayer.class).isEmpty()) {
368            if (errorLayer != null) {
369                Main.main.removeLayer(errorLayer);
370            }
371        }
372    }
373}