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