// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.waydownloader;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Future;
import javax.swing.JOptionPane;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.actions.MergeNodesAction;
import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.DataSource;
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.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.DefaultNameFormatter;
import org.openstreetmap.josm.gui.MainMenu;
import org.openstreetmap.josm.gui.Notification;
import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.plugins.Plugin;
import org.openstreetmap.josm.plugins.PluginInformation;
import org.openstreetmap.josm.tools.Shortcut;
/**
* Plugin class for the Way Downloader plugin
*
* @author Harry Wood
*/
public class WayDownloaderPlugin extends Plugin {
private Way priorConnectedWay = null;
private Node selectedNode = null;
/** Plugin constructor called at JOSM startup */
public WayDownloaderPlugin(PluginInformation info) {
super(info);
//add WayDownloadAction to tools menu
MainMenu.add(Main.main.menu.moreToolsMenu, new WayDownloadAction());
}
private class WayDownloadAction extends JosmAction implements Runnable {
/** Set up the action (text appearing on the menu, keyboard shortcut etc */
public WayDownloadAction() {
super( tr("Way Download") ,
"way-download",
tr("Download map data on the end of selected way"),
Shortcut.registerShortcut("waydownloader:waydownload", tr("Way Download"), KeyEvent.VK_W, Shortcut.CTRL_SHIFT),
true);
}
/** Called when the WayDownloadAction action is triggered (e.g. user clicked the menu option) */
@Override
public void actionPerformed(ActionEvent e) {
selectedNode = null;
DataSet ds = Main.getLayerManager().getEditDataSet();
Collection<Node> selection = ds.getSelectedNodes();
if (selection.isEmpty()) {
Collection<Way> selWays = ds.getSelectedWays();
if (!workFromWaySelection(selWays)) {
showWarningMessage(tr("<html>Neither a node nor a way with an endpoint outside of the<br>current download areas is selected.<br>Select a node on the start or end of a way or an entire way first.</html>"));
return;
}
selection = ds.getSelectedNodes();
}
if ( selection.isEmpty() || selection.size()>1 || ! (selection.iterator().next() instanceof Node)) {
showWarningMessage(tr("<html>Could not find a unique node to start downloading from.</html>"));
return;
}
selectedNode = (Node) selection.iterator().next();
Main.map.mapView.zoomTo(selectedNode.getEastNorth());
//Before downloading. Figure a few things out.
//Find connected way
List<Way> connectedWays = findConnectedWays(selectedNode);
if (connectedWays.isEmpty()) {
showWarningMessage(
tr("<html>There are no ways connected to node ''{0}''. Aborting.</html>",
selectedNode.getDisplayName(DefaultNameFormatter.getInstance()))
);
return;
}
priorConnectedWay = connectedWays.get(0);
//Download a little rectangle around the selected node
double latbuffer = Main.pref.getDouble("waydownloader.latbuffer", 0.00001);
double lonbuffer = Main.pref.getDouble("waydownloader.latbuffer", 0.00002);
DownloadOsmTask downloadTask = new DownloadOsmTask();
final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor();
LatLon ll = selectedNode.getCoor();
final Future<?> future = downloadTask.download(
false /* no new layer */,
new Bounds(
ll.lat()- latbuffer,
ll.lon()- lonbuffer,
ll.lat()+ latbuffer,
ll.lon()+ lonbuffer
),
monitor
);
// schedule closing of the progress monitor after the download
// job has finished
Main.worker.submit(
new Runnable() {
@Override
public void run() {
try {
future.get();
} catch(Exception e) {
Main.error(e);
return;
}
monitor.close();
}
}
);
//The download is scheduled to be executed.
//Now schedule the run() method (below) to be executed once that's completed.
Main.worker.execute(this);
}
/**
* Logic to excute after the download has happened
*/
@Override
public void run() {
//Find ways connected to the node after the download
List<Way> connectedWays = findConnectedWays(selectedNode);
if (connectedWays.isEmpty()) {
String msg = tr("Way downloader data inconsistency. Prior connected way ''{0}'' wasn''t discovered after download",
priorConnectedWay.getDisplayName(DefaultNameFormatter.getInstance())
);
showErrorMessage(msg);
return;
}
if (connectedWays.size()==1) {
//Just one way connecting still to the node . Presumably the one which was there before
//Check if it's just a duplicate node
Node dupeNode = findDuplicateNode(selectedNode);
if (dupeNode!=null) {
String msg = tr("<html>There aren''t further connected ways to download.<br>"
+ "A potential duplicate node of the currently selected node was found, though.<br><br>"
+ "The currently selected node is ''{0}''<br>"
+ "The potential duplicate node is ''{1}''<br>"
+ "Merge the duplicate node onto the currently selected node and continue way downloading?"
+ "</html>",
selectedNode.getDisplayName(DefaultNameFormatter.getInstance()),
dupeNode.getDisplayName(DefaultNameFormatter.getInstance())
);
int ret = JOptionPane.showConfirmDialog(
Main.parent,
msg,
tr("Merge duplicate node?"),
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE
);
if (ret != JOptionPane.YES_OPTION)
return;
Command cmd = MergeNodesAction.mergeNodes(
Main.getLayerManager().getEditLayer(),
Collections.singletonList(dupeNode),
selectedNode
);
if (cmd != null) {
Main.main.undoRedo.add(cmd);
Main.getLayerManager().getEditLayer().data.setSelected(selectedNode);
}
connectedWays = findConnectedWays(selectedNode);
} else {
showInfoMessage(tr("<html>No more connected ways to download.</html>"));
return;
}
return;
}
if (connectedWays.size()>2) {
//Three or more ways meeting at this node. Means we have a junction.
String msg = tr(
"Node ''{0}'' is a junction with more than 2 connected ways.",
selectedNode.getDisplayName(DefaultNameFormatter.getInstance())
);
showWarningMessage(msg);
return;
}
if (connectedWays.size()==2) {
//Two connected ways (The "normal" way downloading case)
//Figure out which of the two is new.
Way wayA = connectedWays.get(0);
Way wayB = connectedWays.get(1);
Way nextWay = wayA;
if (priorConnectedWay.equals(wayA)) nextWay = wayB;
Node nextNode = findOtherEnd(nextWay, selectedNode);
//Select the next node
Main.getLayerManager().getEditDataSet().setSelected(nextNode);
Main.map.mapView.zoomTo(nextNode.getEastNorth());
}
}
@Override
protected void updateEnabledState() {
setEnabled(getLayerManager().getEditLayer() != null);
}
@Override
protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
// do nothing
}
}
/**
* Check whether there is a potentially duplicate node for <code>referenceNode</code>.
*
* @param referenceNode the reference node
* @return the potential duplicate node. null, if no duplicate found.
*/
private Node findDuplicateNode(Node referenceNode) {
DataSet ds = Main.getLayerManager().getEditDataSet();
List<Node> candidates = ds.searchNodes(new Bounds(referenceNode.getCoor(), 0.0003, 0.0005).toBBox());
for (Node candidate: candidates) {
if (!candidate.equals(referenceNode)
&& !candidate.isIncomplete()
&& candidate.getCoor().equals(referenceNode.getCoor()))
return candidate;
}
return null;
}
/** Given the node on one end of the way, return the node on the other end */
private Node findOtherEnd(Way way, Node firstEnd) {
Node otherEnd = way.firstNode();
if (otherEnd.equals(firstEnd)) otherEnd = way.lastNode();
return otherEnd;
}
/**
* Replies the list of ways <code>referenceNode</code> is either the first or the
* last node in.
*
* @param referenceNode the reference node
* @return the list of ways. May be empty, but null.
*/
private List<Way> findConnectedWays(Node referenceNode) {
List<Way> referers = OsmPrimitive.getFilteredList(referenceNode.getReferrers(), Way.class);
ArrayList<Way> connectedWays = new ArrayList<>(referers.size());
//loop through referers
for (Way way: referers) {
if (way.getNodesCount() >= 2 && way.isFirstLastNode(referenceNode)) {
connectedWays.add(way);
}
}
return connectedWays;
}
/**
* given a selected way, select a node on the end of the way which is not in a downloaded area
* return true if this worked
*/
private boolean workFromWaySelection(Collection<? extends OsmPrimitive> selection) {
if (selection.size() != 1)
return false;
Way selectedWay = (Way) selection.iterator().next();
selectedNode = selectedWay.firstNode();
if (isDownloaded(selectedNode)) {
selectedNode = selectedWay.lastNode();
if (isDownloaded(selectedNode)) return false;
}
Main.getLayerManager().getEditDataSet().setSelected(selectedNode);
return true;
}
private boolean isDownloaded(Node node) {
for (DataSource datasource : Main.getLayerManager().getEditDataSet().getDataSources()) {
Bounds bounds = datasource.bounds;
if (bounds != null && bounds.contains(node.getCoor())) return true;
}
return false;
}
private static void showWarningMessage(final String msg) {
if (msg != null) {
Main.warn(msg.replace("<html>", "").replace("</html>", ""));
GuiHelper.runInEDT(new Runnable() {
@Override
public void run() {
new Notification(msg)
.setIcon(JOptionPane.WARNING_MESSAGE)
.show();
}
});
}
}
private static void showErrorMessage(final String msg) {
if (msg != null) {
Main.error(msg.replace("<html>", "").replace("</html>", ""));
GuiHelper.runInEDT(new Runnable() {
@Override
public void run() {
new Notification(msg)
.setIcon(JOptionPane.ERROR_MESSAGE)
.show();
}
});
}
}
private static void showInfoMessage(final String msg) {
if (msg != null) {
Main.info(msg.replace("<html>", "").replace("</html>", ""));
GuiHelper.runInEDT(new Runnable() {
@Override
public void run() {
new Notification(msg)
.setIcon(JOptionPane.INFORMATION_MESSAGE)
.setDuration(Notification.TIME_SHORT)
.show();
}
});
}
}
}