// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.pt_assistant.validation;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.SelectCommand;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.validation.Severity;
import org.openstreetmap.josm.data.validation.Test;
import org.openstreetmap.josm.data.validation.TestError;
import org.openstreetmap.josm.data.validation.TestError.Builder;
import org.openstreetmap.josm.plugins.pt_assistant.PTAssistantPlugin;
import org.openstreetmap.josm.plugins.pt_assistant.actions.FixTask;
import org.openstreetmap.josm.plugins.pt_assistant.actions.IncompleteMembersDownloadThread;
import org.openstreetmap.josm.plugins.pt_assistant.data.PTRouteDataManager;
import org.openstreetmap.josm.plugins.pt_assistant.data.PTRouteSegment;
import org.openstreetmap.josm.plugins.pt_assistant.data.PTStop;
import org.openstreetmap.josm.plugins.pt_assistant.data.PTWay;
import org.openstreetmap.josm.plugins.pt_assistant.gui.IncompleteMembersDownloadDialog;
import org.openstreetmap.josm.plugins.pt_assistant.gui.PTAssistantLayer;
import org.openstreetmap.josm.plugins.pt_assistant.gui.ProceedDialog;
import org.openstreetmap.josm.plugins.pt_assistant.utils.RouteUtils;
import org.openstreetmap.josm.plugins.pt_assistant.utils.StopToWayAssigner;
import org.openstreetmap.josm.plugins.pt_assistant.utils.StopUtils;
import org.openstreetmap.josm.tools.Utils;
public class PTAssistantValidatorTest extends Test {
public static final int ERROR_CODE_SORTING = 3711;
public static final int ERROR_CODE_ROAD_TYPE = 3721;
public static final int ERROR_CODE_CONSTRUCTION = 3722;
public static final int ERROR_CODE_DIRECTION = 3731;
public static final int ERROR_CODE_END_STOP = 3741;
public static final int ERROR_CODE_SPLIT_WAY = 3742;
public static final int ERROR_CODE_RELAITON_MEMBER_ROLES = 3743;
public static final int ERROR_CODE_SOLITARY_STOP_POSITION = 3751;
public static final int ERROR_CODE_PLATFORM_PART_OF_HIGHWAY = 3752;
public static final int ERROR_CODE_STOP_NOT_SERVED = 3753;
public static final int ERROR_CODE_STOP_BY_STOP = 3754;
public static final int ERROR_CODE_NOT_PART_OF_STOP_AREA = 3761;
public static final int ERROR_CODE_STOP_AREA_NO_STOPS = 3762;
public static final int ERROR_CODE_STOP_AREA_NO_PLATFORM = 3763;
public static final int ERROR_CODE_STOP_AREA_COMPARE_RELATIONS = 3764;
private PTAssistantLayer layer;
public PTAssistantValidatorTest() {
super(tr("Public Transport Assistant tests"),
tr("Check if route relations are compatible with public transport version 2"));
layer = PTAssistantLayer.getLayer();
DataSet.addSelectionListener(layer);
}
@Override
public void visit(Node n) {
if (n.isIncomplete()) {
return;
}
NodeChecker nodeChecker = new NodeChecker(n, this);
// select only stop_positions
if (n.hasTag("public_transport", "stop_position")) {
// check if stop positions are on a way:
nodeChecker.performSolitaryStopPositionTest();
if (Main.pref.getBoolean("pt_assistant.stop-area-tests", true) == true) {
// check if stop positions are in any stop_area relation:
nodeChecker.performNodePartOfStopAreaTest();
}
}
// select only platforms
if (n.hasTag("public_transport", "platform")) {
// check that platforms are not part of any way:
nodeChecker.performPlatformPartOfWayTest();
if (Main.pref.getBoolean("pt_assistant.stop-area-tests", true) == true) {
// check if platforms are in any stop_area relation:
nodeChecker.performNodePartOfStopAreaTest();
}
}
this.errors.addAll(nodeChecker.getErrors());
}
@Override
public void visit(Relation r) {
// Download incomplete members. If the download does not work, return
// and do not do any testing.
if (r.hasIncompleteMembers()) {
boolean downloadSuccessful = this.downloadIncompleteMembers();
if (!downloadSuccessful) {
return;
}
}
if (r.hasIncompleteMembers()) {
return;
}
// Do some testing on stop area relations
if (Main.pref.getBoolean("pt_assistant.stop-area-tests", true) == true && StopUtils.isStopArea(r)) {
StopChecker stopChecker = new StopChecker(r, this);
// Check if stop area relation has one stop position.
stopChecker.performStopAreaStopPositionTest();
// Check if stop area relation has one platform.
stopChecker.performStopAreaPlatformTest();
// Check if stop position(s) belong the same route relation as
// related platform(s)
stopChecker.performStopAreaRelationsTest();
// Attach thrown errors
this.errors.addAll(stopChecker.getErrors());
}
if (!RouteUtils.isTwoDirectionRoute(r)) {
return;
}
// Check individual ways using the oneway direction test and the road
// type test:
WayChecker wayChecker = new WayChecker(r, this);
wayChecker.performDirectionTest();
wayChecker.performRoadTypeTest();
this.errors.addAll(wayChecker.getErrors());
proceedWithSorting(r);
// This allows to modify the route before the sorting and
// SegmentChecker are carried out:
// if (this.errors.isEmpty()) {
// proceedWithSorting(r);
// } else {
// this.proceedAfterWayCheckerErrors(r);
// }
}
/**
* Downloads incomplete relation members in an extra thread (user input
* required)
*
* @return true if successful, false if not successful
*/
private boolean downloadIncompleteMembers() {
final int[] userSelection = { 0 };
try {
if (SwingUtilities.isEventDispatchThread()) {
userSelection[0] = showIncompleteMembersDownloadDialog();
} else {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
try {
userSelection[0] = showIncompleteMembersDownloadDialog();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
} catch (InterruptedException | InvocationTargetException e) {
return false;
}
if (userSelection[0] == JOptionPane.YES_OPTION) {
Thread t = new IncompleteMembersDownloadThread();
t.start();
synchronized (t) {
try {
t.wait();
} catch (InterruptedException e) {
return false;
}
}
}
return true;
}
/**
* Shows the dialog asking the user about an incomplete member download
*
* @return user's selection
* @throws InterruptedException
* if interrupted
*/
private int showIncompleteMembersDownloadDialog() throws InterruptedException {
if (Main.pref.getBoolean("pt_assistant.download-incomplete", false) == true) {
return JOptionPane.YES_OPTION;
}
if (Main.pref.getBoolean("pt_assistant.download-incomplete", false) == false) {
return JOptionPane.NO_OPTION;
}
IncompleteMembersDownloadDialog incompleteMembersDownloadDialog = new IncompleteMembersDownloadDialog();
return incompleteMembersDownloadDialog.getUserSelection();
}
/**
* Gets user input after errors were detected by WayChecker. Although this
* method is not used in the current implementation, it can be used to fix
* errors from the previous testing stage and modify the route before the
* second stage of testing is carried out.
*/
@SuppressWarnings("unused")
private void proceedAfterWayCheckerErrors(Relation r) {
// count errors of each type:
int numberOfDirectionErrors = 0;
int numberOfRoadTypeErrors = 0;
for (TestError e : this.errors) {
if (e.getCode() == ERROR_CODE_DIRECTION) {
numberOfDirectionErrors++;
}
if (e.getCode() == ERROR_CODE_ROAD_TYPE) {
numberOfRoadTypeErrors++;
}
}
final int[] userInput = { 0 };
final long idParameter = r.getId();
final int directionErrorParameter = numberOfDirectionErrors;
final int roadTypeErrorParameter = numberOfRoadTypeErrors;
if (SwingUtilities.isEventDispatchThread()) {
userInput[0] = showProceedDialog(idParameter, directionErrorParameter, roadTypeErrorParameter);
} else {
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
userInput[0] = showProceedDialog(idParameter, directionErrorParameter, roadTypeErrorParameter);
}
});
} catch (InvocationTargetException | InterruptedException e1) {
e1.printStackTrace();
}
}
if (userInput[0] == 0) {
this.fixErrorFromPlugin(this.errors);
proceedWithSorting(r);
return;
}
if (userInput[0] == 1) {
JOptionPane.showMessageDialog(null, "This is not implemented yet!");
return;
}
if (userInput[0] == 2) {
proceedWithSorting(r);
}
// if userInput==-1 (i.e. no input), do nothing and stop testing of the
// route.
}
private int showProceedDialog(long id, int numberOfDirectionErrors, int numberOfRoadTypeErrors) {
if (numberOfDirectionErrors == 0 && numberOfRoadTypeErrors == 0) {
return 2;
}
if (Main.pref.getBoolean("pt_assistant.proceed-without-fix", true) == false) {
return 0;
}
if (Main.pref.getBoolean("pt_assistant.proceed-without-fix", true) == true) {
return 2;
}
ProceedDialog proceedDialog = new ProceedDialog(id, numberOfDirectionErrors, numberOfRoadTypeErrors);
return proceedDialog.getUserSelection();
}
/**
* Carries out the second stage of the testing: sorting
*
* @param r
* relation
*/
private void proceedWithSorting(Relation r) {
// Check if the relation is correct, or only has a wrong sorting order:
RouteChecker routeChecker = new RouteChecker(r, this);
routeChecker.performSortingTest();
List<TestError> routeCheckerErrors = routeChecker.getErrors();
/*- At this point, there are 3 variants:
*
* 1) There are no errors => route is correct
* 2) There is only a sorting error (can only be 1), but otherwise
* correct.
* 3) There are some other errors/gaps that cannot be fixed by
* sorting => start further test (stop-by-stop)
*
* */
if (!routeCheckerErrors.isEmpty()) {
// Variant 2
// If there is only the sorting error, add it
this.errors.addAll(routeChecker.getErrors());
}
// if (!routeChecker.getHasGap()) {
// // Variant 1
// storeCorrectRouteSegments(r);
// }
// Variant 3:
proceedAfterSorting(r);
}
/**
* Carries out the stop-by-stop testing which includes building the route
* data model.
*
* @param r
* route relation
*/
private void proceedAfterSorting(Relation r) {
SegmentChecker segmentChecker = new SegmentChecker(r, this);
// Check if the creation of the route data model in the segment checker
// worked. If it did not, it means the roles in the route relation do
// not match the tags of the route members.
if (!segmentChecker.getErrors().isEmpty()) {
this.errors.addAll(segmentChecker.getErrors());
}
segmentChecker.performFirstStopTest();
segmentChecker.performLastStopTest();
segmentChecker.performStopNotServedTest();
boolean sortingErrorFound = false;
for (TestError error : this.errors) {
if (error.getCode() == ERROR_CODE_SORTING) {
sortingErrorFound = true;
break;
}
}
if (!sortingErrorFound) {
segmentChecker.performStopByStopTest();
segmentChecker.findFixes();
}
for (TestError error : segmentChecker.getErrors()) {
if (error.getCode() != PTAssistantValidatorTest.ERROR_CODE_RELAITON_MEMBER_ROLES) {
this.errors.add(error);
}
}
}
/**
* Method is called after all primitives has been visited, overrides the
* method of the superclass.
*/
public void endTest() {
// modify the error messages for the stop-by-stop test:
SegmentChecker.modifyStopByStopErrorMessages();
// add the stop-by-stop errors with modified messages:
for (Entry<Builder, PTRouteSegment> entry : SegmentChecker.wrongSegmentBuilders.entrySet()) {
TestError error = entry.getKey().build();
SegmentChecker.wrongSegments.put(error, entry.getValue());
this.errors.add(error);
}
// reset the static collections in SegmentChecker:
SegmentChecker.reset();
super.endTest();
}
/**
* Creates the PTRouteSegments of a route that has been found correct and
* stores them in the list of correct route segments
*
* @param r
* route relation
*/
@SuppressWarnings("unused")
private void storeCorrectRouteSegments(Relation r) {
PTRouteDataManager manager = new PTRouteDataManager(r);
StopToWayAssigner assigner = new StopToWayAssigner(manager.getPTWays());
if (manager.getPTStops().size() > 1) {
for (int i = 1; i < manager.getPTStops().size(); i++) {
PTStop segmentStartStop = manager.getPTStops().get(i - 1);
PTStop segmentEndStop = manager.getPTStops().get(i);
Way segmentStartWay = assigner.get(segmentStartStop);
Way segmentEndWay = assigner.get(segmentEndStop);
List<PTWay> waysBetweenStops = manager.getPTWaysBetween(segmentStartWay, segmentEndWay);
PTRouteSegment routeSegment = new PTRouteSegment(segmentStartStop, segmentEndStop, waysBetweenStops, r);
SegmentChecker.addCorrectSegment(routeSegment);
}
}
}
/**
* Checks if the test error is fixable
*/
@Override
public boolean isFixable(TestError testError) {
if (testError.getCode() == ERROR_CODE_DIRECTION || testError.getCode() == ERROR_CODE_ROAD_TYPE
|| testError.getCode() == ERROR_CODE_CONSTRUCTION || testError.getCode() == ERROR_CODE_SORTING
|| testError.getCode() == PTAssistantValidatorTest.ERROR_CODE_SOLITARY_STOP_POSITION
|| testError.getCode() == PTAssistantValidatorTest.ERROR_CODE_PLATFORM_PART_OF_HIGHWAY) {
return true;
}
if (testError.getCode() == ERROR_CODE_STOP_BY_STOP && SegmentChecker.isFixable(testError)) {
return true;
}
return false;
}
/**
* Fixes the given error
*/
@Override
public Command fixError(TestError testError) {
// repaint the relation in the pt_assistant layer:
if (testError.getPrimitives().iterator().next().getType().equals(OsmPrimitiveType.RELATION)) {
Relation relationToBeFixed = (Relation) testError.getPrimitives().iterator().next();
this.layer.repaint(relationToBeFixed);
}
// reset the last fix:
PTAssistantPlugin.setLastFix(null);
List<Command> commands = new ArrayList<>();
if (testError.getCode() == ERROR_CODE_ROAD_TYPE || testError.getCode() == ERROR_CODE_CONSTRUCTION) {
commands.add(WayChecker.fixErrorByZooming(testError));
}
if (testError.getCode() == ERROR_CODE_DIRECTION) {
commands.add(WayChecker.fixErrorByZooming(testError));
}
if (testError.getCode() == ERROR_CODE_SORTING) {
commands.add(RouteChecker.fixSortingError(testError));
}
if (testError.getCode() == ERROR_CODE_SOLITARY_STOP_POSITION
|| testError.getCode() == ERROR_CODE_PLATFORM_PART_OF_HIGHWAY) {
commands.add(NodeChecker.fixError(testError));
}
if (testError.getCode() == ERROR_CODE_STOP_BY_STOP) {
commands.add(SegmentChecker.fixError(testError));
// make sure the primitives of this testError are selected:
Collection<OsmPrimitive> primitivesToSelect = new ArrayList<>();
for (Object obj : testError.getPrimitives()) {
primitivesToSelect.add((OsmPrimitive) obj);
}
SelectCommand selectCommand = new SelectCommand(primitivesToSelect);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
selectCommand.executeCommand();
}
});
}
if (commands.isEmpty()) {
return null;
}
if (commands.size() == 1) {
return commands.get(0);
}
return new SequenceCommand(tr("Fix error"), commands);
}
/**
* This method is the counterpart of the fixError(TestError testError)
* method. The fixError method is invoked from the core validator (e.g. when
* user presses the "Fix" button in the validator). This method is invoken
* when the fix is initiated from within the plugin (e.g. automated fixes).
*/
private void fixErrorFromPlugin(List<TestError> testErrors) {
// run fix task asynchronously
FixTask fixTask = new FixTask(testErrors);
Thread t = new Thread(fixTask);
t.start();
try {
t.join();
errors.removeAll(testErrors);
} catch (InterruptedException e) {
JOptionPane.showMessageDialog(null, "Error occurred during fixing");
}
}
public void addFixVariants(List<List<PTWay>> fixVariants) {
layer.addFixVariants(fixVariants);
}
public void clearFixVariants() {
layer.clearFixVariants();
}
public List<PTWay> getFixVariant(Character c) {
return layer.getFixVariant(c);
}
@SuppressWarnings("unused")
private void performDummyTest(Relation r) {
List<Relation> primitives = new ArrayList<>(1);
primitives.add(r);
Builder builder = TestError.builder(this, Severity.WARNING, ERROR_CODE_DIRECTION);
builder.message(tr("PT: dummy test warning"));
builder.primitives(primitives);
errors.add(builder.build());
}
}