From c3d65d39d9e677e100891e95d4e2714a257e5de1 Mon Sep 17 00:00:00 2001 From: Michael Cuthbert Date: Tue, 13 Feb 2018 13:55:02 -0800 Subject: [PATCH] Adding new SignPostCheck and IntersectingBuildingsCheck. (#19) * Adding new SignPostCheck and IntersectingBuildingsCheck. Also updating MapRoulette functions to allow the use of tags when creating challenges. Updating documents to describe how to develop and debug Atlas-Checks. * Updates to SignPostCheck * Updates to debugging and unit test documentation * Removing git merge artifacts --- config/configuration.json | 38 +- dependencies.gradle | 4 +- docs/checks/sharpAngleCheck.md | 4 +- docs/debugging.md | 72 +++ docs/dev.md | 5 + docs/unit_tests.md | 98 ++++ .../atlas/checks/base/BaseCheck.java | 24 +- .../maproulette/MapRouletteCommand.java | 4 +- .../checks/maproulette/data/Challenge.java | 14 +- .../atlas/checks/maproulette/data/Survey.java | 4 +- .../serializer/ChallengeDeserializer.java | 3 +- .../checks/persistence/SparkFileHelper.java | 5 +- .../IntersectingBuildingsCheck.java | 254 ++++++++++ .../linear/edges/SignPostCheck.java | 143 ++++++ src/main/resources/log4j.properties | 1 + .../BuildingRoadIntersectionCheckTest.java | 5 +- .../BuildingRoadIntersectionTestCaseRule.java | 7 +- .../IntersectingBuildingsCheckTest.java | 119 +++++ .../IntersectingBuildingsTestCaseRule.java | 114 +++++ .../linear/edges/SignPostCheckTest.java | 196 ++++++++ .../linear/edges/SignPostCheckTestRule.java | 444 ++++++++++++++++++ 21 files changed, 1518 insertions(+), 40 deletions(-) create mode 100644 docs/debugging.md create mode 100644 docs/unit_tests.md create mode 100644 src/main/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsCheck.java create mode 100644 src/main/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheck.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsCheckTest.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsTestCaseRule.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheckTest.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheckTestRule.java diff --git a/config/configuration.json b/config/configuration.json index ce17e7820..22cd41e6d 100644 --- a/config/configuration.json +++ b/config/configuration.json @@ -29,7 +29,8 @@ "mediumPriorityRule": { "condition":"OR", "rules":["highway=primary","highway=primary_link","highway=secondary","highway=secondary_link"] - } + }, + "tags":"building,highway" } }, "DuplicateNodeCheck": { @@ -66,7 +67,8 @@ "mediumPriorityRule": { "condition":"OR", "rules":["highway=primary","highway=primary_link","highway=secondary","highway=secondary_link"] - } + }, + "tags":"highway" } }, "InvalidTurnRestrictionCheck": { @@ -74,7 +76,8 @@ "description": "Tasks containing invalid turn restrictions", "blurb": "Invalid Turn Restrictions", "instruction": "Correct the displayed invalid turn restriction", - "difficulty": "HARD" + "difficulty": "HARD", + "tags":"highway" } }, "RoundaboutClosedLoopCheck": { @@ -91,7 +94,8 @@ "mediumPriorityRule": { "condition":"OR", "rules":["highway=primary","highway=primary_link","highway=secondary","highway=secondary_link"] - } + }, + "tags":"highway" } }, "SelfIntersectingPolylineCheck": { @@ -109,7 +113,8 @@ "mediumPriorityRule": { "condition":"OR", "rules":["highway=primary","highway=primary_link","highway=secondary","highway=secondary_link"] - } + }, + "tags":"highway" } }, "SharpAngleCheck": { @@ -127,7 +132,8 @@ "mediumPriorityRule": { "condition":"OR", "rules":["highway=primary","highway=primary_link","highway=secondary","highway=secondary_link"] - } + }, + "tags":"highway" } }, "SinkIslandCheck": { @@ -145,7 +151,8 @@ "mediumPriorityRule": { "condition":"OR", "rules":["highway=primary","highway=primary_link","highway=secondary","highway=secondary_link"] - } + }, + "tags":"highway" } }, "SnakeRoadCheck": { @@ -162,7 +169,22 @@ "mediumPriorityRule": { "condition":"OR", "rules":["highway=primary","highway=primary_link","highway=secondary","highway=secondary_link"] - } + }, + "tags":"highway" } + }, + "SignPostCheck": { + "enabled": false, + "linkLength": { + "minimum.meters": 50.0 + }, + "challenge": { + "description": "Tasks contain nodes where sign post tagging could be missing. In particular it looks for motorway and trunk ways which have a link edge exiting them. A task is generated if either the connecting node is missing the motorway_junction tag or the exiting segment is missing the destination tag.", + "blurb": "Missing sign post tags", + "instruction": "Either add the missing motorway_junction tag to the identified node and / or the destination tag to the exiting link segment.", + "difficulty": "NORMAL", + "defaultPriority": "MEDIUM" + }, + "tags":"highway" } } diff --git a/dependencies.gradle b/dependencies.gradle index a76408e99..400b4971b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,8 +1,8 @@ project.ext.versions = [ checkstyle: '7.6.1', - atlas: '5.0.3', + atlas: '5.0.6', commons:'2.6', - atlas_generator: '4.0.0', + atlas_generator: '4.0.1', ] project.ext.packages = [ diff --git a/docs/checks/sharpAngleCheck.md b/docs/checks/sharpAngleCheck.md index e041b0d06..639fe742e 100644 --- a/docs/checks/sharpAngleCheck.md +++ b/docs/checks/sharpAngleCheck.md @@ -2,7 +2,9 @@ #### Description -Flags edges that have an angle that is too sharp within their polyline. Sharp angles may indicate inaccurate digitization once this threshold is exceeded. There may be other factors in play here, such as numbers of intersections, type of highway, etc. But the main breaking point is any angles that are less than 31 degrees. +Flags edges that have an angle that is too sharp within their polyline. Sharp angles may indicate inaccurate +digitization once this threshold is exceeded. There may be other factors in play here, such as numbers of +intersections, type of highway, etc. But the main breaking point is any angles that are less than 31 degrees. #### Code Review diff --git a/docs/debugging.md b/docs/debugging.md new file mode 100644 index 000000000..e9da81372 --- /dev/null +++ b/docs/debugging.md @@ -0,0 +1,72 @@ +# Debugging Atlas Checks + +### Overview + +When developing an Atlas Checks, it is very important to be able to debug your checks to understand what they are doing or potentially where your check is not doing what you would expect it to do. The document describes how you would debug Atlas Checks using the Intellij IDE by JetBrains. There are various IDE's out there that perform similar debugging functions, namely an IDE like Eclipse, and transferring the instructions from Intellij to your IDE of choice should be fairly straight forward. This document should give you the basics for whatever IDE you wish to use. + +### Setup + +The first thing required is to import the project into Intellij. Intellij supports gradle projects which is what Atlas-Checks is, so importing the project is fairly straight forward. These instructions assumed + +1. Clone the Atlas-Checks project into a directory of your choice +2. Click on "File" -> "New" -> "Project from Existing Sources..." +3. Select the folder that you original cloned your Atlas-Checks project into in Step 1. +4. Select the "Import project from external model" and then select the "Gradle" option +5. In the final screen check the box "Use auto-import" and make sure that "Use default gradle wrapper (recommended)" is selected. +6. Click Finish and wait for gradle tasks to complete. + +This will create a new Atlas-Checks project that you can start developing in Intellij. Most other full fledged IDEs should following a similar path. Smaller lightweight IDEs will simply have you point to a directory. + +### Creating Configuration + +In Intellij, a configuration is used to run a project. With the setup above you will be able to build the project directly from within IDE but we need to setup a configuration to actually run and debug it. We won't be running it as mentioned in other documents using the gradle task `gradle run`, we will be running it directly against the main class. Essentially diving into what `gradle run` hides from the user. + +1. Click on the drop down bar in the toolbar, it should currently be empty. If you are not sure where this is you can also go to File Menu and click on "run" and then select the "run" option in the drop down. + + - If you click on the drop down bar, it will drop down an "edit configurations" option that you must click. + - If you went through the file menu, then a small dialog will pop up, click on the "edit configurations" option. + +2. In the "Run/Debug Configurations" dialog box click on the + sign in the top left hand corner of the dialog. +3. Select the "Application" option +4. In the pane on the right that now contains the new Application update the following. + + - Change the name to something like 'Atlas-Checks' + - Change the main class to `org.openstreetmap.atlas.checks.distributed.IntegrityCheckSparkJob` + - Optionally include VM Options: -Xms2048m -Xmx10240m -XX:MaxPermSize=4096m, this will help with larger atlas files. + - Update the working directory to be the current directory of your Atlas-Checks project. + - Set "Use of classpath module" to `atlas-checks_main` + - Include the following parameter template in "Program Arguments". Replace explanation with actual value. + + - inputFolder=[Folder pointing to location of country folders with atlas files] + - startedFolder=[Output directory for Spark, can be any directory you create] + - output=[Output directory for results of job] + - countries=[ISO3 country code comma separated list] + - maproulette=[Maproulette configuration in format "server:host:project:api_key"] + - saveCheckOutput=false + - master=local + - configFiles=[Points to the configuration file, should be something like file:/atlas_checks_root_dir/config/configuration.json] + - sparkOptions=spark.executor.memory->4g,spark.driver.memory->4g,spark.rdd.compress->true + +A couple of notes about the program arguments. + +1. SparkOptions should generally not be changed, but if you require more memory for either driver or workers then you can update it. For more information about Spark options see [here](http://spark.apache.org/docs/1.6.0/configuration.html). +2. This can be used to deploy your jobs to a Spark cluster, that information you can find [here](cluster.md) +3. The `gradle run` task hides the structure of the atlas files, however in the background the files are separated by country into ISO3 country folders. So unlike using the gradle task you need to have the actual atlas files and place them in the correct folder structure for this to work, an example folder structure would be like below: + +``` + - Root Folder + - ABC + - ABC_7-41-57.atlas + - XYZ + - XYZ_10-806-508.atlas + - XYZ_11-1614-1016.atlas + - XYZ_11-1614-1017.atlas + - XYZ_11-1615-1016.atlas + - XYZ_7-101-63.atlas + - XYZ_8-201-126.atlas +``` +Where ABC and XYZ are two different countries. + +### Running/Debugging Atlas Checks + +At this point running Atlas-Checks is as simple as either clicking on the play button on the taskbar or clicking on "Run" -> "Run..." in the file menu. Similarly with debugging you can either click on the little bug icon (usually next to the play button) on the taskbar or clicking "Run" -> "Debug..." in the file menu. The code will run and the code will break at any breakpoints that you place in the code. diff --git a/docs/dev.md b/docs/dev.md index 9cb2b91dd..53024bae3 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -239,3 +239,8 @@ The properties of each flag will contain the following items: - [InvalidTurnRestrictionCheck](checks/invalidTurnRestrictionCheck.md) ** For Best Practices around writing Atlas Checks, please view our [best practices document](bestpractices.md). ** + +### Debugging and Unit Tests + +For information on debugging Atlas Checks please see [Debugging Altas Checks](debugging.md) +For information around writing unit tests for Atlas Checks see [Writing Unit Tests](unit_tests.md) diff --git a/docs/unit_tests.md b/docs/unit_tests.md new file mode 100644 index 000000000..070adbd1f --- /dev/null +++ b/docs/unit_tests.md @@ -0,0 +1,98 @@ +# Writing Unit Tests for Altas Checks + +It is important to make sure your code works as expected as well as making sure that any changes you make don't break existing changes. A good approach to developing integrity checks is using a [Test Driven Development](https://en.wikipedia.org/wiki/Test-driven_development) based approach. In a nutshell design your unit tests first and then build your check making sure that the tests that you originally built succeeds. + +### Example Unit Test + +For real examples you can look a couple that have already been built for the currently implemented checks: + +- [SinkIslandCheckTest](../src/test/java/org/openstreetmap/atlas/checks/validation/linear/edges/SinkIslandCheckTest.java) and [SinkIslandCheckTestRule](../src/test/java/org/openstreetmap/atlas/checks/validation/linear/edges/SinkIslandCheckTestRule.java) +- [AbbreviatedNameCheckTest](../src/test/java/org/openstreetmap/atlas/checks/validation/tag/AbbreviatedNameCheckTest.java) and [AbbreviatedNameCheckTestRule](../src/test/java/org/openstreetmap/atlas/checks/validation/tag/AbbreviatedNameCheckTestRule.java) + +As you see in the above their are two files associated with the unit tests. The first file are the unit tests themselves, the second file is, for lack of a better term, test Atlas files. Below we will first describe the structure of a unit test to be used to validate your Atlas Check and then describe how the data file is built and how to generate test atlas'. + +### Our First Unit Test + +```java +public class MyUnitTest { + // For now we will assume that there is a class called MyUnitTestRule with a test atlas inside called "testAtlas" + @Rule + public MyUnitTestRule setup = new MyUnitTestRule(); + + @Rule + public ConsumerBasedExpectedCheckVerifier verifier = new ConsumerBasedExpectedCheckVerifier(); + + @Test + public void testMyUnitTest() + { + // MyCheck would be replaced with the name of the check that you are testing + // The configuration is optional, and only required if you have specific configuration for the check that you want to test or is required for the check itself + this.verifier.actual(this.setup.testAtlas(), + new MyCheck(ConfigurationResolver.inlineConfiguration("{\"key\":\"value\"}"))); + // There are various helper functions described below, this one simply checks if the result of the check on the test atlas produces at least 1 flag. + this.verifier.verifyNotEmpty(); + + // Another common scenario would be to loop through the results and check something on it. + this.verifier.verify(flag -> { + // Check the flag object for some expected values + }); + } +} +``` + +A basic Atlas-Checks unit test contains 3 things: +1. A TestRule class that is essentially an atlas or multiple atlas to test against. +2. A `ConsumerBasedExpectedCheckVerifier` instance that will execute the check you are testing against the test atlas. +3. A series of unit tests using the JUnit framework. + +### The `ConsumerBasedExpectedCheckVerifier` + +The ConsumerBasedExpectedCheckVerifier is a class that will run your check against a test atlas. The test atlas are often very small atlas objects that contain specific data to test against. This verifier has too main types of functions + +1. `actual(Atlas, BaseCheck)` - The actual method establishes what data to run against (ie. what atlas file) and what check to run against the provided test atlas. +2. verify functions - The verify functions will be used to verify the results of the running the check over the test atlas file. Below are some useful functions + + - `verifyNotEmpty()` - Verifies that at least 1 flag was produced by the check + - `verifyEmpty()` - Verifies that no flags were produced by the check + - `verifyExpectedSize(int)` - Verifies that a specific number of flags were produced by the check + - `verify(Consumer>)` - A custom verifier allowing the developer to loop through each individual flag that is produced and verify it in any way that the developer wishes. + + ### The `TestRule` + + As mentioned previously the TestRule is a class that is associated with the Unit test class and contains all the test data, which would essentially be an Atlas in some form or another. An Atlas can be created in 2 primary ways: + + - Inline - An inline atlas uses tags to build it within the code. The code below will create an inline atlas that contains 3 nodes and 2 edges. You can also use the inline @tags to build an Atlas containing `lines`, `points` or `relations`. An example of this can be found in the [FloatingEdgeCheckTestRule](../src/test/java/org/openstreetmap/atlas/checks/validation/linear/edges/FloatingEdgeCheckTestRule.java) + ```java +@TestAtlas(nodes = { @Node(coordinates = @Loc(value = "37.32544,-122.033948")), + @Node(coordinates = @Loc(value = "37.33531,-122.009566")), + @Node(coordinates = @Loc(value = "37.3314171,-122.0304871")) }, edges = { + @Edge(coordinates = { @Loc(value = "37.32544,-122.033948"), @Loc(value = "37.33531,-122.009566") }, tags = { + "highway=SECONDARY" }), + @Edge(coordinates = { @Loc(value = "37.33531,-122.009566"), @Loc(value = "37.3314171,-122.0304871") }, tags = { + "highway=SECONDARY" }) }) +``` +- Atlas Text Files - It is fairly easy to produce very small test atlas text files which that can be stored in resources much like the [poolsize.altas](../src/test/resources/org/openstreetmap/atlas/checks/validation/areas/poolsize.atlas). And generating this files are fairly straight forward, and can be done by following these steps: +1. Generate a new Configuration in Intellij Idea IDE. (These general steps should work in any IDE, and the concept should be able to be transferred over to anything) +2. Choose Application and input the following parameters in the dialog to the right. + - Name: AtlasTestGenerator + - Main Class: org.openstreetmap.atlas.utilities.runtime.FlexibleCommand + - Program Arguments: atlas-with-this-entity
+ -input=`[Input location for atlas file]`
+ -osmid=`[OSM ID of primary feature]`
+ -expand=`[Amount you want to expand around feature]`
+ -text-output=`[Output location for resultant atlas text file]`
+ - Working directory: [Root directory of Atlas-Checks project] + - Use classpath of module: atlas-checks_main +3. Run new AtlasTestGenerator configuration in Intellij. + +This will produce a text atlas file in the location provided. You can then move the resultant file to the resources directory, much like the [poolsize.atlas](../src/test/resources/org/openstreetmap/atlas/checks/validation/areas/poolsize.atlas), Or you could set the output location to the resources directory to begin with. It is important to note that these files should be small as they would be checked into the repository as part of the code. There are options in the Atlas-Generator to zip up the results, but generally it is preferable to be able to see the contents of the atlas file. Once you have the text atlas file you can insert it into your test rule with the following code. +```java +@TestAtlas(loadFromTextResource = "test.atlas") +private Atlas testAtlas; + +public Atlas getTestAtlas() +{ + return this.testAtlas; +} +``` +The first line is the @tag that will load the text resource and instantiate the Atlas object below it. Then include a basic getter for the Atlas so that your unit test can use the test atlas. After completing all this you are ready to run your first unit test. diff --git a/src/main/java/org/openstreetmap/atlas/checks/base/BaseCheck.java b/src/main/java/org/openstreetmap/atlas/checks/base/BaseCheck.java index ebc06249e..2b9baa0d2 100644 --- a/src/main/java/org/openstreetmap/atlas/checks/base/BaseCheck.java +++ b/src/main/java/org/openstreetmap/atlas/checks/base/BaseCheck.java @@ -89,7 +89,7 @@ public BaseCheck(final Configuration configuration) if (challengeMap.isEmpty()) { this.challenge = new Challenge(this.getClass().getSimpleName(), "", "", "", - ChallengeDifficulty.EASY); + ChallengeDifficulty.EASY, ""); } else { @@ -341,22 +341,19 @@ protected String getTaskIdentifier(final Set objects) } /** - * Generates a unique identifier given an {@link AtlasObject}. OSM/Atlas objects with different - * types can share the identifier (way 12345 - node 12345). This method makes sure we generate a - * truly unique identifier, based on the OSM identifier, among different types for an - * {@link AtlasObject}. If the AtlasObject is an instanceof AtlasEntity then it will simply use - * the type for the first part of the identifier, otherwise it will use the simple class name. + * Similar to {@link BaseCheck#getUniqueOSMIdentifier(AtlasObject)} except instead of using the + * OSM identifier we use the Atlas identifier * * @param object * {@link AtlasObject} to generate unique identifier for * @return unique object identifier among different types */ - protected String getUniqueOSMIdentifier(final AtlasObject object) + protected String getUniqueObjectIdentifier(final AtlasObject object) { if (object instanceof AtlasEntity) { return String.format("%s%s", ((AtlasEntity) object).getType().toShortString(), - object.getOsmIdentifier()); + object.getIdentifier()); } else { @@ -365,19 +362,22 @@ protected String getUniqueOSMIdentifier(final AtlasObject object) } /** - * Similar to {@link BaseCheck#getUniqueOSMIdentifier(AtlasObject)} except instead of using the - * OSM identifier we use the Atlas identifier + * Generates a unique identifier given an {@link AtlasObject}. OSM/Atlas objects with different + * types can share the identifier (way 12345 - node 12345). This method makes sure we generate a + * truly unique identifier, based on the OSM identifier, among different types for an + * {@link AtlasObject}. If the AtlasObject is an instanceof AtlasEntity then it will simply use + * the type for the first part of the identifier, otherwise it will use the simple class name. * * @param object * {@link AtlasObject} to generate unique identifier for * @return unique object identifier among different types */ - protected String getUniqueObjectIdentifier(final AtlasObject object) + protected String getUniqueOSMIdentifier(final AtlasObject object) { if (object instanceof AtlasEntity) { return String.format("%s%s", ((AtlasEntity) object).getType().toShortString(), - object.getIdentifier()); + object.getOsmIdentifier()); } else { diff --git a/src/main/java/org/openstreetmap/atlas/checks/maproulette/MapRouletteCommand.java b/src/main/java/org/openstreetmap/atlas/checks/maproulette/MapRouletteCommand.java index db9d684c7..bf3fd4082 100644 --- a/src/main/java/org/openstreetmap/atlas/checks/maproulette/MapRouletteCommand.java +++ b/src/main/java/org/openstreetmap/atlas/checks/maproulette/MapRouletteCommand.java @@ -34,8 +34,8 @@ public MapRouletteClient getClient() protected synchronized void addTask(final String challengeName, final Task task) throws UnsupportedEncodingException, URISyntaxException { - this.mapRouletteClient - .addTask(new Challenge(challengeName, "", "", "", ChallengeDifficulty.EASY), task); + this.mapRouletteClient.addTask( + new Challenge(challengeName, "", "", "", ChallengeDifficulty.EASY, ""), task); } protected synchronized void addTask(final Challenge challenge, final Task task) diff --git a/src/main/java/org/openstreetmap/atlas/checks/maproulette/data/Challenge.java b/src/main/java/org/openstreetmap/atlas/checks/maproulette/data/Challenge.java index d57f8e331..77a75357b 100644 --- a/src/main/java/org/openstreetmap/atlas/checks/maproulette/data/Challenge.java +++ b/src/main/java/org/openstreetmap/atlas/checks/maproulette/data/Challenge.java @@ -34,6 +34,7 @@ public class Challenge implements Serializable public static final String KEY_RULE_OPERATOR = "operator"; public static final String VALUE_RULE_OPERATOR = "equal"; public static final String KEY_RULE_VALUE = "value"; + public static final String KEY_TAGS = "tags"; private static final long serialVersionUID = -8034692909431083341L; private static final Gson CHALLENGE_GSON = new GsonBuilder().disableHtmlEscaping() @@ -48,22 +49,23 @@ public class Challenge implements Serializable private final ChallengeDifficulty difficulty; private final String instruction; private String name; + private final String tags; private final ChallengePriority defaultPriority; private final String highPriorityRule; private final String mediumPriorityRule; private final String lowPriorityRule; public Challenge(final String name, final String description, final String blurb, - final String instruction, final ChallengeDifficulty difficulty) + final String instruction, final ChallengeDifficulty difficulty, final String tags) { this(name, description, blurb, instruction, difficulty, ChallengePriority.NONE, null, null, - null); + null, tags); } public Challenge(final String name, final String description, final String blurb, final String instruction, final ChallengeDifficulty difficulty, final ChallengePriority defaultPriority, final String highPriorityRule, - final String mediumPriorityRule, final String lowPriorityRule) + final String mediumPriorityRule, final String lowPriorityRule, final String tags) { this.name = name; this.description = description; @@ -74,6 +76,7 @@ public Challenge(final String name, final String description, final String blurb this.highPriorityRule = highPriorityRule; this.mediumPriorityRule = mediumPriorityRule; this.lowPriorityRule = lowPriorityRule; + this.tags = tags; } public long getId() @@ -146,6 +149,11 @@ public void setName(final String name) this.name = name; } + public String getTags() + { + return tags; + } + public JsonObject toJson(final String challengeName) { // if the challenge doesn't exist yet then create/update it diff --git a/src/main/java/org/openstreetmap/atlas/checks/maproulette/data/Survey.java b/src/main/java/org/openstreetmap/atlas/checks/maproulette/data/Survey.java index 628d59d6f..86e0d07d3 100644 --- a/src/main/java/org/openstreetmap/atlas/checks/maproulette/data/Survey.java +++ b/src/main/java/org/openstreetmap/atlas/checks/maproulette/data/Survey.java @@ -37,9 +37,9 @@ public static Challenge fromResource(final String checkName) public Survey(final String name, final String description, final String blurb, final String instruction, final ChallengeDifficulty difficulty, - final List answers) + final List answers, final String tags) { - super(name, description, blurb, instruction, difficulty); + super(name, description, blurb, instruction, difficulty, tags); this.answers = answers; } diff --git a/src/main/java/org/openstreetmap/atlas/checks/maproulette/serializer/ChallengeDeserializer.java b/src/main/java/org/openstreetmap/atlas/checks/maproulette/serializer/ChallengeDeserializer.java index ee1985b59..6c4819d00 100644 --- a/src/main/java/org/openstreetmap/atlas/checks/maproulette/serializer/ChallengeDeserializer.java +++ b/src/main/java/org/openstreetmap/atlas/checks/maproulette/serializer/ChallengeDeserializer.java @@ -60,7 +60,8 @@ public Challenge deserialize(final JsonElement json, final Type typeOfT, this.getStringValue(challengeObject, KEY_INSTRUCTION, ""), difficulty, priority, this.getValue(challengeObject, Challenge.KEY_HIGH_PRIORITY, null), this.getValue(challengeObject, Challenge.KEY_MEDIUM_PRIORITY, null), - this.getValue(challengeObject, Challenge.KEY_LOW_PRIORITY, null)); + this.getValue(challengeObject, Challenge.KEY_LOW_PRIORITY, null), + this.getStringValue(challengeObject, Challenge.KEY_TAGS, "")); } private String getStringValue(final JsonObject object, final String key, diff --git a/src/main/java/org/openstreetmap/atlas/checks/persistence/SparkFileHelper.java b/src/main/java/org/openstreetmap/atlas/checks/persistence/SparkFileHelper.java index d836e8a70..e2ebb4a0e 100644 --- a/src/main/java/org/openstreetmap/atlas/checks/persistence/SparkFileHelper.java +++ b/src/main/java/org/openstreetmap/atlas/checks/persistence/SparkFileHelper.java @@ -306,10 +306,7 @@ public void commit(final SparkFilePath path) logger.debug("Creating {}.", path.getTargetPath()); this.mkdir(path.getTargetPath()); } - if (!this.isDirectory(path.getTargetPath())) - { - throw new CoreException("{} not a directory.", path.getTargetPath()); - } + this.list(path.getTemporaryPath()).forEach(resource -> { logger.debug("Renaming {} in {} into {}.", resource.getName(), diff --git a/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsCheck.java b/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsCheck.java new file mode 100644 index 000000000..d102efab5 --- /dev/null +++ b/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsCheck.java @@ -0,0 +1,254 @@ +package org.openstreetmap.atlas.checks.validation.intersections; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.openstreetmap.atlas.checks.base.BaseCheck; +import org.openstreetmap.atlas.checks.flag.CheckFlag; +import org.openstreetmap.atlas.geography.PolyLine; +import org.openstreetmap.atlas.geography.Polygon; +import org.openstreetmap.atlas.geography.atlas.items.Area; +import org.openstreetmap.atlas.geography.atlas.items.AtlasObject; +import org.openstreetmap.atlas.geography.clipping.Clip; +import org.openstreetmap.atlas.geography.clipping.Clip.ClipType; +import org.openstreetmap.atlas.tags.BuildingTag; +import org.openstreetmap.atlas.utilities.configuration.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vividsolutions.jts.geom.TopologyException; + +/** + * Flags the buildings that intersect/overlap with other buildings. + * + * @author sid + * @author mkalender + */ +public class IntersectingBuildingsCheck extends BaseCheck +{ + /** + * Differentiate intersection and overlap and create separate descriptions for MapRoulette + * challenges. Overlapping buildings are ones where intersection of two buildings covers at + * least one of team. + */ + public enum IntersectionType + { + NONE, + INTERSECT, + OVERLAP; + } + + private static final Logger logger = LoggerFactory.getLogger(IntersectingBuildingsCheck.class); + private static final long serialVersionUID = 5796448445672515517L; + + // Instructions + private static final String INTERSECT_INSTRUCTION = "Building (id={0,number,#}) intersects with another building (id={1,number,#})."; + private static final String OVERLAP_INSTRUCTION = "Building (id={0,number,#}) is overlapped by another building (id={1,number,#})."; + private static final List FALLBACK_INSTRUCTIONS = Arrays.asList(OVERLAP_INSTRUCTION, + INTERSECT_INSTRUCTION); + + // Default values for configurable settings + private static final double INTERSECTION_LOWER_LIMIT_DEFAULT = 0.01; + private static final double OVERLAP_LOWER_LIMIT_DEFAULT = 0.90; + + // Minimum number of points for a polygon + private static final int MINIMUM_POINT_COUNT_FOR_POLYGON = 3; + + // Format to use to generate unique identifier tuples from areas + private static final String UNIQUE_IDENTIFIER_FORMAT = "%s,%s"; + + /* + * If the proportion of intersection compared to area of building is more than 2%, then we have + * a building intersection + */ + private final double intersectionLowerLimit; + + /* + * In addition to intersections, we have some cases where buildings exactly overlap, due to + * inadvertent duplicates. We flag them into a separate category + */ + private final double overlapLowerLimit; + + /** + * Generates a unique identifier tuple as a {@link String} for given {@link Area}s identifiers + * + * @param area + * First {@link Area} instance + * @param otherArea + * Another {@link Area} instance + * @return A unique identifier tuple in {@link String} + */ + private static String getIdentifierTuple(final Area area, final Area otherArea) + { + return area.getIdentifier() < otherArea.getIdentifier() + ? String.format(UNIQUE_IDENTIFIER_FORMAT, area.getIdentifier(), + otherArea.getIdentifier()) + : String.format(UNIQUE_IDENTIFIER_FORMAT, otherArea.getIdentifier(), + area.getIdentifier()); + } + + /** + * Default constructor + * + * @param configuration + * the JSON configuration for this check + */ + public IntersectingBuildingsCheck(final Configuration configuration) + { + super(configuration); + + this.intersectionLowerLimit = this.configurationValue(configuration, + "intersection.lower.limit", INTERSECTION_LOWER_LIMIT_DEFAULT, Double::valueOf); + this.overlapLowerLimit = this.configurationValue(configuration, "overlap.lower.limit", + OVERLAP_LOWER_LIMIT_DEFAULT, Double::valueOf); + } + + @Override + public boolean validCheckForObject(final AtlasObject object) + { + return object instanceof Area && BuildingTag.isBuilding(object); + } + + @Override + protected Optional flag(final AtlasObject object) + { + final Area building = (Area) object; + + // Fetch building's area as polygon and make sure it has at least 3 points + final Polygon buildingPolygon = building.asPolygon(); + if (buildingPolygon.size() < MINIMUM_POINT_COUNT_FOR_POLYGON) + { + return Optional.empty(); + } + + // Fetch possibly intersecting buildings + final Iterable possiblyIntersectingBuildings = object.getAtlas().areasIntersecting( + building.bounds(), + area -> BuildingTag.isBuilding(area) + && building.getIdentifier() != area.getIdentifier() + && area.intersects(buildingPolygon)); + + // Assuming that we'd find intersections/overlaps below, create a flag + final CheckFlag flag = new CheckFlag(this.getTaskIdentifier(object)); + flag.addObject(object); + boolean hadIntersection = false; + + // Go over possible intersections + for (final Area otherBuilding : possiblyIntersectingBuildings) + { + // Fetch other building's area as polygon and make sure it has at least 3 points + final Polygon otherBuildingsPolygon = otherBuilding.asPolygon(); + if (otherBuildingsPolygon.size() < MINIMUM_POINT_COUNT_FOR_POLYGON) + { + continue; + } + + // Create a unique identifier for building tuple to avoid processing same buildings more + // than once + final String uniqueIdentifier = getIdentifierTuple(building, otherBuilding); + if (this.isFlagged(getIdentifierTuple(building, otherBuilding))) + { + continue; + } + + // Find intersection type + final IntersectionType resultType = this.findIntersectionType(buildingPolygon, + otherBuildingsPolygon); + + // Flag based on intersection type + if (resultType == IntersectionType.OVERLAP) + { + flag.addObject(otherBuilding, this.getLocalizedInstruction(0, + object.getOsmIdentifier(), otherBuilding.getOsmIdentifier())); + this.markAsFlagged(uniqueIdentifier); + hadIntersection = true; + } + else if (resultType == IntersectionType.INTERSECT) + { + flag.addObject(otherBuilding, this.getLocalizedInstruction(1, + object.getOsmIdentifier(), otherBuilding.getOsmIdentifier())); + this.markAsFlagged(uniqueIdentifier); + hadIntersection = true; + } + } + + if (hadIntersection) + { + return Optional.of(flag); + } + + return Optional.empty(); + } + + @Override + protected List getFallbackInstructions() + { + return FALLBACK_INSTRUCTIONS; + } + + /** + * Find {@link IntersectionType} for given {@link Polygon}s. There are some edge cases where + * there are minor boundary intersections. So we do additional area check to filter off the + * false positives. + * + * @param polygon + * {@link Polygon} to check for intersection + * @param otherPolygon + * Another {@link Polygon} to check against for intersection + * @param areaSizeToCheck + * Area size to decide if intersection area is big enough for overlap + * @return {@link IntersectionType} between given {@link Polygon}s + */ + private IntersectionType findIntersectionType(final Polygon polygon, final Polygon otherPolygon) + { + Clip clip = null; + try + { + clip = polygon.clip(otherPolygon, ClipType.AND); + } + catch (final TopologyException e) + { + logger.warn(String.format("Skipping intersection check. Error clipping [%s] and [%s].", + polygon, otherPolygon), e); + } + + // Skip if nothing is returned + if (clip == null) + { + return IntersectionType.NONE; + } + + // Sum intersection area + long intersectionArea = 0; + for (final PolyLine polyline : clip.getClip()) + { + if (polyline != null && polyline instanceof Polygon) + { + final Polygon clippedPolygon = (Polygon) polyline; + intersectionArea += clippedPolygon.surface().asDm7Squared(); + } + } + + // Avoid division by zero + if (intersectionArea == 0) + { + return IntersectionType.NONE; + } + + // Pick the smaller building's area as baseline + final long baselineArea = Math.min(polygon.surface().asDm7Squared(), + otherPolygon.surface().asDm7Squared()); + final double proportion = (double) intersectionArea / baselineArea; + if (proportion >= this.overlapLowerLimit) + { + return IntersectionType.OVERLAP; + } + else if (proportion >= this.intersectionLowerLimit) + { + return IntersectionType.INTERSECT; + } + + return IntersectionType.NONE; + } +} diff --git a/src/main/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheck.java b/src/main/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheck.java new file mode 100644 index 000000000..8684f95a8 --- /dev/null +++ b/src/main/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheck.java @@ -0,0 +1,143 @@ +package org.openstreetmap.atlas.checks.validation.linear.edges; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import org.openstreetmap.atlas.checks.atlas.predicates.TypePredicates; +import org.openstreetmap.atlas.checks.base.BaseCheck; +import org.openstreetmap.atlas.checks.flag.CheckFlag; +import org.openstreetmap.atlas.geography.atlas.items.AtlasObject; +import org.openstreetmap.atlas.geography.atlas.items.Edge; +import org.openstreetmap.atlas.tags.DestinationTag; +import org.openstreetmap.atlas.tags.HighwayTag; +import org.openstreetmap.atlas.tags.annotations.validation.Validators; +import org.openstreetmap.atlas.utilities.configuration.Configuration; +import org.openstreetmap.atlas.utilities.scalars.Distance; + +/** + * The SignPostCheck is used to help identify segments that are missing the proper tagging for sign + * posts. The basic logic of the check is to first find all motorway and trunk edges which have link + * edges departing them. Once you have identified these candidates a flag is thrown if one or both + * of the following conditions are met. + *

+ * 1) The shared node is missing the highway=motorway_junction tag
+ * 2) The exiting link edge is missing the destination tag
+ *

+ * If either of these cases is true and the link edge is over a certain length then a flag is + * signalled. + * + * @author ericgodwin + */ +public class SignPostCheck extends BaseCheck +{ + private static final long serialVersionUID = 8042255121118115024L; + + // Instruction + private static final String INSTRUCTION = "Junction node is missing the following tags: {0}."; + private static final List FALLBACK_INSTRUCTIONS = Arrays.asList(INSTRUCTION); + + // Predicate defining the type of outbound links we are looking for. + private static final Predicate LINK_PREDICATE = edge -> edge + .highwayTag() == HighwayTag.MOTORWAY_LINK || edge.highwayTag() == HighwayTag.TRUNK_LINK; + + // The default minimum link length which will trigger the check. + public static final double DISTANCE_MINIMUM_METERS_DEFAULT = 50; + + // The minimum link length to examine. + private final Distance minimumLinkLength; + + /** + * The default constructor that must be supplied. The Atlas Checks framework will generate the + * checks with this constructor, supplying a configuration that can be used to adjust any + * parameters that the check uses during operation. + * + * @param configuration + * the JSON configuration for this check + */ + public SignPostCheck(final Configuration configuration) + { + super(configuration); + + this.minimumLinkLength = configurationValue(configuration, "linkLength.minimum.meters", + DISTANCE_MINIMUM_METERS_DEFAULT, Distance::meters); + } + + /** + * This function will validate if the supplied atlas object is valid for the check. + * + * @param object + * the atlas object supplied by the Atlas-Checks framework for evaluation + * @return {@code true} if this object should be checked + */ + @Override + public boolean validCheckForObject(final AtlasObject object) + { + return TypePredicates.IS_EDGE.test(object) && Validators.isOfType(object, HighwayTag.class, + HighwayTag.MOTORWAY, HighwayTag.TRUNK, HighwayTag.PRIMARY, HighwayTag.PRIMARY_LINK); + } + + /** + * This is the actual function that will check to see whether the object needs to be flagged. + * + * @param object + * the atlas object supplied by the Atlas-Checks framework for evaluation + * @return an optional {@link CheckFlag} object that + */ + @Override + protected Optional flag(final AtlasObject object) + { + final Edge edge = (Edge) object; + final Set missingTags = new HashSet(); + final Set offendingObjects = new HashSet(); + + // First find any motorway_link edges going out of the edge + edge.outEdges().stream().filter(LINK_PREDICATE).forEach(outEdge -> + { + // Let's ignore really short segments as they are often connectors + // between doubly digitized trunk roads. + if (outEdge.length().isGreaterThan(this.minimumLinkLength)) + { + // Check to see if the node leaving the mainline is missing the + // junction tag + if (!Validators.isOfType(outEdge.start(), HighwayTag.class, + HighwayTag.MOTORWAY_JUNCTION)) + { + missingTags.add(String.format("%s=%s", HighwayTag.KEY, + HighwayTag.MOTORWAY_JUNCTION.getTagValue())); + offendingObjects.add(outEdge.start()); + } + + // See if the out edge has the destination tag. + if (!outEdge.getTag(DestinationTag.KEY).isPresent()) + { + missingTags.add(DestinationTag.KEY); + + // Instead of adding the edge we are adding the node so + // that a single point in the task is highlighted. + offendingObjects.add(outEdge.start()); + } + } + }); + + // If it has an out link edge make sure that the end node has the appropriate + // highway=motorway_junction tag. + if (!offendingObjects.isEmpty()) + { + final String instruction = this.getLocalizedInstruction(0, + String.join(", ", missingTags)); + return Optional.of(this.createFlag(offendingObjects, instruction)); + } + + return Optional.empty(); + } + + @Override + protected List getFallbackInstructions() + { + return FALLBACK_INSTRUCTIONS; + } +} diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties index 3af91d2d3..91d33524c 100644 --- a/src/main/resources/log4j.properties +++ b/src/main/resources/log4j.properties @@ -8,6 +8,7 @@ log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1 # Show our logs log4j.logger.org.openstreetmap.atlas=INFO +log4j.logger.org.openstreetmap.atlas.geography.clipping=ERROR # Define the file appender log4j.appender.FILE=org.apache.log4j.RollingFileAppender diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BuildingRoadIntersectionCheckTest.java b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BuildingRoadIntersectionCheckTest.java index 9cd3a7810..db6d6f228 100644 --- a/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BuildingRoadIntersectionCheckTest.java +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BuildingRoadIntersectionCheckTest.java @@ -18,7 +18,7 @@ public class BuildingRoadIntersectionCheckTest ConfigurationResolver.emptyConfiguration()); private static final BuildingRoadIntersectionCheck spanishCheck = new BuildingRoadIntersectionCheck( ConfigurationResolver.inlineConfiguration( - "{\"CheckResourceLoader\": {\"scanUrls\": [\"org.openstreetmap.atlas.checks\"]},\"BuildingRoadIntersectionCheck\":{\"enabled\":true,\"locale\":\"es\",\"flags\":{\"en\":[\"Building (id-{0}) intersects road (id-{1})\"],\"es\":[\"Edificio(id-{0}) cruza calle(id-{0})\"]}}}")); + "{\"CheckResourceLoader\": {\"scanUrls\": [\"org.openstreetmap.atlas.checks\"]},\"BuildingRoadIntersectionCheck\":{\"enabled\":true,\"locale\":\"es\",\"flags\":{\"en\":[\"Building (id-{0,number,#}) intersects road (id-{1,number,#})\"],\"es\":[\"Edificio(id-{0,number,#}) cruza calle(id-{1,number,#})\"]}}}")); @Rule public BuildingRoadIntersectionTestCaseRule setup = new BuildingRoadIntersectionTestCaseRule(); @@ -57,7 +57,8 @@ public void testSpanishCheck() this.verifier.actual(this.setup.getAtlas(), spanishCheck); this.verifier.verify(flag -> { - Assert.assertTrue(flag.getInstructions().contains("Edificio(id-0) cruza calle(id-0)")); + Assert.assertTrue( + flag.getInstructions().contains("Edificio(id-323232) cruza calle(id-292929)")); }); this.verifier.verifyExpectedSize(2); diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BuildingRoadIntersectionTestCaseRule.java b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BuildingRoadIntersectionTestCaseRule.java index 91651f44e..04d20a965 100644 --- a/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BuildingRoadIntersectionTestCaseRule.java +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BuildingRoadIntersectionTestCaseRule.java @@ -37,13 +37,14 @@ public class BuildingRoadIntersectionTestCaseRule extends CoreTestRule edges = { - @Edge(coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_2), + @Edge(id = "292929292929", coordinates = { @Loc(value = TEST_1), + @Loc(value = TEST_2), @Loc(value = TEST_3) }, tags = { "highway=primary" }) }, areas = { // Regular building - flagged - @Area(coordinates = { @Loc(value = TEST_5), @Loc(value = TEST_2), - @Loc(value = TEST_4), @Loc(value = TEST_1), + @Area(id = "323232323232", coordinates = { @Loc(value = TEST_5), + @Loc(value = TEST_2), @Loc(value = TEST_4), @Loc(value = TEST_1), @Loc(value = TEST_6) }, tags = { "building=yes" }), // Non-Pedestrian Area - flagged @Area(coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_2), diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsCheckTest.java b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsCheckTest.java new file mode 100644 index 000000000..07738600b --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsCheckTest.java @@ -0,0 +1,119 @@ +package org.openstreetmap.atlas.checks.validation.intersections; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.openstreetmap.atlas.checks.configuration.ConfigurationResolver; +import org.openstreetmap.atlas.checks.flag.CheckFlag; +import org.openstreetmap.atlas.checks.validation.verifier.ConsumerBasedExpectedCheckVerifier; + +/** + * {@link IntersectingBuildingsCheck} unit test + * + * @author mkalender + */ +public class IntersectingBuildingsCheckTest +{ + private static final IntersectingBuildingsCheck CHECK = new IntersectingBuildingsCheck( + ConfigurationResolver.emptyConfiguration()); + + @Rule + public IntersectingBuildingsTestCaseRule setup = new IntersectingBuildingsTestCaseRule(); + + @Rule + public ConsumerBasedExpectedCheckVerifier verifier = new ConsumerBasedExpectedCheckVerifier(); + + @Test + public void testDuplicateBuildingsAtlas() + { + this.verifier.actual(this.setup.duplicateBuildingsAtlas(), CHECK); + this.verifier.verifyNotEmpty(); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> + { + Assert.assertEquals(2, flag.getFlaggedObjects().size()); + Assert.assertTrue(flag.getInstructions().contains("overlapped by another building")); + }); + } + + @Test + public void testNeighborBuildingsAtlas() + { + this.verifier.actual(this.setup.neighborBuildingsAtlas(), CHECK); + this.verifier.verifyEmpty(); + } + + @Test + public void testNoIntersectingBuildingAtlas() + { + this.verifier.actual(this.setup.noIntersectingBuildingAtlas(), CHECK); + this.verifier.verifyEmpty(); + } + + @Test + public void testSeveralDuplicateBuildingsAtlas() + { + this.verifier.actual(this.setup.severalDuplicateBuildingsAtlas(), CHECK); + this.verifier.verifyNotEmpty(); + this.verifier.verifyExpectedSize(3); + this.verifier.globallyVerify(flags -> + { + // First building overlaps with all other buildings + final CheckFlag firstFlag = flags.get(0); + Assert.assertEquals(4, firstFlag.getFlaggedObjects().size()); + + // Second building overlaps with the third and fourth one (it's overlap with first is + // already flagged) + final CheckFlag secondFlag = flags.get(1); + Assert.assertEquals(3, secondFlag.getFlaggedObjects().size()); + + // Third building's overlap with fourth building will be flagged with the third flag + final CheckFlag thirdFlag = flags.get(2); + Assert.assertEquals(2, thirdFlag.getFlaggedObjects().size()); + }); + } + + @Test + public void testSmallIntersectionAreasAtlas() + { + this.verifier.actual(this.setup.smallIntersectionAreasAtlas(), CHECK); + this.verifier.verifyEmpty(); + } + + @Test + public void testSmallIntersectionBuildingsAtlas() + { + this.verifier.actual(this.setup.smallIntersectionBuildingsAtlas(), CHECK); + this.verifier.verifyNotEmpty(); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> + { + Assert.assertEquals(2, flag.getFlaggedObjects().size()); + Assert.assertTrue(flag.getInstructions().contains("intersects with another building")); + }); + } + + @Test + public void testSmallIntersectionBuildingsAtlasWithIncreasedIntersectionLowerLimit() + { + this.verifier.actual(this.setup.smallIntersectionBuildingsAtlas(), + new IntersectingBuildingsCheck(ConfigurationResolver.inlineConfiguration( + "{\"IntersectingBuildingsCheck\": {\"intersection.lower.limit\": 0.15}}"))); + this.verifier.verifyEmpty(); + } + + @Test + public void testSmallIntersectionBuildingsAtlasWithSmallOverlapLowerLimit() + { + this.verifier.actual(this.setup.smallIntersectionBuildingsAtlas(), + new IntersectingBuildingsCheck(ConfigurationResolver.inlineConfiguration( + "{\"IntersectingBuildingsCheck\": {\"intersection.lower.limit\": 0.01, \"overlap.lower.limit\": 0.10}}"))); + this.verifier.verifyNotEmpty(); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> + { + Assert.assertEquals(2, flag.getFlaggedObjects().size()); + Assert.assertTrue(flag.getInstructions().contains("overlapped by another building")); + }); + } +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsTestCaseRule.java b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsTestCaseRule.java new file mode 100644 index 000000000..92ab75a6a --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/IntersectingBuildingsTestCaseRule.java @@ -0,0 +1,114 @@ +package org.openstreetmap.atlas.checks.validation.intersections; + +import org.openstreetmap.atlas.geography.atlas.Atlas; +import org.openstreetmap.atlas.utilities.testing.CoreTestRule; +import org.openstreetmap.atlas.utilities.testing.TestAtlas; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Area; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Loc; + +/** + * {@link IntersectingBuildingsCheckTest} data generator + * + * @author mkalender + */ +public class IntersectingBuildingsTestCaseRule extends CoreTestRule +{ + private static final String TEST_1 = "47.620079, -122.206879"; + private static final String TEST_2 = "47.619576, -122.206917"; + private static final String TEST_3 = "47.620094, -122.205856"; + private static final String TEST_4 = "47.619587, -122.205833"; + private static final String TEST_5 = "47.620087, -122.204948"; + private static final String TEST_6 = "47.619522, -122.204926"; + private static final String TEST_7 = "47.620102, -122.206024"; + private static final String TEST_8 = "47.619583, -122.20594"; + + @TestAtlas(areas = { + // a building + @Area(coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_7), @Loc(value = TEST_8), + @Loc(value = TEST_2) }, tags = { "building=yes" }), + // another building, but no intersection + @Area(coordinates = { @Loc(value = TEST_3), @Loc(value = TEST_5), @Loc(value = TEST_6), + @Loc(value = TEST_4) }, tags = { "building=yes" }) }) + private Atlas noIntersectingBuildingAtlas; + + @TestAtlas(areas = { + // a building + @Area(coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_7), @Loc(value = TEST_8), + @Loc(value = TEST_2) }, tags = { "building=yes" }), + // another neighbor building, but no intersection + @Area(coordinates = { @Loc(value = TEST_7), @Loc(value = TEST_3), @Loc(value = TEST_4), + @Loc(value = TEST_8) }, tags = { "building=yes" }) }) + private Atlas neighborBuildingsAtlas; + + @TestAtlas(areas = { + // a building + @Area(coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_3), @Loc(value = TEST_4), + @Loc(value = TEST_2) }, tags = { "building=yes" }), + // another neighbor building intersecting with the first one + @Area(coordinates = { @Loc(value = TEST_7), @Loc(value = TEST_5), @Loc(value = TEST_6), + @Loc(value = TEST_8) }, tags = { "building=yes" }) }) + private Atlas smallIntersectionBuildingsAtlas; + + @TestAtlas(areas = { + // a building + @Area(coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_3), @Loc(value = TEST_4), + @Loc(value = TEST_2) }, tags = { "building=yes" }), + // another area intersecting, but not a building + @Area(coordinates = { @Loc(value = TEST_7), @Loc(value = TEST_5), @Loc(value = TEST_6), + @Loc(value = TEST_8) }, tags = { "building=no" }) }) + private Atlas smallIntersectionAreasAtlas; + + @TestAtlas(areas = { + // a building + @Area(coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_3), @Loc(value = TEST_4), + @Loc(value = TEST_2) }, tags = { "building=yes" }), + // another building with same footprint + @Area(coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_3), @Loc(value = TEST_4), + @Loc(value = TEST_2) }, tags = { "building=yes" }) }) + private Atlas duplicateBuildingsAtlas; + + @TestAtlas(areas = { + // a building + @Area(id = "1234567000000", coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_3), + @Loc(value = TEST_4), @Loc(value = TEST_2) }, tags = { "building=yes" }), + // another building with same footprint + @Area(id = "2234567000000", coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_3), + @Loc(value = TEST_4), @Loc(value = TEST_2) }, tags = { "building=yes" }), + // another building with different footprint but overlapping + @Area(id = "3234567000000", coordinates = { @Loc(value = TEST_7), @Loc(value = TEST_3), + @Loc(value = TEST_4), @Loc(value = TEST_8) }, tags = { "building=yes" }), + // another building with different footprint but overlapping + @Area(id = "4234567000000", coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_5), + @Loc(value = TEST_6), @Loc(value = TEST_2) }, tags = { "building=yes" }) }) + private Atlas severalDuplicateBuildingsAtlas; + + public Atlas duplicateBuildingsAtlas() + { + return this.duplicateBuildingsAtlas; + } + + public Atlas neighborBuildingsAtlas() + { + return this.neighborBuildingsAtlas; + } + + public Atlas noIntersectingBuildingAtlas() + { + return this.noIntersectingBuildingAtlas; + } + + public Atlas severalDuplicateBuildingsAtlas() + { + return this.severalDuplicateBuildingsAtlas; + } + + public Atlas smallIntersectionAreasAtlas() + { + return this.smallIntersectionAreasAtlas; + } + + public Atlas smallIntersectionBuildingsAtlas() + { + return this.smallIntersectionBuildingsAtlas; + } +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheckTest.java b/src/test/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheckTest.java new file mode 100644 index 000000000..109fc3d96 --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheckTest.java @@ -0,0 +1,196 @@ +package org.openstreetmap.atlas.checks.validation.linear.edges; + +import java.util.Set; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.openstreetmap.atlas.checks.configuration.ConfigurationResolver; +import org.openstreetmap.atlas.checks.flag.CheckFlag; +import org.openstreetmap.atlas.checks.flag.FlaggedObject; +import org.openstreetmap.atlas.checks.validation.verifier.ConsumerBasedExpectedCheckVerifier; + +/** + * Tests for {@link SignPostCheck} + * + * @author mkalender + */ +public class SignPostCheckTest +{ + private static SignPostCheck CHECK = new SignPostCheck( + ConfigurationResolver.emptyConfiguration()); + + @Rule + public SignPostCheckTestRule setup = new SignPostCheckTestRule(); + + @Rule + public ConsumerBasedExpectedCheckVerifier verifier = new ConsumerBasedExpectedCheckVerifier(); + + private static void verify(final CheckFlag flag, final boolean shouldFlagDestination, + final boolean shouldFlagJunction) + { + // Verify that only node is flagged + final Set flaggedObjects = flag.getFlaggedObjects(); + Assert.assertEquals(1, flaggedObjects.size()); + + // Verify the node id + Assert.assertEquals(SignPostCheckTestRule.JUNCTION_NODE_ID, flag.getIdentifier()); + + // Verify the instruction tag keys and values + Assert.assertTrue(!shouldFlagDestination || flag.getInstructions().contains("destination")); + Assert.assertTrue(!shouldFlagJunction + || flag.getInstructions().contains("highway=motorway_junction")); + } + + @Test + public void motorwayMotorwayLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.motorwayMotorwayLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verify(flag, true, true)); + } + + @Test + public void motorwayMotorwayLinkWithJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.motorwayMotorwayLinkWithJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyEmpty(); + } + + @Test + public void motorwayMultipleLinksMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.motorwayMultipleLinksMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verify(flag, true, true)); + } + + @Test + public void motorwayPrimaryLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.motorwayPrimaryLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyEmpty(); + } + + @Test + public void motorwayShortMotorwayLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual( + this.setup.motorwayShortMotorwayLinkMissingJunctionAndDestinationAtlas(), CHECK); + this.verifier.verifyEmpty(); + } + + @Test + public void motorwayShortTrunkLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.motorwayShortTrunkLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyEmpty(); + } + + @Test + public void motorwayTrunkLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.motorwayTrunkLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verify(flag, true, true)); + } + + @Test + public void motorwayTrunkLinkWithDestinationAtlas() + { + this.verifier.actual(this.setup.motorwayTrunkLinkWithDestinationAtlas(), CHECK); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verify(flag, false, true)); + } + + @Test + public void primaryLinkMotorwayLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.primaryLinkMotorwayLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verify(flag, true, true)); + } + + @Test + public void primaryLinkTrunkLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.primaryLinkTrunkLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verify(flag, true, true)); + } + + @Test + public void primaryMotorwayLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.primaryMotorwayLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verify(flag, true, true)); + } + + @Test + public void primaryTrunkLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.primaryTrunkLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verify(flag, true, true)); + } + + @Test + public void trunkMotorwayLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.trunkMotorwayLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verify(flag, true, true)); + } + + @Test + public void trunkMotorwayLinkWithJunctionAtlas() + { + this.verifier.actual(this.setup.trunkMotorwayLinkWithJunctionAtlas(), CHECK); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verify(flag, true, false)); + } + + @Test + public void trunkPrimaryLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.trunkPrimaryLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyEmpty(); + } + + @Test + public void trunkShortMotorwayLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.trunkShortMotorwayLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyEmpty(); + } + + @Test + public void trunkShortTrunkLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.trunkShortTrunkLinkMissingJunctionAndDestinationAtlas(), + CHECK); + this.verifier.verifyEmpty(); + } + + @Test + public void trunkTrunkLinkMissingJunctionAndDestinationAtlas() + { + this.verifier.actual(this.setup.trunkTrunkLinkMissingJunctionAndDestinationAtlas(), CHECK); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verify(flag, true, true)); + } +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheckTestRule.java b/src/test/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheckTestRule.java new file mode 100644 index 000000000..8bab18051 --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/linear/edges/SignPostCheckTestRule.java @@ -0,0 +1,444 @@ +package org.openstreetmap.atlas.checks.validation.linear.edges; + +import org.openstreetmap.atlas.geography.atlas.Atlas; +import org.openstreetmap.atlas.utilities.testing.CoreTestRule; +import org.openstreetmap.atlas.utilities.testing.TestAtlas; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Edge; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Loc; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Node; + +/** + * {@link SignPostCheckTest} data generator + * + * @author mkalender + */ +public class SignPostCheckTestRule extends CoreTestRule +{ + private static final String HIGHWAY_1 = "47.636459, -122.322678"; + private static final String HIGHWAY_2 = "47.637684, -122.322617"; + private static final String HIGHWAY_3 = "47.639133, -122.322647"; + private static final String HIGHWAY_4 = "47.641319, -122.322601"; + private static final String HIGHWAY_5 = "47.643585, -122.322487"; + + private static final String LINK_1 = "47.639336, -122.322525"; + private static final String LINK_2 = "47.640697, -122.322395"; + private static final String LINK_3 = "47.642033, -122.321556"; + private static final String LINK_4 = "47.642471, -122.319885"; + private static final String LINK_5 = "47.641216, -122.3218"; + + public static final String JUNCTION_NODE_ID = "123456789"; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1) }, tags = { + "highway=trunk_link" }) }) + private Atlas trunkShortTrunkLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1) }, tags = { + "highway=motorway_link" }) }) + private Atlas trunkShortMotorwayLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1) }, tags = { + "highway=trunk_link" }) }) + private Atlas motorwayShortTrunkLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1) }, tags = { + "highway=motorway_link" }) }) + private Atlas motorwayShortMotorwayLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=trunk_link" }) }) + private Atlas trunkTrunkLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=motorway_link" }) }) + private Atlas trunkMotorwayLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=trunk_link" }) }) + private Atlas motorwayTrunkLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=motorway_link" }) }) + private Atlas motorwayMotorwayLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)), + @Node(coordinates = @Loc(value = LINK_5)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=motorway_link" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), + @Loc(value = LINK_5) }, tags = { "highway=trunk_link" }) }) + private Atlas motorwayMultipleLinksMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=primary_link" }) }) + private Atlas trunkPrimaryLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=primary_link" }) }) + private Atlas motorwayPrimaryLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=primary" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=primary" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=trunk_link" }) }) + private Atlas primaryTrunkLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=primary_link" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=primary_link" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=trunk_link" }) }) + private Atlas primaryLinkTrunkLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=primary" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=primary" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=motorway_link" }) }) + private Atlas primaryMotorwayLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=primary_link" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=primary_link" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=motorway_link" }) }) + private Atlas primaryLinkMotorwayLinkMissingJunctionAndDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3)), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=trunk_link", + "destination=somewhere" }) }) + private Atlas motorwayTrunkLinkWithDestinationAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3), tags = { + "highway=motorway_junction" }), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=trunk" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=motorway_link" }) }) + private Atlas trunkMotorwayLinkWithJunctionAtlas; + + @TestAtlas( + // nodes + nodes = { @Node(coordinates = @Loc(value = HIGHWAY_1)), + @Node(id = JUNCTION_NODE_ID, coordinates = @Loc(value = HIGHWAY_3), tags = { + "highway=motorway_junction" }), + @Node(coordinates = @Loc(value = HIGHWAY_5)), + @Node(coordinates = @Loc(value = LINK_1)), + @Node(coordinates = @Loc(value = LINK_4)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = HIGHWAY_1), @Loc(value = HIGHWAY_2), + @Loc(value = HIGHWAY_3) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = HIGHWAY_4), + @Loc(value = HIGHWAY_5) }, tags = { "highway=motorway" }), + @Edge(coordinates = { @Loc(value = HIGHWAY_3), @Loc(value = LINK_1), + @Loc(value = LINK_2), @Loc(value = LINK_3), + @Loc(value = LINK_4) }, tags = { "highway=motorway_link", + "destination=somewhere" }) }) + private Atlas motorwayMotorwayLinkWithJunctionAndDestinationAtlas; + + public Atlas motorwayMotorwayLinkMissingJunctionAndDestinationAtlas() + { + return this.motorwayMotorwayLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas motorwayMotorwayLinkWithJunctionAndDestinationAtlas() + { + return this.motorwayMotorwayLinkWithJunctionAndDestinationAtlas; + } + + public Atlas motorwayMultipleLinksMissingJunctionAndDestinationAtlas() + { + return this.motorwayMultipleLinksMissingJunctionAndDestinationAtlas; + } + + public Atlas motorwayPrimaryLinkMissingJunctionAndDestinationAtlas() + { + return this.motorwayPrimaryLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas motorwayShortMotorwayLinkMissingJunctionAndDestinationAtlas() + { + return this.motorwayShortMotorwayLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas motorwayShortTrunkLinkMissingJunctionAndDestinationAtlas() + { + return this.motorwayShortTrunkLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas motorwayTrunkLinkMissingJunctionAndDestinationAtlas() + { + return this.motorwayTrunkLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas motorwayTrunkLinkWithDestinationAtlas() + { + return this.motorwayTrunkLinkWithDestinationAtlas; + } + + public Atlas primaryLinkMotorwayLinkMissingJunctionAndDestinationAtlas() + { + return this.primaryLinkMotorwayLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas primaryLinkTrunkLinkMissingJunctionAndDestinationAtlas() + { + return this.primaryLinkTrunkLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas primaryMotorwayLinkMissingJunctionAndDestinationAtlas() + { + return this.primaryMotorwayLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas primaryTrunkLinkMissingJunctionAndDestinationAtlas() + { + return this.primaryTrunkLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas trunkMotorwayLinkMissingJunctionAndDestinationAtlas() + { + return this.trunkMotorwayLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas trunkMotorwayLinkWithJunctionAtlas() + { + return this.trunkMotorwayLinkWithJunctionAtlas; + } + + public Atlas trunkPrimaryLinkMissingJunctionAndDestinationAtlas() + { + return this.trunkPrimaryLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas trunkShortMotorwayLinkMissingJunctionAndDestinationAtlas() + { + return this.trunkShortMotorwayLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas trunkShortTrunkLinkMissingJunctionAndDestinationAtlas() + { + return this.trunkShortTrunkLinkMissingJunctionAndDestinationAtlas; + } + + public Atlas trunkTrunkLinkMissingJunctionAndDestinationAtlas() + { + return this.trunkTrunkLinkMissingJunctionAndDestinationAtlas; + } +}