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