// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.validation; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.GraphicsEnvironment; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.util.TreeSet; import javax.swing.JOptionPane; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.ValidateAction; import org.openstreetmap.josm.data.validation.tests.Addresses; import org.openstreetmap.josm.data.validation.tests.ApiCapabilitiesTest; import org.openstreetmap.josm.data.validation.tests.BarriersEntrances; import org.openstreetmap.josm.data.validation.tests.Coastlines; import org.openstreetmap.josm.data.validation.tests.ConditionalKeys; import org.openstreetmap.josm.data.validation.tests.CrossingWays; import org.openstreetmap.josm.data.validation.tests.DuplicateNode; import org.openstreetmap.josm.data.validation.tests.DuplicateRelation; import org.openstreetmap.josm.data.validation.tests.DuplicateWay; import org.openstreetmap.josm.data.validation.tests.DuplicatedWayNodes; import org.openstreetmap.josm.data.validation.tests.Highways; import org.openstreetmap.josm.data.validation.tests.InternetTags; import org.openstreetmap.josm.data.validation.tests.Lanes; import org.openstreetmap.josm.data.validation.tests.LongSegment; import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker; import org.openstreetmap.josm.data.validation.tests.MultipolygonTest; import org.openstreetmap.josm.data.validation.tests.NameMismatch; import org.openstreetmap.josm.data.validation.tests.OpeningHourTest; import org.openstreetmap.josm.data.validation.tests.OverlappingWays; import org.openstreetmap.josm.data.validation.tests.PowerLines; import org.openstreetmap.josm.data.validation.tests.PublicTransportRouteTest; import org.openstreetmap.josm.data.validation.tests.RelationChecker; import org.openstreetmap.josm.data.validation.tests.SelfIntersectingWay; import org.openstreetmap.josm.data.validation.tests.SimilarNamedWays; import org.openstreetmap.josm.data.validation.tests.TagChecker; import org.openstreetmap.josm.data.validation.tests.TurnrestrictionTest; import org.openstreetmap.josm.data.validation.tests.UnclosedWays; import org.openstreetmap.josm.data.validation.tests.UnconnectedWays; import org.openstreetmap.josm.data.validation.tests.UntaggedNode; import org.openstreetmap.josm.data.validation.tests.UntaggedWay; import org.openstreetmap.josm.data.validation.tests.WayConnectedToArea; import org.openstreetmap.josm.data.validation.tests.WronglyOrderedWays; import org.openstreetmap.josm.gui.layer.ValidatorLayer; import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference; import org.openstreetmap.josm.tools.Utils; /** * A OSM data validator. * * @author Francisco R. Santos <frsantos@gmail.com> */ public final class OsmValidator { private OsmValidator() { // Hide default constructor for utilities classes } private static volatile ValidatorLayer errorLayer; /** The validate action */ public static final ValidateAction validateAction = new ValidateAction(); /** Grid detail, multiplier of east,north values for valuable cell sizing */ private static double griddetail; private static final Collection<String> ignoredErrors = new TreeSet<>(); /** * All registered tests */ private static final Collection<Class<? extends Test>> allTests = new ArrayList<>(); private static final Map<String, Test> allTestsMap = new HashMap<>(); /** * All available tests in core */ @SuppressWarnings("unchecked") private static final Class<Test>[] CORE_TEST_CLASSES = new Class[] { /* FIXME - unique error numbers for tests aren't properly unique - ignoring will not work as expected */ DuplicateNode.class, // ID 1 .. 99 OverlappingWays.class, // ID 101 .. 199 UntaggedNode.class, // ID 201 .. 299 UntaggedWay.class, // ID 301 .. 399 SelfIntersectingWay.class, // ID 401 .. 499 DuplicatedWayNodes.class, // ID 501 .. 599 CrossingWays.Ways.class, // ID 601 .. 699 CrossingWays.Boundaries.class, // ID 601 .. 699 CrossingWays.Barrier.class, // ID 601 .. 699 CrossingWays.SelfCrossing.class, // ID 601 .. 699 SimilarNamedWays.class, // ID 701 .. 799 Coastlines.class, // ID 901 .. 999 WronglyOrderedWays.class, // ID 1001 .. 1099 UnclosedWays.class, // ID 1101 .. 1199 TagChecker.class, // ID 1201 .. 1299 UnconnectedWays.UnconnectedHighways.class, // ID 1301 .. 1399 UnconnectedWays.UnconnectedRailways.class, // ID 1301 .. 1399 UnconnectedWays.UnconnectedWaterways.class, // ID 1301 .. 1399 UnconnectedWays.UnconnectedNaturalOrLanduse.class, // ID 1301 .. 1399 UnconnectedWays.UnconnectedPower.class, // ID 1301 .. 1399 DuplicateWay.class, // ID 1401 .. 1499 NameMismatch.class, // ID 1501 .. 1599 MultipolygonTest.class, // ID 1601 .. 1699 RelationChecker.class, // ID 1701 .. 1799 TurnrestrictionTest.class, // ID 1801 .. 1899 DuplicateRelation.class, // ID 1901 .. 1999 WayConnectedToArea.class, // ID 2301 .. 2399 PowerLines.class, // ID 2501 .. 2599 Addresses.class, // ID 2601 .. 2699 Highways.class, // ID 2701 .. 2799 BarriersEntrances.class, // ID 2801 .. 2899 OpeningHourTest.class, // 2901 .. 2999 MapCSSTagChecker.class, // 3000 .. 3099 Lanes.class, // 3100 .. 3199 ConditionalKeys.class, // 3200 .. 3299 InternetTags.class, // 3300 .. 3399 ApiCapabilitiesTest.class, // 3400 .. 3499 LongSegment.class, // 3500 .. 3599 PublicTransportRouteTest.class, // 3600 .. 3699 }; public static void addTest(Class<? extends Test> testClass) { allTests.add(testClass); try { allTestsMap.put(testClass.getName(), testClass.getConstructor().newInstance()); } catch (ReflectiveOperationException e) { Main.error(e); } } static { for (Class<? extends Test> testClass : CORE_TEST_CLASSES) { addTest(testClass); } } /** * Initializes {@code OsmValidator}. */ public static void initialize() { checkValidatorDir(); initializeGridDetail(); loadIgnoredErrors(); //FIXME: load only when needed } /** * Returns the validator directory. * * @return The validator directory */ public static String getValidatorDir() { return new File(Main.pref.getUserDataDirectory(), "validator").getAbsolutePath(); } /** * Check if validator directory exists (store ignored errors file) */ private static void checkValidatorDir() { File pathDir = new File(getValidatorDir()); if (!pathDir.exists()) { Utils.mkDirs(pathDir); } } private static void loadIgnoredErrors() { ignoredErrors.clear(); if (ValidatorPreference.PREF_USE_IGNORE.get()) { Path path = Paths.get(getValidatorDir()).resolve("ignorederrors"); if (path.toFile().exists()) { try { ignoredErrors.addAll(Files.readAllLines(path, StandardCharsets.UTF_8)); } catch (final FileNotFoundException e) { Main.debug(Main.getErrorMessage(e)); } catch (final IOException e) { Main.error(e); } } } } public static void addIgnoredError(String s) { ignoredErrors.add(s); } public static boolean hasIgnoredError(String s) { return ignoredErrors.contains(s); } public static void saveIgnoredErrors() { try (PrintWriter out = new PrintWriter(new File(getValidatorDir(), "ignorederrors"), StandardCharsets.UTF_8.name())) { for (String e : ignoredErrors) { out.println(e); } } catch (IOException e) { Main.error(e); } } /** * Initializes error layer. */ public static synchronized void initializeErrorLayer() { if (!ValidatorPreference.PREF_LAYER.get()) return; if (errorLayer == null) { errorLayer = new ValidatorLayer(); Main.getLayerManager().addLayer(errorLayer); } } /** * Resets error layer. * @since 11852 */ public static synchronized void resetErrorLayer() { errorLayer = null; } /** * Gets a map from simple names to all tests. * @return A map of all tests, indexed and sorted by the name of their Java class */ public static SortedMap<String, Test> getAllTestsMap() { applyPrefs(allTestsMap, false); applyPrefs(allTestsMap, true); return new TreeMap<>(allTestsMap); } /** * Returns the instance of the given test class. * @param <T> testClass type * @param testClass The class of test to retrieve * @return the instance of the given test class, if any, or {@code null} * @since 6670 */ @SuppressWarnings("unchecked") public static <T extends Test> T getTest(Class<T> testClass) { if (testClass == null) { return null; } return (T) allTestsMap.get(testClass.getName()); } private static void applyPrefs(Map<String, Test> tests, boolean beforeUpload) { for (String testName : Main.pref.getCollection(beforeUpload ? ValidatorPreference.PREF_SKIP_TESTS_BEFORE_UPLOAD : ValidatorPreference.PREF_SKIP_TESTS)) { Test test = tests.get(testName); if (test != null) { if (beforeUpload) { test.testBeforeUpload = false; } else { test.enabled = false; } } } } public static Collection<Test> getTests() { return getAllTestsMap().values(); } public static Collection<Test> getEnabledTests(boolean beforeUpload) { Collection<Test> enabledTests = getTests(); for (Test t : new ArrayList<>(enabledTests)) { if (beforeUpload ? t.testBeforeUpload : t.enabled) { continue; } enabledTests.remove(t); } return enabledTests; } /** * Gets the list of all available test classes * * @return A collection of the test classes */ public static Collection<Class<? extends Test>> getAllAvailableTestClasses() { return Collections.unmodifiableCollection(allTests); } /** * Initialize grid details based on current projection system. Values based on * the original value fixed for EPSG:4326 (10000) using heuristics (that is, test&error * until most bugs were discovered while keeping the processing time reasonable) */ public static void initializeGridDetail() { String code = Main.getProjection().toCode(); if (Arrays.asList(ProjectionPreference.wgs84.allCodes()).contains(code)) { OsmValidator.griddetail = 10_000; } else if (Arrays.asList(ProjectionPreference.mercator.allCodes()).contains(code)) { OsmValidator.griddetail = 0.01; } else if (Arrays.asList(ProjectionPreference.lambert.allCodes()).contains(code)) { OsmValidator.griddetail = 0.1; } else { OsmValidator.griddetail = 1.0; } } /** * Returns grid detail, multiplier of east,north values for valuable cell sizing * @return grid detail * @since 11852 */ public static double getGridDetail() { return griddetail; } private static boolean testsInitialized; /** * Initializes all tests if this operations hasn't been performed already. */ public static synchronized void initializeTests() { if (!testsInitialized) { Main.debug("Initializing validator tests"); final long startTime = System.currentTimeMillis(); initializeTests(getTests()); testsInitialized = true; if (Main.isDebugEnabled()) { final long elapsedTime = System.currentTimeMillis() - startTime; Main.debug("Initializing validator tests completed in " + Utils.getDurationString(elapsedTime)); } } } /** * Initializes all tests * @param allTests The tests to initialize */ public static void initializeTests(Collection<? extends Test> allTests) { for (Test test : allTests) { try { if (test.enabled) { test.initialize(); } } catch (Exception e) { // NOPMD Main.error(e); if (!GraphicsEnvironment.isHeadless()) { JOptionPane.showMessageDialog(Main.parent, tr("Error initializing test {0}:\n {1}", test.getClass().getSimpleName(), e), tr("Error"), JOptionPane.ERROR_MESSAGE); } } } } }