package org.openstreetmap.josm.plugins.contourmerge; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Point; import java.awt.geom.Line2D; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.validation.constraints.NotNull; import org.apache.commons.lang3.Validate; import org.openstreetmap.josm.command.ChangeCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.DeleteCommand; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.data.coor.EastNorth; 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.data.osm.WaySegment; import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; import org.openstreetmap.josm.data.osm.event.DataChangedEvent; import org.openstreetmap.josm.data.osm.event.DataSetListener; import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; import org.openstreetmap.josm.gui.layer.OsmDataLayer; /** * <strong>ContourMergeModel</strong> keeps the current edit state for a * specific edit layer, if the <tt>contourmerge</tt> map mode is enabled.</p> */ public class ContourMergeModel implements DataSetListener{ @SuppressWarnings("unused") static private final Logger logger = Logger.getLogger(ContourMergeModel.class.getName()); private final OsmDataLayer layer; private Node feedbackNode; private WaySegment dragStartFeedbackSegment; private WaySegment dropFeedbackSegment; private final ArrayList<Node> selectedNodes = new ArrayList<>(); private Point dragOffset = null; /** * <p>Creates a new contour merge model for the layer {@code layer}.</p> * * @param layer the data layer. Must not be null. * @throws NullPointerException thrown if {@code layer} is null */ public ContourMergeModel(@NotNull OsmDataLayer layer){ Validate.notNull(layer); this.layer = layer; } /** * <p>Replies the data layer this model operates on.</p> * * @return the data layer */ public OsmDataLayer getLayer() { return layer; } /** * <p>Replies the node the mouse is currently hovering over.</p> * * @return the node */ public Node getFeedbackNode(){ return feedbackNode; } /** * <p>Sets the node the mouse is currently hovering over.</p> * * @param node the node */ public void setFeedbackNode(Node node){ this.feedbackNode = node; } public void reset() { setFeedbackNode(null); } /* --------------------------------------------------------------------- */ /* selecting nodes and way segments */ /* --------------------------------------------------------------------- */ /** * <p>Replies true, if {@code node} is currently selected in the contour * merge mode.</p> * * @param node the node. Must not be null. Must be owned by this models * layer. * @return true, if {@code node} is currently selected in the contour merge * mode. */ public boolean isSelected(@NotNull Node node) { Validate.notNull(node); Validate.isTrue(node.getDataSet() == layer.data, "Node must be owned by this contour merge models layer"); // don't //translate return selectedNodes.contains(node); } /** * <p>Selects the node {@code node}.</p> * * @param node the node. Must not be null. Must be owned by this models * layer. */ public void selectNode(@NotNull Node node) { Validate.notNull(node); Validate.isTrue(node.getDataSet() == layer.data, "Node must be owned by this contour merge models layer"); if (!isSelected(node)) selectedNodes.add(node); } /** * <p>Deselects the node {@code node}.</p> * * @param node the node. Must not be null. Must be owned by this models * layer. */ public void deselectNode(@NotNull Node node) { Validate.notNull(node); Validate.isTrue(node.getDataSet() == layer.data, //don't translate "Node must be owned by this contour merge models layer"); selectedNodes.remove(node); } /** * <p>Toggles whether the node {@code node} is selected or not.</p> * * @param node the node. Must not be null. Must be owned by this models * layer. */ public void toggleSelected(Node node) { Validate.notNull(node); Validate.isTrue(node.getDataSet() == layer.data, // don't translate "Node must be owned by this contour merge models layer"); if (isSelected(node)) { deselectNode(node); } else { selectNode(node); } } /** * <p>Deselects all nodes.</p> */ public void deselectAllNodes(){ selectedNodes.clear(); } /** * <p>Replies an <strong>unmodifiable</strong> list of the currently * selected nodes.</p> * * @return an <strong>unmodifiable</strong> list of the currently * selected nodes.</p> */ public List<Node> getSelectedNodes() { return Collections.unmodifiableList(selectedNodes); } /** * <p>Sets the way segment which would be affected by the next drag/drop * operation.</p> * * @param segment the way segment. null, if there is no feedback segment */ public void setDragStartFeedbackWaySegment(WaySegment segment){ this.dragStartFeedbackSegment = segment; } /** * <p>Replies the current feedback way segment or null, if there is * currently no such segment * * @return the feedback way segment */ public WaySegment getDragStartFeedbackWaySegement(){ return dragStartFeedbackSegment; } public void setDropFeedbackSegment(WaySegment segment){ this.dropFeedbackSegment = segment; } public WaySegment getDropFeedbackSegment(){ return dropFeedbackSegment; } /** * <p>Replies the set of selected ways, i.e. the set of all parent ways of * the selected nodes.</p> * * @return the set of selected ways */ protected Set<Way> computeSelectedWays(){ return selectedNodes.stream() .flatMap(n -> OsmPrimitive.getFilteredList( n.getReferrers(),Way.class ).stream()) .collect(Collectors.toSet()); } /** * <p>Replies the set of selected nodes on the way {@code way}.</p> * * @param way the way * @return the set of selected nodes */ protected Set<Node> computeSelectedNodesOnWay(Way way){ return selectedNodes.stream() .filter(n -> OsmPrimitive.getFilteredSet(n.getReferrers(), Way.class).contains(way) ) .collect(Collectors.toSet()); } /** * <p>Replies true, if we can start a drag/drop operation on way slice * which is given by the currently selected nodes and the way segment * {@code ws}.</p> * * @return true, if we can start a drag/drop operation. false, otherwise */ public boolean isWaySegmentDragable(WaySegment ws){ WaySlice slice = getWaySliceFromSelectedNodes(ws); return slice != null; } /** * <p>Replies true, if {@code ws} is part of a potential drop target.</p> * * @param ws the way segment. If null, replies false. * @return true, if {@code ws} is part of a potential drop target */ public boolean isPotentialDropTarget(WaySegment ws){ if (ws == null) return false; WaySlice dropTarget = getWaySliceFromSelectedNodes(ws); if (dropTarget == null) return false; // make sure we don't try to drop on the drag source, not even // on a different way slice on the way we drag from WaySlice dragSource = getDragSource(); if (dragSource == null) return true; return ! dragSource.getWay().equals(dropTarget.getWay()); } protected List<Integer> computeSelectedNodeIndicesOnWay(Way way){ return computeSelectedNodesOnWay(way).stream() .map(n -> way.getNodes().indexOf(n)) .sorted() .collect(Collectors.toList()); } protected WaySlice getWaySliceFromSelectedNodes( WaySegment referenceSegment){ if (referenceSegment == null) return null; Way way = referenceSegment.way; if (way == null || way.getNodesCount() == 0) { // shouldn't happen, but consistency of a dataset is sometimes // violated, after undo/redo/merge/etc. operations // This is a workaround for potential defects similar to // https://github.com/Gubaer/josm-contourmerge-plugin/issues/4 return null; } if (way.isClosed()){ /* * This is a closed way. We need at least two selected nodes to * come up with a way slice. */ List<Integer> selIndices = computeSelectedNodeIndicesOnWay(way); if (selIndices.size() <2) return null; int nn= way.getNodesCount(); int li = referenceSegment.lowerIndex; int lower = -1; int upper = nn; /* * Find the first selected node to the "left" of the way segment, * wrapping around at the join-node, if necessary. */ for (int i=li; i>=0;i--){ if (selIndices.contains(i)) {lower = i; break;} } if (lower == -1){ // not found yet - wrap around and continue search for (int i=nn-1; i>li; i--){ if (selIndices.contains(i)) {lower = i; break;} } } /* * Find the first selected node to the "right" of the way segment, * wrapping around at the join-node, if necessary. */ for (int i=li+1; i< nn-1 ; i++){ if (selIndices.contains(i)) {upper = i; break;} } if (upper == nn){ // not found yet - wrap around and continue search for (int i=0; i<li; i++){ if (selIndices.contains(i)) {upper = i; break;} } /* * not really a wrap around? => adjust the index */ if (upper == 0) upper = nn-1; } if (lower < upper){ if (upper == nn -1) { return new WaySlice(way, 0, lower, false /* reverse direction */); } else { return new WaySlice(way, lower,upper); } } else if (lower == upper ){ return new WaySlice(way, 0, upper, false /* reverse direction */); } else { return new WaySlice(way, upper, lower, false /* reverse direction */); } } else { /* * This is an open way. We can always reply a way slice. If no * nodes are selected, we drag the entire way. If 1 node * is selected, the way segment determines whether we drag the * first or the second half. If more than 1 nodes are selected, * we drag the way slice between two selected, or the first or the * last node respectively. */ List<Integer> selIndices = computeSelectedNodeIndicesOnWay( referenceSegment.way); int nn= way.getNodesCount(); int li = referenceSegment.lowerIndex; int lastPos = nn -1; int lower = 0; int upper = lastPos; for (int pos=li; pos >=0; pos--){ if (selIndices.contains(pos)) {lower = pos; break;} } for (int pos=li+1; pos <=lastPos; pos++){ if (selIndices.contains(pos)) {upper = pos; break;} } if (lower == upper) return null; return new WaySlice(referenceSegment.way, lower, upper); } } /** * <p>Replies the way slice we are currently dragging, or null, if we * aren't in a drag operation.</p> * * @return the way slice or null */ public WaySlice getDragSource(){ if (dragStartFeedbackSegment == null) return null; return getWaySliceFromSelectedNodes(dragStartFeedbackSegment); } /** * <p>Replies the way slice we are currently hovering over and which is * suitable as drop target, or null, if no such way slice is currently * known.</p> * * @return the way slice or null */ public WaySlice getDropTarget(){ if (dropFeedbackSegment == null) return null; return getWaySliceFromSelectedNodes(dropFeedbackSegment); } /** * <p>Sets the current drag offset, relative to the point where the * drag operation started. Set null to indicate, that there is currently * no drag operation. </p> * * @param offset the drag offset */ public void setDragOffset(Point offset){ this.dragOffset = offset; } /** * <p>Replies the current drag offset or null, if we aren't in a drag * operation.</p> * * @return the drag offset */ public Point getDragOffset(){ return dragOffset; } /** * <p>Replies true, if we are currently in a drag operation.</p> * * @return true, if we are currently in a drag operation */ public boolean isDragging() { return dragOffset != null; } /** * <p>Builds the command to align the two contours. Replies null, if the * command can't be created, i.e. because there is no defined drag source * or drop target.</p> * * @return the contour align command */ public Command buildContourAlignCommand() { WaySlice dragSource = getDragSource(); WaySlice dropTarget = getDropTarget(); if (dragSource == null) return null; if (dropTarget == null) return null; List<Node> targetNodes = dropTarget.getNodes(); if (! areDirectionAligned(dragSource, dropTarget)) { Collections.reverse(targetNodes); } List<Command> cmds = new ArrayList<>(); // the command to change the source way cmds.add(new ChangeCommand(dragSource.getWay(), dragSource.replaceNodes(targetNodes))); // the commands to delete nodes we don't need anymore for (Node n: dragSource.getNodes()) { List<OsmPrimitive> parents = n.getReferrers(); parents.remove(dragSource.getWay()); if (parents.isEmpty() && !n.isTagged()) { cmds.add(new DeleteCommand(n)); } } SequenceCommand cmd = new SequenceCommand(tr("Merging Contour"), cmds); return cmd; } protected boolean haveSameStartAndEndNode(List<Node> n1, List<Node> n2) { return n1.get(0) == n2.get(0) && n1.get(n1.size()-1) == n2.get(n2.size()-1); } protected boolean haveReversedStartAndEndeNode(List<Node> n1, List<Node> n2) { return n1.get(0) == n2.get(n2.size()-1) && n1.get(n1.size()-1) == n2.get(0); } /** * <p>Replies true, if the two polylines given by the node lists {@code n1} * and {@code n2} are "direction aligned". Their direction is aligned, * if the two lines between the two start nodes and the two end nodes * of {@code n1} and code {@code n2} respectively, do not intersect.</p> * * @param n1 the first list of nodes * @param n2 the second list of nodes * @return true, if the two polylines are "direction aligned". */ protected boolean areDirectionAligned(List<Node> n1, List<Node> n2) { /* * Check whether n1 and n2 start and end at the same nodes */ if (haveSameStartAndEndNode(n1, n2)) return true; if (haveReversedStartAndEndeNode(n1,n2)) return false; /* * n1 and n2 have different start or end nodes. Draw an imaginary line * from the start of n1 to start of n2 and from the end of n1 to the end * of n2 and check whether they intersect. */ EastNorth s1 = n1.get(0).getEastNorth(); EastNorth s2 = n1.get(n1.size()-1).getEastNorth(); EastNorth t1 = n2.get(0).getEastNorth(); EastNorth t2 = n2.get(n2.size()-1).getEastNorth(); Line2D l1 = new Line2D.Double(s1.getX(), s1.getY(), t1.getX(), t1.getY()); Line2D l2 = new Line2D.Double(s2.getX(), s2.getY(), t2.getX(), t2.getY()); return ! l1.intersectsLine(l2); } /** * <p>Replies true, if the two way slices are "direction aligned".</p> * * @param dragSource the first way slice * @param dropTarget the second way slice * @return true, if the two way slices are "direction aligned" * @see #areDirectionAligned(List, List) */ protected boolean areDirectionAligned(WaySlice dragSource, WaySlice dropTarget){ if (dragSource == null) return false; if (dropTarget == null) return false; return areDirectionAligned(dragSource.getNodes(), dropTarget.getNodes()); } protected void ensureSelectedNodesConsistent() { Iterator<Node> it = selectedNodes.iterator(); while(it.hasNext()) { Node n = it.next(); if (!layer.data.getNodes().contains(n)) { it.remove(); } else if (OsmPrimitive.getFilteredSet(n.getReferrers(), Way.class).isEmpty()) { it.remove(); } else if (n.isDeleted()) { it.remove(); } } } /* --------------------------------------------------------------------- */ /* interface DataSetListener */ /* --------------------------------------------------------------------- */ @Override public void primitivesAdded(PrimitivesAddedEvent arg0) {/* ignore */} @Override public void primitivesRemoved(PrimitivesRemovedEvent event) { ensureSelectedNodesConsistent(); } @Override public void wayNodesChanged(WayNodesChangedEvent event) { ensureSelectedNodesConsistent(); } @Override public void dataChanged(DataChangedEvent event) { ensureSelectedNodesConsistent(); } @Override public void relationMembersChanged(RelationMembersChangedEvent event) {/* ignore */} @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {/*ignore */} public void primtivesAdded(PrimitivesAddedEvent event) {/* ignore */} @Override public void tagsChanged(TagsChangedEvent event) { /* ignore */} @Override public void nodeMoved(NodeMovedEvent event) {/* ignore */} }