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