diff --git a/gradle.properties b/gradle.properties index 914aa89a..98c7099d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,6 @@ plugin.canloadatruntime=true plugin.icon=images/bus.svg # Minimum JOSM version (any numeric version) -plugin.main.version=15238 +plugin.main.version=15418 # JOSM version to compile against (any numeric version available for download, or the special values "tested" or "latest") plugin.compile.version=17084 diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/PTAssistantPlugin.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/PTAssistantPlugin.java index 43a1c058..a741c775 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/PTAssistantPlugin.java +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/PTAssistantPlugin.java @@ -39,6 +39,7 @@ import org.openstreetmap.josm.plugins.pt_assistant.actions.SortPTRouteMembersAction; import org.openstreetmap.josm.plugins.pt_assistant.actions.SortPTRouteMembersMenuBar; import org.openstreetmap.josm.plugins.pt_assistant.actions.SplitRoundaboutAction; +import org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.RouteExplorer; import org.openstreetmap.josm.plugins.pt_assistant.data.PTRouteSegment; import org.openstreetmap.josm.plugins.pt_assistant.gui.PTAssistantLayerManager; import org.openstreetmap.josm.plugins.pt_assistant.validation.BicycleFootRouteValidatorTest; @@ -79,6 +80,7 @@ public PTAssistantPlugin(PluginInformation info) { .addMenu("File", trc("menu", "Public Transport"), KeyEvent.VK_P, 5, ht("/Menu/Public Transport")); addToMenu(PublicTransportMenu); + SelectionEventManager.getInstance().addSelectionListener(new RouteExplorer()); SelectionEventManager.getInstance().addSelectionListener(PTAssistantLayerManager.PTLM); KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener(PTAssistantLayerManager.PTLM); initialiseWizard(); diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/RouteExplorer.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/RouteExplorer.java new file mode 100644 index 00000000..7686cce3 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/RouteExplorer.java @@ -0,0 +1,179 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.pt_assistant.routeexplorer; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.stream.Collectors; + +import javax.swing.JOptionPane; + +import com.drew.lang.annotations.NotNull; +import org.openstreetmap.josm.data.osm.AbstractPrimitive; +import org.openstreetmap.josm.data.osm.DataSelectionListener; +import org.openstreetmap.josm.data.osm.Relation; +import org.openstreetmap.josm.data.osm.RelationMember; +import org.openstreetmap.josm.gui.MainApplication; +import org.openstreetmap.josm.gui.MapFrame; +import org.openstreetmap.josm.gui.Notification; +import org.openstreetmap.josm.gui.dialogs.relation.DownloadRelationMemberTask; +import org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.transportmode.ITransportMode; +import org.openstreetmap.josm.plugins.pt_assistant.utils.DialogUtils; +import org.openstreetmap.josm.plugins.pt_assistant.utils.RouteUtils; +import org.openstreetmap.josm.plugins.pt_assistant.utils.WayUtils; +import org.openstreetmap.josm.tools.I18n; + +public class RouteExplorer implements DataSelectionListener { + @NotNull + private Optional activeTransportMode = Optional.empty(); + + private final RouteExplorerPanel routingHelperPanel = new RouteExplorerPanel(this); + + @NotNull + private Optional currentRelation = Optional.empty(); + + @NotNull + private Optional currentMember = Optional.empty(); + + @Override + public void selectionChanged(final SelectionChangeEvent event) { + final MapFrame mapframe = MainApplication.getMap(); + if (mapframe != null) { + final Optional singleRelationSelection = Optional.of(event.getSelection()) + .filter(selection -> selection.size() == 1) + .map(selection -> selection.iterator().next()) + .map(selectedPrimitive -> selectedPrimitive instanceof Relation ? (Relation) selectedPrimitive : null) + .filter(RouteUtils::isRoute); + this.currentRelation = singleRelationSelection; + this.activeTransportMode = currentRelation.flatMap(relation -> ITransportMode.TRANSPORT_MODES.stream().filter(it -> it.canBeUsedForRelation(relation)).findFirst()); + if (singleRelationSelection.isPresent()) { + routingHelperPanel.onRelationChange(singleRelationSelection.get(), activeTransportMode); + if (mapframe.getTopPanel(RouteExplorerPanel.class) == null) { + mapframe.addTopPanel(routingHelperPanel); + } + } else { + mapframe.removeTopPanel(RouteExplorerPanel.class); + } + } + } + + public void goToFirstWay() { + final Optional currentRelation = this.currentRelation; + final long missingMembersCount = currentRelation + .map(it -> + it.getMembers().stream() + .filter(member -> member.getMember().isIncomplete()) + .count() + ) + .orElse(0L); + if (missingMembersCount > 0) { + if ( + DialogUtils.showYesNoQuestion( + routingHelperPanel, + I18n.tr("Relation is incomplete"), + I18n.trn( + "The relations has {0} missing member. Would you like to download the missing member now?", + "The relations has {0} missing members. Would you like to download the missing members now?", + missingMembersCount, + missingMembersCount + ) + ) + ) { + final Future f = MainApplication.worker.submit(new DownloadRelationMemberTask( + currentRelation.get(), + currentRelation.get().getMembers().stream() + .map(RelationMember::getMember) + .filter(AbstractPrimitive::isIncomplete) + .collect(Collectors.toSet()), + MainApplication.getLayerManager().getActiveDataLayer() + )); + new Thread(() -> { + try { + f.get(); + + // try again, now the missingMembersCount should be 0, so we should go to the else-branch this time + goToFirstWay(); + } catch (CancellationException | InterruptedException | ExecutionException e) { + JOptionPane.showMessageDialog( + routingHelperPanel, + I18n.tr("The download of missing members has failed!"), + I18n.tr("Download failed"), + JOptionPane.ERROR_MESSAGE + ); + } + + }).start(); + } + } else { + final List wayMembers = currentRelation.map(relation -> relation.getMembers().stream().filter(RouteUtils::isRouteWayMember).collect(Collectors.toList())).orElse(Collections.emptyList()); + this.currentMember = wayMembers.stream().findFirst(); + if (wayMembers.isEmpty()) { + JOptionPane.showMessageDialog(routingHelperPanel, "No way found to traverse", "Could not find a way to traverse", JOptionPane.ERROR_MESSAGE); + } else { + routingHelperPanel.onCurrentWayChange( + currentRelation.get(), + wayMembers.get(0), + RouteExplorerPanel.ConnectionType.END, + wayMembers.size() == 1 + ? RouteExplorerPanel.ConnectionType.END + : ( + WayUtils.isTouchingOtherWay(wayMembers.get(0).getWay(), wayMembers.get(1).getWay()) + ? RouteExplorerPanel.ConnectionType.CONNECTED + : RouteExplorerPanel.ConnectionType.NOT_CONNECTED + ), + 0, + wayMembers.size() + ); + } + } + } + + public void goToPreviousGap() { + JOptionPane.showMessageDialog(routingHelperPanel, "Not implemented yet", "Not implemented", JOptionPane.ERROR_MESSAGE); + } + + public void goToPreviousWay() { + goNWaysForward(-1); + } + + public void goToNextWay() { + goNWaysForward(1); + } + + private void goNWaysForward(final int n) { + currentRelation.ifPresent(relation -> + currentMember.ifPresent(member -> { + final List wayMembers = relation.getMembers().stream().filter(RouteUtils::isRouteWayMember).collect(Collectors.toList()); + final int targetIndex = wayMembers.indexOf(member) + n; + if (targetIndex < 0 || targetIndex >= wayMembers.size() - 1) { + new Notification(I18n.tr("You reached the end of the route")).setIcon(JOptionPane.INFORMATION_MESSAGE).setDuration(Notification.TIME_SHORT).show(); + } else { + currentMember = Optional.of(wayMembers.get(targetIndex)); + routingHelperPanel.onCurrentWayChange( + relation, + wayMembers.get(targetIndex), + targetIndex <= 0 ? RouteExplorerPanel.ConnectionType.END : ( + WayUtils.isTouchingOtherWay(wayMembers.get(targetIndex).getWay(), wayMembers.get(targetIndex - 1).getWay()) + ? RouteExplorerPanel.ConnectionType.CONNECTED + : RouteExplorerPanel.ConnectionType.NOT_CONNECTED + ), + targetIndex >= wayMembers.size() - 1 ? RouteExplorerPanel.ConnectionType.END : ( + WayUtils.isTouchingOtherWay(wayMembers.get(targetIndex).getWay(), wayMembers.get(targetIndex + 1).getWay()) + ? RouteExplorerPanel.ConnectionType.CONNECTED + : RouteExplorerPanel.ConnectionType.NOT_CONNECTED + ), + targetIndex, + wayMembers.size() + ); + } + }) + ); + } + + public void goToNextGap() { + JOptionPane.showMessageDialog(routingHelperPanel, "Not implemented yet", "Not implemented", JOptionPane.ERROR_MESSAGE); + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/RouteExplorerPanel.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/RouteExplorerPanel.java new file mode 100644 index 00000000..dc657e7b --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/RouteExplorerPanel.java @@ -0,0 +1,206 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.pt_assistant.routeexplorer; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.util.Collections; +import java.util.Optional; + +import javax.swing.AbstractAction; +import javax.swing.BoxLayout; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import org.openstreetmap.josm.data.osm.Relation; +import org.openstreetmap.josm.data.osm.RelationMember; +import org.openstreetmap.josm.gui.MainApplication; +import org.openstreetmap.josm.gui.MapFrame; +import org.openstreetmap.josm.gui.dialogs.relation.GenericRelationEditor; +import org.openstreetmap.josm.gui.util.HighlightHelper; +import org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.transportmode.ITransportMode; +import org.openstreetmap.josm.plugins.pt_assistant.utils.BoundsUtils; +import org.openstreetmap.josm.plugins.pt_assistant.utils.ColorPalette; +import org.openstreetmap.josm.plugins.pt_assistant.utils.GuiUtils; +import org.openstreetmap.josm.plugins.pt_assistant.utils.PTIcons; +import org.openstreetmap.josm.tools.I18n; +import org.openstreetmap.josm.tools.ImageProvider; + +/** + * The top panel that is added via {@link MapFrame#addTopPanel(Component)}. + * This class should just handle the display and input. The state of the routing helper should be handled in {@link RouteExplorer}. + */ +public class RouteExplorerPanel extends JPanel { + + private final HighlightHelper highlighter = new HighlightHelper(); + + private final JLabel relationLabel = GuiUtils.createJLabel(); + + private final JLabel activeWayLabel = GuiUtils.createJLabel(true); + private final JLabel previousWayConnectionLabel = GuiUtils.createJLabel(); + private final JLabel nextWayConnectionLabel = GuiUtils.createJLabel(); + private final JButton openRelationEditorButton = new JButton(); + + private final JPanel containerPanel = GuiUtils.createJPanel(new BorderLayout()); + + private final JPanel defaultPanel; + + private final JPanel wayTraversalPanel; + + public RouteExplorerPanel(@NotNull final RouteExplorer routingHelperAction) { + this.defaultPanel = createDefaultPanel(routingHelperAction); + this.wayTraversalPanel = createWayTraversalPanel( + routingHelperAction, + activeWayLabel, + previousWayConnectionLabel, + nextWayConnectionLabel, + openRelationEditorButton + ); + + JButton closeButton = new JButton(ImageProvider.get("misc", "black_x")); + closeButton.setContentAreaFilled(false); + closeButton.setRolloverEnabled(true); + closeButton.setBorderPainted(false); + closeButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + closeButton.setToolTipText(I18n.tr("Close the routing helper")); + closeButton.addActionListener(__ -> + Optional.ofNullable(MainApplication.getMap()) + .ifPresent(map -> map.removeTopPanel(RouteExplorerPanel.class)) + ); + + final JPanel mainPanel = GuiUtils.createJPanel(new BorderLayout()); + relationLabel.setBorder(new EmptyBorder(10, 10, 10, 10)); + mainPanel.add(relationLabel, BorderLayout.CENTER); + mainPanel.add(closeButton, BorderLayout.EAST); + + // put everything together + setBackground(ColorPalette.ROUTING_HELPER_BACKGROUND); + setLayout(new BorderLayout()); + add(mainPanel, BorderLayout.CENTER); + add(containerPanel, BorderLayout.SOUTH); + } + + public void onRelationChange(@Nullable final Relation relation, @NotNull Optional activeTransportMode) { + relationLabel.setIcon(activeTransportMode.map(it -> it.getIcon().setSize(ImageProvider.ImageSizes.SMALLICON).get()).orElse(null)); + relationLabel.setText(relation == null ? "‹no relation selected›" : "Relation #" + relation.getId()); + onCurrentWayCleared(); + } + + public void onCurrentWayCleared() { + highlighter.clear(); + activeWayLabel.setText(""); + previousWayConnectionLabel.setText(""); + nextWayConnectionLabel.setText(""); + updateContainerPanel(defaultPanel); + } + + public void onCurrentWayChange( + @NotNull final Relation parentRelation, + @NotNull final RelationMember member, + @NotNull final ConnectionType previousWayConnection, + @NotNull final ConnectionType nextWayConnection, + final int currentWayIndex, + final int numWaysTotal + ) { + highlighter.highlightOnly(member.getMember()); + Optional.ofNullable(MainApplication.getMap()).ifPresent(map -> map.mapView.zoomTo(BoundsUtils.fromBBox(member.getMember().getBBox()))); + activeWayLabel.setText("Way" + (currentWayIndex + 1) + '/' + numWaysTotal + ": #" + member.getMember().getId()); + previousWayConnectionLabel.setIcon(previousWayConnection.icon); + previousWayConnectionLabel.setText(previousWayConnection.previousWayText); + nextWayConnectionLabel.setIcon(nextWayConnection.icon); + nextWayConnectionLabel.setText(nextWayConnection.nextWayText); + openRelationEditorButton.setAction(new AbstractAction(I18n.tr("Open in relation editor")) { + @Override + public void actionPerformed(ActionEvent __) { + new GenericRelationEditor(MainApplication.getLayerManager().getActiveDataLayer(), parentRelation, Collections.singleton(member)).setVisible(true); + } + }); + updateContainerPanel(wayTraversalPanel); + } + + private void updateContainerPanel(final JPanel newPanel) { + if (containerPanel.getComponentCount() != 1 || containerPanel.getComponent(0) != newPanel) { + containerPanel.removeAll(); + containerPanel.add(newPanel, BorderLayout.CENTER); + containerPanel.revalidate(); + containerPanel.repaint(); + } + } + + /** + * Initializes the panel shown when traversing the ways one by one + */ + private static JPanel createWayTraversalPanel( + final RouteExplorer routingHelperAction, + final JLabel activeWayLabel, + final JLabel previousWayConnectionLabel, + final JLabel nextWayConnectionLabel, + final JButton openRelationEditorButton + ) { + final JPanel wayTraversalPanel = GuiUtils.createJPanel(new BorderLayout()); + + // Left side: buttons to navigate backwards + wayTraversalPanel.add( + GuiUtils.createJPanel( + new FlowLayout(FlowLayout.LEFT), + GuiUtils.createJButton("‹ to previous way", routingHelperAction::goToPreviousWay) + ), + BorderLayout.WEST + ); + + // Center: label for active element + final JPanel activeWayPanel = new JPanel(); + activeWayPanel.setOpaque(false); + activeWayPanel.setLayout(new BoxLayout(activeWayPanel, BoxLayout.PAGE_AXIS)); + + activeWayPanel.add(activeWayLabel); + activeWayPanel.add(previousWayConnectionLabel); + activeWayPanel.add(nextWayConnectionLabel); + activeWayPanel.add(openRelationEditorButton); + wayTraversalPanel.add(activeWayPanel, BorderLayout.CENTER); + + // Right side: buttons to navigate forwards + wayTraversalPanel.add( + GuiUtils.createJPanel( + new FlowLayout(FlowLayout.RIGHT), + GuiUtils.createJButton("to next way ›", routingHelperAction::goToNextWay) + ), + BorderLayout.EAST + ); + + return wayTraversalPanel; + } + + private static JPanel createDefaultPanel(final RouteExplorer routingHelperAction) { + return GuiUtils.createJPanel( + new FlowLayout(FlowLayout.CENTER), + GuiUtils.createJButton( + I18n.tr("Start way traversal"), + routingHelperAction::goToFirstWay + ) + ); + } + + public enum ConnectionType { + CONNECTED(PTIcons.GREEN_CHECKMARK, I18n.tr("connected with previous way"), I18n.tr("connected with next way")), + END(PTIcons.BUFFER_STOP, I18n.tr("first way in the relation"), I18n.tr("last way in the relation")), + NOT_CONNECTED(PTIcons.STOP_SIGN, I18n.tr("not connected with previous way"), I18n.tr("not connected with next way")); + + final Icon icon; + final String previousWayText; + final String nextWayText; + + ConnectionType(final ImageProvider imageProvider, final String previousWayText, final String nextWayText) { + this.icon = imageProvider.setSize(ImageProvider.ImageSizes.SMALLICON).get(); + this.previousWayText = previousWayText; + this.nextWayText = nextWayText; + } + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/WayTraversalDirection.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/WayTraversalDirection.java new file mode 100644 index 00000000..d4f1bdf4 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/WayTraversalDirection.java @@ -0,0 +1,49 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.pt_assistant.routeexplorer; + +import java.util.function.Function; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import org.openstreetmap.josm.data.osm.INode; +import org.openstreetmap.josm.data.osm.IWay; +import org.openstreetmap.josm.data.osm.Way; + +public enum WayTraversalDirection { + FORWARD(IWay::firstNode, IWay::lastNode), + BACKWARD(IWay::lastNode, IWay::firstNode); + + private final Function, INode> startNodeGetter; + private final Function, INode> endNodeGetter; + + WayTraversalDirection( + @NotNull final Function, INode> startNodeGetter, + @NotNull final Function, INode> endNodeGetter + ) { + this.startNodeGetter = startNodeGetter; + this.endNodeGetter = endNodeGetter; + } + + /** + * Finds the first node that you come across when traversin the given way. + * + * @param way the way for which the first or last node is returned, depending on the direction + * @return {@link #FORWARD} returns the {@link Way#firstNode()}, {@link #BACKWARD} returns the {@link Way#lastNode()} + */ + @Nullable + public INode getStartNodeFor(@NotNull final IWay way) { + return startNodeGetter.apply(way); + } + + + /** + * Finds the node where traversal of the given way ends. + * + * @param way the way for which the first or last node is returned, depending on the direction + * @return {@link #FORWARD} returns the {@link Way#lastNode()}, {@link #BACKWARD} returns the {@link Way#firstNode()} + */ + @Nullable + public INode getEndNodeFor(@NotNull final IWay way) { + return endNodeGetter.apply(way); + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/AbstractTransportMode.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/AbstractTransportMode.java new file mode 100644 index 00000000..6278077f --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/AbstractTransportMode.java @@ -0,0 +1,98 @@ +package org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.transportmode; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import com.drew.lang.annotations.NotNull; +import org.openstreetmap.josm.data.osm.IRelation; +import org.openstreetmap.josm.data.osm.IWay; +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.plugins.pt_assistant.routeexplorer.WayTraversalDirection; +import org.openstreetmap.josm.plugins.pt_assistant.utils.WayUtils; + +public abstract class AbstractTransportMode implements ITransportMode { + String modeOfTransport = ""; + String[] additionalTypesForTurnRestriction; + String[] turnRestrictionExceptionsFor; + String[] oneWayExceptionsFor; + + @Override + public boolean canBeUsedForRelation(@NotNull final IRelation relation) { + return relation.hasTag("type", "route"); + } + + @Override + public boolean canTraverseWay(@NotNull final IWay way, @NotNull final WayTraversalDirection direction) { + final String oneway = way.get("oneway"); + return oneway == null + || Arrays.stream(oneWayExceptionsFor) + .map(mode -> way.get("oneway:" + mode)) + .anyMatch("no"::equals) + || ("yes".equals(oneway) && direction == WayTraversalDirection.FORWARD) + || ("-1".equals(oneway) && direction == WayTraversalDirection.BACKWARD); + } + + @Override + public boolean canTurn(@NotNull final Way from, @NotNull final Node via, @NotNull final Way to) { + List types = new java.util.ArrayList<>(Collections.singletonList("restriction")); + Arrays.stream(additionalTypesForTurnRestriction).map(at -> "restriction:" + at).forEach(types::add); + final boolean bothWaysAreConnectedAtViaNode = from.containsNode(via) && to.containsNode(via); + for (Relation restrictionRelation : getTurnRestrictions(from, via, to, types)) { + for (String type : types) { + final String restriction = restrictionRelation.get(type); + if (restriction != null && restriction.startsWith("no_")) { + final String except = !"".equals(modeOfTransport) ? restrictionRelation.get("except") : ""; + if (except != null) { + for (String tre : turnRestrictionExceptionsFor) { + if (except.contains(tre)) { + return bothWaysAreConnectedAtViaNode; + } + } + } + return false; + } + } + } + return bothWaysAreConnectedAtViaNode; + } + + @Override + public boolean canTurn(@NotNull final Way from, @NotNull final Way via, @NotNull final Way to) { + List types = new java.util.ArrayList<>(Collections.singletonList("restriction")); + Arrays.stream(additionalTypesForTurnRestriction).map(at -> "restriction:" + at).forEach(types::add); + for (Relation restrictionRelation : getTurnRestrictions(from, via, to, types)) { + for (String type : types) { + final String restriction = restrictionRelation.get(type); + if (restriction != null && restriction.startsWith("no_")) { + final String except = !"".equals(modeOfTransport) ? restrictionRelation.get("except") : ""; + if (except != null) { + return Arrays.stream(turnRestrictionExceptionsFor).anyMatch(except::contains); + } + return false; + } + } + } + return true; + } + + private Set getTurnRestrictions(Way from, OsmPrimitive via, Way to, List types) { + return from.getReferrers().stream() + .map(it -> it.getType() == OsmPrimitiveType.RELATION ? (Relation) it : null) + .filter(relation -> + relation != null && types.contains(relation.get("type")) + && relation.findRelationMembers("from").contains(from) + && relation.findRelationMembers("via").contains(via) + && relation.findRelationMembers("to").contains(to) + ).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/BicycleTransportMode.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/BicycleTransportMode.java new file mode 100644 index 00000000..9c51da65 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/BicycleTransportMode.java @@ -0,0 +1,54 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.transportmode; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.drew.lang.annotations.NotNull; +import org.openstreetmap.josm.data.osm.IRelation; +import org.openstreetmap.josm.data.osm.IWay; +import org.openstreetmap.josm.data.osm.Node; +import org.openstreetmap.josm.data.osm.Way; +import org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.WayTraversalDirection; +import org.openstreetmap.josm.plugins.pt_assistant.utils.PTIcons; +import org.openstreetmap.josm.tools.I18n; +import org.openstreetmap.josm.tools.ImageProvider; + +public class BicycleTransportMode extends AbstractTransportMode { + + private static final List suitableHighways = Stream.concat( + // This list is ordered from most suitable to least suitable + Stream.of("cycleway", "cyclestreet", "path", "residential", "unclassified", "service", "track", "living_street"), + Stream.of("tertiary", "secondary", "primary", "trunk").flatMap(it -> Stream.of(it, it + "_link")) + ).collect(Collectors.toList()); + + public BicycleTransportMode() { + // should only be instantiable in `ITransportMode` + modeOfTransport = "bicycle"; + additionalTypeForTurnRestriction = "bicycle"; + oneWayExceptionFor = "bicycle"; + } + + @Override + public boolean canTraverseWay(@NotNull final IWay way, @NotNull final WayTraversalDirection direction) { + return !way.hasTag(modeOfTransport, "no") + && (way.hasTag("highway", suitableHighways) || way.hasTag(modeOfTransport, "yes")) + && super.canTraverseWay(way, direction); + } + + @Override + public boolean canBeUsedForRelation(@NotNull final IRelation relation) { + return relation.hasTag("route", modeOfTransport) && super.canBeUsedForRelation(relation); + } + + @Override + public ImageProvider getIcon() { + return PTIcons.BICYCLE; + } + + @Override + public String getName() { + return I18n.marktr("bicycle"); + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/BusTransportMode.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/BusTransportMode.java new file mode 100644 index 00000000..4eace45e --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/BusTransportMode.java @@ -0,0 +1,53 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.transportmode; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.drew.lang.annotations.NotNull; +import org.openstreetmap.josm.data.osm.IRelation; +import org.openstreetmap.josm.data.osm.IWay; +import org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.WayTraversalDirection; +import org.openstreetmap.josm.plugins.pt_assistant.utils.PTIcons; +import org.openstreetmap.josm.tools.I18n; +import org.openstreetmap.josm.tools.ImageProvider; + +public class BusTransportMode extends AbstractTransportMode { + + private static final List suitableHighways = Stream.concat( + Stream.of("unclassified", "residential", "service", "living_street", "cyclestreet"), + Stream.of("tertiary", "secondary", "primary", "trunk", "motorway").flatMap(it -> Stream.of(it, it + "_link")) + ).collect(Collectors.toList()); + + protected BusTransportMode() { + // should only be instantiable in `ITransportMode` + modeOfTransport = "bus"; + additionalTypesForTurnRestriction = new String[]{"bus", "psv"}; + turnRestrictionExceptionsFor = new String[]{"bus", "psv"}; + oneWayExceptionsFor = new String[]{"bus", "psv"}; + } + + @Override + public boolean canTraverseWay(@NotNull final IWay way, @NotNull final WayTraversalDirection direction) { + return ( way.hasTag("highway", suitableHighways) + || way.hasTag("psv", "yes") + || way.hasTag ("bus", "yes")) + && super.canTraverseWay(way, direction); + } + + @Override + public boolean canBeUsedForRelation(@NotNull final IRelation relation) { + return relation.hasTag("route", "bus", "coach", "minibus") && super.canBeUsedForRelation(relation); + } + + @Override + public ImageProvider getIcon() { + return PTIcons.BUS; + } + + @Override + public String getName() { + return I18n.marktr("bus"); + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/HorseTransportMode.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/HorseTransportMode.java new file mode 100644 index 00000000..68a123c8 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/HorseTransportMode.java @@ -0,0 +1,59 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.transportmode; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.drew.lang.annotations.NotNull; +import org.openstreetmap.josm.data.osm.IRelation; +import org.openstreetmap.josm.data.osm.IWay; +import org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.WayTraversalDirection; +import org.openstreetmap.josm.plugins.pt_assistant.utils.PTIcons; +import org.openstreetmap.josm.tools.I18n; +import org.openstreetmap.josm.tools.ImageProvider; + +public class HorseTransportMode extends AbstractTransportMode { + + private static final List suitableHighways = Stream.concat( + // This list is ordered from most suitable to least suitable + Stream.of( + "bridleway", "pedestrian", "footway", "path", "track", "living_street", "residential", + "unclassified", "cyclestreet", "service", "cycleway" + ), + Stream.of("tertiary", "secondary", "primary", "trunk").flatMap(it -> Stream.of(it, it + "_link")) + ).collect(Collectors.toList()); + + protected HorseTransportMode() { + // should only be instantiable in `ITransportMode` + modeOfTransport = "horse"; + } + + @Override + public boolean canTraverseWay(@NotNull final IWay way, @NotNull final WayTraversalDirection direction) { + final String onewayValue = way.get("oneway"); + return + !way.hasTag("horse", "no") + && (way.hasTag("highway", suitableHighways) || way.hasTag("horse", "yes")) + && ( + onewayValue == null + || ("yes".equals(onewayValue) && direction == WayTraversalDirection.FORWARD) + || ("-1".equals(onewayValue) && direction == WayTraversalDirection.BACKWARD) + ); + } + + @Override + public boolean canBeUsedForRelation(@NotNull final IRelation relation) { + return relation.hasTag("route", "horse") && super.canBeUsedForRelation(relation); + } + + @Override + public ImageProvider getIcon() { + return PTIcons.HORSE; + } + + @Override + public String getName() { + return I18n.marktr("equestrian"); + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/ITransportMode.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/ITransportMode.java new file mode 100644 index 00000000..68b1df65 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/ITransportMode.java @@ -0,0 +1,79 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.transportmode; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.drew.lang.annotations.NotNull; +import org.openstreetmap.josm.data.osm.IRelation; +import org.openstreetmap.josm.data.osm.IWay; +import org.openstreetmap.josm.data.osm.Node; +import org.openstreetmap.josm.data.osm.Way; +import org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.WayTraversalDirection; +import org.openstreetmap.josm.tools.I18n; +import org.openstreetmap.josm.tools.ImageProvider; + +public interface ITransportMode { + Set TRANSPORT_MODES = Stream.of( + new BicycleTransportMode(), + new BusTransportMode(), + new HorseTransportMode(), + new PedestrianTransportMode(), + new TrolleyBusTransportMode() + ).collect(Collectors.toSet()); + + /** + * Just a convenience method for {@link #canTraverseWay(IWay, WayTraversalDirection)} that assumes {@link WayTraversalDirection#FORWARD} + * @param way the way for which we check, if it can be traversed by the transport mode + * @return {@code true} if the transport mode can travel along the way in the forward direction. Otherwise {@code false}. + */ + default boolean canTraverseWay(final Way way) { + return canTraverseWay(way, WayTraversalDirection.FORWARD); + } + + /** + * @param way the way that is checked, if the transport mode can traverse + * @param direction the travel direction for which we check + * @return {@code true} iff the transport mode can travel along the given way in the given direction. Otherwise {@code false}. + */ + boolean canTraverseWay(@NotNull IWay way, @NotNull WayTraversalDirection direction); + + /** + * Checks if this transport mode should be used for the given relation + * @param relation the relation that is checked, if it is suitable for the transport mode + * @return {@code true} if the transport mode is suitable for the relation. Otherwise {@code false}. + */ + boolean canBeUsedForRelation(@NotNull final IRelation relation); + + /** + * @param from the way from which the vehicle is coming + * @param via the node that the vehicle travels through, must be part of {@code from} and {@code to} ways, + * or this method will return false + * @param to the way onto which the vehicle makes the turn + * @return {@code true} if the transport mode can make a turn from the given {@code from} way, + * via the given {@code via} node to the given {@code to} way. Otherwise {@code false}. + * This method assumes that both ways can be traversed by the transport mode, it does not check that. + */ + boolean canTurn(@NotNull final Way from, @NotNull final Node via, @NotNull final Way to); + + /** + * @param from the way from which the vehicle is coming + * @param via the way that the vehicle travels through + * @param to the way onto which the vehicle makes the turn + * @return {@code true} if the transport mode can make a turn from the given {@code from} way, + * through the given {@code via} way to the given {@code to} way. Otherwise {@code false}. + * This method assumes that all three ways can be traversed by the transport mode, it does not check that. + */ + boolean canTurn(@NotNull final Way from, @NotNull final Way via, @NotNull final Way to); + + /** + * @return an icon representing the transport mode + */ + ImageProvider getIcon(); + + /** + * @return a unique name for the transport mode. This string should be translatable, so please use {@link I18n#marktr} on the string that's returned! + */ + String getName(); +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/PedestrianTransportMode.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/PedestrianTransportMode.java new file mode 100644 index 00000000..3fa7b4c1 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/PedestrianTransportMode.java @@ -0,0 +1,86 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.transportmode; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.drew.lang.annotations.NotNull; +import org.openstreetmap.josm.data.osm.IRelation; +import org.openstreetmap.josm.data.osm.IWay; +import org.openstreetmap.josm.data.osm.Node; +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.plugins.pt_assistant.routeexplorer.WayTraversalDirection; +import org.openstreetmap.josm.plugins.pt_assistant.utils.PTIcons; +import org.openstreetmap.josm.tools.I18n; +import org.openstreetmap.josm.tools.ImageProvider; + +public class PedestrianTransportMode implements ITransportMode { + + private static final List suitableHighwaysForPedestrians = Stream.concat( + // This list is ordered from most suitable to least suitable + Stream.of( + "pedestrian", "footway", "path", "track", "living_street", "residential", + "unclassified", "cyclestreet", "service", "cycleway", "bridleway" + ), + Stream.of("tertiary", "secondary", "primary", "trunk").flatMap(it -> Stream.of(it, it + "_link")) + ).collect(Collectors.toList()); + + protected PedestrianTransportMode() { + // should only be instantiable in `ITransportMode` + } + + @Override + public boolean canTraverseWay(@NotNull final IWay way, @NotNull final WayTraversalDirection direction) { + final String onewayValue = way.get("oneway"); + return + !way.hasTag("foot", "no") + && (way.hasTag("highway", suitableHighwaysForPedestrians) || way.hasTag("foot", "yes")) + && ( + onewayValue == null + || "no".equals(way.get("foot:backward")) + || "no".equals(way.get("oneway:foot")) + || ("yes".equals(onewayValue) && direction == WayTraversalDirection.FORWARD) + || ("-1".equals(onewayValue) && direction == WayTraversalDirection.BACKWARD) + ); + } + + @Override + public boolean canBeUsedForRelation(@NotNull final IRelation relation) { + return relation.hasTag("type", "route") && relation.hasTag("route", "foot", "walking", "hiking"); + } + + @Override + public boolean canTurn(@NotNull final Way from, @NotNull final Node via, @NotNull final Way to) { + final Set restrictionRelations = from.getReferrers().stream() + .map(it -> it.getType() == OsmPrimitiveType.RELATION ? (Relation) it : null) + .filter(Objects::nonNull) + .filter(it -> "restriction".equals(it.get("type"))) + .filter(it -> it.findRelationMembers("from").contains(from)) + .filter(it -> it.findRelationMembers("via").contains(via)) + .filter(it -> it.findRelationMembers("to").contains(to)) + .collect(Collectors.toSet()); + for (Relation restrictionRelation : restrictionRelations) { + final String restriction = restrictionRelation.get("restriction"); + if (restriction.startsWith("no_")) { + return false; + } + } + + return from.containsNode(via) && to.containsNode(via); + } + + @Override + public ImageProvider getIcon() { + return PTIcons.PEDESTRIAN; + } + + @Override + public String getName() { + return I18n.marktr("pedestrian"); + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/TrolleyBusTransportMode.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/TrolleyBusTransportMode.java new file mode 100644 index 00000000..a8bb78ef --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/TrolleyBusTransportMode.java @@ -0,0 +1,39 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.transportmode; + +import com.drew.lang.annotations.NotNull; +import org.openstreetmap.josm.data.osm.IRelation; +import org.openstreetmap.josm.data.osm.IWay; +import org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.WayTraversalDirection; +import org.openstreetmap.josm.plugins.pt_assistant.utils.PTIcons; +import org.openstreetmap.josm.tools.I18n; +import org.openstreetmap.josm.tools.ImageProvider; + +public class TrolleyBusTransportMode extends BusTransportMode { + protected TrolleyBusTransportMode() { + // should only be instantiable in `ITransportMode` + modeOfTransport = "trolleybus"; + additionalTypeForTurnRestriction = "trolleybus"; + oneWayExceptionFor = "psv"; + } + + @Override + public boolean canTraverseWay(@NotNull final IWay way, @NotNull final WayTraversalDirection direction) { + return super.canTraverseWay(way, direction) && way.hasTag("trolley_wire", "yes"); + } + + @Override + public boolean canBeUsedForRelation(@NotNull final IRelation relation) { + return relation.hasTag("type", "route") && relation.hasTag("route", "trolleybus"); + } + + @Override + public ImageProvider getIcon() { + return PTIcons.TROLLEY_BUS; + } + + @Override + public String getName() { + return I18n.marktr("trolley bus"); + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/BoundsUtils.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/BoundsUtils.java index 9cca26a3..be800a2f 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/BoundsUtils.java +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/BoundsUtils.java @@ -4,6 +4,7 @@ import java.util.Collection; import java.util.Optional; +import com.drew.lang.annotations.NotNull; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.osm.BBox; import org.openstreetmap.josm.data.osm.Way; @@ -53,4 +54,13 @@ public static Optional createBoundsWithPadding(final BBox bbox, final do ); }); } + + /** + * Converts the given {@link BBox} to {@link Bounds} + * @param bbox the bbox to convert + * @return the bounds equivalent to the passed bbox + */ + public static Bounds fromBBox(@NotNull final BBox bbox) { + return new Bounds(bbox.getBottomRightLat(), bbox.getTopLeftLon(), bbox.getTopLeftLat(), bbox.getBottomRightLon()); + } } diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/ColorPalette.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/ColorPalette.java index 5ce9bb07..62dd0f19 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/ColorPalette.java +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/ColorPalette.java @@ -18,6 +18,7 @@ private ColorPalette() { public static final Color WHITE = new Color(255, 255, 255, 180); public static final Color REF_LABEL_COLOR = new Color(0x80FFFFFF, true); + public static final Color ROUTING_HELPER_BACKGROUND = new Color(0xFF9966); public static Color[] FIVE_COLORS = { GREEN, RED, BLUE, YELLOW, CYAN }; } diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/GuiUtils.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/GuiUtils.java new file mode 100644 index 00000000..caf2dc29 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/GuiUtils.java @@ -0,0 +1,58 @@ +package org.openstreetmap.josm.plugins.pt_assistant.utils; + +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.LayoutManager; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; + +public final class GuiUtils { + private GuiUtils() { + // Private constructor to avoid instantiation + } + + public static JPanel createJPanel(final FlowLayout layout, final Component... components) { + final JPanel panel = createJPanel(layout); + for (final Component component : components) { + panel.add(component); + } + return panel; + } + + public static JPanel createJPanel(final LayoutManager layoutManager) { + final JPanel panel = new JPanel(layoutManager); + panel.setOpaque(false); + return panel; + } + + public static JButton createJButton(final String text, final Runnable action) { + return createJButton(text, __ -> action.run()); + } + + public static JButton createJButton(final String text, final ActionListener action) { + return new JButton(new AbstractAction(text) { + @Override + public void actionPerformed(ActionEvent e) { + action.actionPerformed(e); + } + }); + } + + public static JLabel createJLabel() { + return createJLabel(false); + } + + public static JLabel createJLabel(final boolean bold) { + final JLabel label = new JLabel(); + label.setFont(label.getFont().deriveFont(bold ? label.getFont().getStyle() | Font.BOLD : label.getFont().getStyle() & ~Font.BOLD)); + return label; + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/PTIcons.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/PTIcons.java index c59df706..84a8f784 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/PTIcons.java +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/PTIcons.java @@ -7,5 +7,14 @@ private PTIcons() { // Private constructor to avoid instantiation } + public static final ImageProvider BICYCLE_DESIGNATED = new ImageProvider("presets/vehicle/restriction", "bicycle-designated"); + public static final ImageProvider BICYCLE = new ImageProvider("presets/sport", "cycling"); public static final ImageProvider BUS = new ImageProvider("bus"); + public static final ImageProvider TROLLEY_BUS = new ImageProvider("presets/transport", "trolleybus"); + public static final ImageProvider PEDESTRIAN = new ImageProvider("presets/vehicle/restriction","foot-designated"); + public static final ImageProvider HORSE = new ImageProvider("presets/leisure","horse_riding"); + + public static final ImageProvider STOP_SIGN = new ImageProvider("misc", "error"); + public static final ImageProvider GREEN_CHECKMARK = new ImageProvider("misc", "green_check"); + public static final ImageProvider BUFFER_STOP = new ImageProvider("presets/transport/railway", "buffer_stop"); } diff --git a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/RouteUtils.java b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/RouteUtils.java index 1cd766f5..df9b15a5 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/RouteUtils.java +++ b/src/main/java/org/openstreetmap/josm/plugins/pt_assistant/utils/RouteUtils.java @@ -5,6 +5,7 @@ import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.TO_LAYER; import static org.openstreetmap.josm.plugins.pt_assistant.data.PTStop.isPTPlatform; +import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.List; @@ -127,6 +128,19 @@ public static boolean isPTRoute(Relation r) { return r != null && r.hasTag(OSMTags.KEY_ROUTE, acceptedRouteTags); } + /** + * Checks, if the given relation member is a way that represents part of the route itself + * (i.e. not something along the way like a stop area for public transport). + * At the moment this check just checks the primitive type and the role of the member. + * + * @param member the relation member to check + * @return {@code true} iff the given member contains a primitive of type {@link Way} and has role + * {@code forward}, {@code backward} or the empty role. Otherwise {@code false}. + */ + public static boolean isRouteWayMember(final RelationMember member) { + return member.isWay() && Arrays.asList("", "forward", "backward").contains(member.getRole()); + } + public static boolean isRoute(Relation r) { return r.get(OSMTags.KEY_ROUTE) != null; } diff --git a/test/unit/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/BusTransportModeTest.java b/test/unit/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/BusTransportModeTest.java new file mode 100644 index 00000000..58ef92ed --- /dev/null +++ b/test/unit/org/openstreetmap/josm/plugins/pt_assistant/routeexplorer/transportmode/BusTransportModeTest.java @@ -0,0 +1,344 @@ +package org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.transportmode; + +import org.junit.Rule; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.openstreetmap.josm.plugins.pt_assistant.routeexplorer.WayTraversalDirection.*; + +import org.openstreetmap.josm.data.coor.LatLon; +import org.openstreetmap.josm.data.osm.DataSet; +import org.openstreetmap.josm.data.osm.Node; +import org.openstreetmap.josm.data.osm.Relation; +import org.openstreetmap.josm.data.osm.RelationMember; +import org.openstreetmap.josm.data.osm.Way; +import org.openstreetmap.josm.testutils.JOSMTestRules; + +public class BusTransportModeTest { + @Rule + public JOSMTestRules rules = new JOSMTestRules(); + + private static final ITransportMode transportMode = new BusTransportMode(); + + @Test + public void testCanBeUsedForRelation() { + Relation route = new Relation(); + route.put("type", "route"); + route.put("route", "bus"); + assertTrue(transportMode.canBeUsedForRelation(route)); + + route.put("route", "trolleybus"); + assertFalse(transportMode.canBeUsedForRelation(route)); + + route.put("route", "coach"); + assertTrue(transportMode.canBeUsedForRelation(route)); + + route.put("route", "tram"); + assertFalse(transportMode.canBeUsedForRelation(route)); + + route.put("route", "train"); + assertFalse(transportMode.canBeUsedForRelation(route)); + + route.put("route", "minibus"); + assertTrue(transportMode.canBeUsedForRelation(route)); + + route.remove("route"); + assertFalse(transportMode.canBeUsedForRelation(route)); + } + + @Test + public void testCanTraverseWay() { + Node n1 = new Node(); + Node n2 = new Node(); + + Way w12 = new Way(); + w12.addNode(n1); + w12.addNode(n2); + + Way residentialWay = new Way(w12); + residentialWay.put("highway", "residential"); + + Way unclassifiedWay = new Way(w12); + unclassifiedWay.put("highway", "unclassified"); + + Way serviceWay = new Way(w12); + serviceWay.put("highway", "service"); + + Way livingStreetWay = new Way(w12); + livingStreetWay.put("highway", "living_street"); + + Way cyclestreetWay = new Way(w12); + cyclestreetWay.put("highway", "cyclestreet"); + + Way primaryWay = new Way(w12); + primaryWay.put("highway", "primary"); + + Way secondaryWay = new Way(w12); + secondaryWay.put("highway", "secondary"); + + Way tertiaryWay = new Way(w12); + tertiaryWay.put("highway", "tertiary"); + + Way trunkWay = new Way(w12); + trunkWay.put("highway", "trunk"); + + Way motorWay = new Way(w12); + motorWay.put("highway", "motorway"); + + Way primaryLinkWay = new Way(w12); + primaryLinkWay.put("highway", "primary_link"); + + Way secondaryLinkWay = new Way(w12); + secondaryLinkWay.put("highway", "secondary_link"); + + Way tertiaryLinkWay = new Way(w12); + tertiaryLinkWay.put("highway", "tertiary_link"); + + Way trunkLinkWay = new Way(w12); + trunkLinkWay.put("highway", "trunk_link"); + + Way motorWayLinkWay = new Way(w12); + motorWayLinkWay.put("highway", "motorway_link"); + + Way trackWay = new Way(w12); + trackWay.put("highway", "track"); + + Way pathWay = new Way(w12); + pathWay.put("highway", "path"); + + Way cycleWay = new Way(w12); + cycleWay.put("highway", "cycleway"); + + Way footWay = new Way(w12); + footWay.put("highway", "footway"); + + Way pedestrianWay = new Way(w12); + pedestrianWay.put("highway", "pedestrian"); + + Way railWay = new Way(w12); + railWay.put("railway", "rail"); + + Way tramWay = new Way(w12); + tramWay.put("railway", "tram"); + + Way subWay = new Way(w12); + subWay.put("railway", "subway"); + + Way light_railWay = new Way(w12); + light_railWay.put("railway", "light_rail"); + + Way[] suitableWaysForBuses = new Way[]{ + residentialWay, unclassifiedWay, serviceWay, livingStreetWay, cyclestreetWay, + primaryWay, secondaryWay, tertiaryWay, trunkWay, motorWay, + primaryLinkWay, secondaryLinkWay, tertiaryLinkWay, trunkLinkWay, motorWayLinkWay}; + + Way[] unSuitableWaysForBuses = new Way[]{cycleWay, footWay, pedestrianWay, railWay, tramWay, subWay, light_railWay}; + + for (Way way : suitableWaysForBuses) { + assertTrue(transportMode.canTraverseWay(way)); + assertTrue(transportMode.canTraverseWay(way, FORWARD)); + assertTrue(transportMode.canTraverseWay(way, BACKWARD)); + } + + for (Way way : unSuitableWaysForBuses) { + assertFalse(transportMode.canTraverseWay(way)); + assertFalse(transportMode.canTraverseWay(way, FORWARD)); + assertFalse(transportMode.canTraverseWay(way, BACKWARD)); + } + + for (Way way : unSuitableWaysForBuses) { + way.put("bus", "yes"); + assertTrue(transportMode.canTraverseWay(way)); + assertTrue(transportMode.canTraverseWay(way, FORWARD)); + assertTrue(transportMode.canTraverseWay(way, BACKWARD)); + } + + for (Way way : unSuitableWaysForBuses) { + way.put("psv", "yes"); + assertTrue(transportMode.canTraverseWay(way)); + assertTrue(transportMode.canTraverseWay(way, FORWARD)); + assertTrue(transportMode.canTraverseWay(way, BACKWARD)); + } + + // what if there is a general oneway tag? + for (Way way : suitableWaysForBuses) { + way.put("oneway", "yes"); + assertTrue(transportMode.canTraverseWay(way, FORWARD)); + assertFalse(transportMode.canTraverseWay(way, BACKWARD)); + } + + // what if there is an exception for buses? + for (Way way : suitableWaysForBuses) { + way.put("oneway:bus", "no"); + assertTrue(transportMode.canTraverseWay(way, FORWARD)); + assertTrue(transportMode.canTraverseWay(way, BACKWARD)); + } + + // what if there is an additional exception for psv? + for (Way way : suitableWaysForBuses) { + way.put("oneway:psv", "no"); + assertTrue(transportMode.canTraverseWay(way, FORWARD)); + assertTrue(transportMode.canTraverseWay(way, BACKWARD)); + } + + // what if there is just an exception for psv?; + for (Way way : suitableWaysForBuses) { + way.remove("oneway:bus"); + assertTrue(transportMode.canTraverseWay(way, FORWARD)); + assertTrue(transportMode.canTraverseWay(way, BACKWARD)); + } + } + + @Test + public void testCanTurn() { + Node n1 = new Node(); + Node n2 = new Node(); + Node n3 = new Node(); + Node n4 = new Node(); + n1.setCoor(new LatLon(50.0, 2.0)); + n2.setCoor(new LatLon(50.1, 2.1)); + n3.setCoor(new LatLon(50.2, 2.2)); + n4.setCoor(new LatLon(50.3, 2.3)); + + Way w12 = new Way(); + w12.addNode(n1); + w12.addNode(n2); + + Way w23 = new Way(); + w23.addNode(n2); + w23.addNode(n3); + + Way w34 = new Way(); + w34.addNode(n2); + w34.addNode(n3); + + Relation turnRestriction = new Relation(); + turnRestriction.put("type", "restriction"); + + RelationMember fromWayMember = new RelationMember("from", w12); + RelationMember viaNodeMember = new RelationMember("via", n2); + RelationMember toWayMember = new RelationMember("to", w23); + + RelationMember viaWayMember = new RelationMember("via", w23); + RelationMember toWayMember2 = new RelationMember("to", w34); + + DataSet ds = new DataSet(); + ds.addPrimitive(n1); + ds.addPrimitive(n2); + ds.addPrimitive(n3); + ds.addPrimitive(n4); + ds.addPrimitive(w12); + ds.addPrimitive(w23); + ds.addPrimitive(w34); + ds.addPrimitive(turnRestriction); + + String[] prohibitingRestrictionTypes = {"no_right_turn", "no_left_turn", "no_u_turn", "no_straight_on", "no_entry", "no_exit"}; + String[] mandatoryRestrictionTypes = {"only_right_turn", "only_left_turn", "only_u_turn", "only_straight_on"}; + String[] appliesForOtherModesOfTransport = {"hgv", "caravan", "motorcar", "agricultural", "motorcycle", "bicycle", "hazmat"}; + String[] exceptForOtherModesOfTransport = {"bicycle", "hgv", "motorcar", "emergency"}; + String[] exceptForThisModeOfTransport = {"bus", "psv"}; + + Relation rel = new Relation(); + ds.addPrimitive(rel); + + String restrictionFor = ""; + for (String prohibitingType : prohibitingRestrictionTypes) { + rel = new Relation(turnRestriction); + rel.addMember(fromWayMember); + rel.addMember(viaNodeMember); + rel.addMember(toWayMember); + ds.removePrimitive(rel); + ds.addPrimitive(rel); + for (String mot : appliesForOtherModesOfTransport) { + restrictionFor = "restriction:" + mot; + rel.put("restriction:" + mot, prohibitingType); + assertTrue(String.format("%s %s", restrictionFor, prohibitingType), transportMode.canTurn(w12, n2, w23)); + rel.remove("restriction:" + mot); + } + restrictionFor = "restriction:bus"; + rel.put(restrictionFor, prohibitingType); + assertFalse(String.format("%s %s", restrictionFor, prohibitingType), transportMode.canTurn(w12, n2, w23)); + rel.remove(restrictionFor); + + rel.put("restriction", prohibitingType); + assertFalse(String.format("%s", prohibitingType), transportMode.canTurn(w12, n2, w23)); + + for (String exc : exceptForOtherModesOfTransport) { + rel.put("except", exc); + assertFalse(String.format("%s", exc), transportMode.canTurn(w12, n2, w23)); + } + + for (String exc : exceptForThisModeOfTransport) { + rel.put("except", exc); + assertTrue(String.format("%s", exc), transportMode.canTurn(w12, n2, w23)); + } + } + + for (String prohibitingType : prohibitingRestrictionTypes) { + rel = new Relation(turnRestriction); + rel.addMember(fromWayMember); + rel.addMember(viaWayMember); + rel.addMember(toWayMember2); + ds.removePrimitive(rel); + ds.addPrimitive(rel); + + for (String mot : appliesForOtherModesOfTransport) { + restrictionFor = "restriction:" + mot; + rel.put(restrictionFor, prohibitingType); + assertTrue(String.format("%s %s", restrictionFor, prohibitingType), transportMode.canTurn(w12, w23, w34)); + rel.remove(restrictionFor); + } + restrictionFor = "restriction:bus"; + rel.put(restrictionFor, prohibitingType); + assertFalse(String.format("%s %s", restrictionFor, prohibitingType), transportMode.canTurn(w12, w23, w34)); + rel.remove(restrictionFor); + + rel.put("restriction", prohibitingType); + assertFalse(String.format("%s", prohibitingType), transportMode.canTurn(w12, w23, w34)); + + for (String exc : exceptForOtherModesOfTransport) { + rel.put("except", exc); + assertFalse(String.format("%s", exc), transportMode.canTurn(w12, w23, w34)); + } + + for (String exc : exceptForThisModeOfTransport) { + rel.put("except", exc); + assertTrue(String.format("%s", exc), transportMode.canTurn(w12, w23, w34)); + } + } + + for (String mandatoryType : mandatoryRestrictionTypes) { + rel = new Relation(turnRestriction); + rel.addMember(fromWayMember); + rel.addMember(viaNodeMember); + rel.addMember(toWayMember); + ds.removePrimitive(rel); + ds.addPrimitive(rel); + for (String mot : appliesForOtherModesOfTransport) { + restrictionFor = "restriction:" + mot; + rel.put(restrictionFor, mandatoryType); + assertTrue(transportMode.canTurn(w12, n2, w23)); + rel.remove(restrictionFor); + } + + restrictionFor = "restriction:bus"; + rel.put(restrictionFor, mandatoryType); + assertTrue(transportMode.canTurn(w12, n2, w23)); + rel.remove(restrictionFor); + + rel.put("restriction", mandatoryType); + assertTrue(transportMode.canTurn(w12, n2, w23)); + + for (String exc : exceptForOtherModesOfTransport) { + rel.put("except", exc); + assertTrue(transportMode.canTurn(w12, n2, w23)); + } + + for (String exc : exceptForThisModeOfTransport) { + rel.put("except", exc); + assertTrue(transportMode.canTurn(w12, n2, w23)); + } + } + } +} diff --git a/test/unit/org/openstreetmap/josm/plugins/pt_assistant/utils/UtilityClassesTest.java b/test/unit/org/openstreetmap/josm/plugins/pt_assistant/utils/UtilityClassesTest.java index 3196a336..885670eb 100644 --- a/test/unit/org/openstreetmap/josm/plugins/pt_assistant/utils/UtilityClassesTest.java +++ b/test/unit/org/openstreetmap/josm/plugins/pt_assistant/utils/UtilityClassesTest.java @@ -16,6 +16,7 @@ public void testAllUtilityClasses() { TestUtil.testUtilityClass(ColorPalette.class); TestUtil.testUtilityClass(DialogUtils.class); TestUtil.testUtilityClass(GeometryUtils.class); + TestUtil.testUtilityClass(GuiUtils.class); TestUtil.testUtilityClass(NodeUtils.class); TestUtil.testUtilityClass(NotificationUtils.class); TestUtil.testUtilityClass(PrimitiveUtils.class);