package com.tilusnet.josm.plugins.alignways; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Shape; import java.awt.RenderingHints; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.util.ArrayList; import java.util.Collection; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.Bounds; 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.WaySegment; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.NavigatableComponent; /** * @author tilusnet <tilusnet@gmail.com> * * The segment to be aligned to the reference segment. Actions it can do: * - remember its selected pivot point * - keeps its potential pivot point list up to date * - rotate itself * - paint itself and its selected pivot point * */ public class AlignWaysAlgnSegment extends AlignWaysSegment { private enum PivotLocations { NONE, NODE1, NODE2, CENTRE } private PivotLocations currPivot; Map<PivotLocations, EastNorth> pivotList = new EnumMap<>( PivotLocations.class); private final Color pivotColor = Color.YELLOW; private final Color crossColor = pivotColor; private final Map<Node,ArrayList<WaySegment>> adjWaySegs = new HashMap<>(); public AlignWaysAlgnSegment(MapView mapview, Point p) throws IllegalArgumentException { super(mapview, p); setSegment(getNearestWaySegment(p)); segmentColor = Color.ORANGE; } /** * Sets segment and initialises its pivot list and activates the centre * rotation pivot. */ @Override public void setSegment(WaySegment segment) { super.setSegment(segment); setPivots(); } @Override void setSegmentEndpoints(WaySegment segment) { super.setSegmentEndpoints(segment); // Update the list of adjacent waysegments to the endpoints for (Node nA : getSegmentEndPoints()) { adjWaySegs.put(nA, new ArrayList<>(determineAdjacentWaysegments(nA))); } } /** * Useful when segments moves (or e.g. rotates) on the map. Updates the end * segment points and the pivot coordinates without changing the current * pivot. */ public void updatePivotsEndpoints() { setPivots(currPivot); setSegmentEndpoints(segment); } /** * Updates the segment's pivot list and sets the rotation pivot to centre. */ private void setPivots(PivotLocations pivotRef) { if (segment != null) { for (PivotLocations pl : PivotLocations.values()) { pivotList.put(pl, getPivotCoord(pl)); } setPivotReference(pivotRef); } else { setPivotReference(PivotLocations.NONE); } } private void setPivots() { setPivots(PivotLocations.CENTRE); } private void setPivotReference(PivotLocations pp) { currPivot = pp; } /** * Returns the EastNorth of the specified pivot point pp. It always returns * up-to-date data from dataset. Assumes segment is not null. * * @param pp * The pivot location */ private EastNorth getPivotCoord(PivotLocations pp) { try { EastNorth n1; EastNorth n2; switch (pp) { case NODE1: return segment.getFirstNode().getEastNorth(); case NODE2: return segment.getSecondNode().getEastNorth(); case CENTRE: n1 = getPivotCoord(PivotLocations.NODE1); n2 = getPivotCoord(PivotLocations.NODE2); return n1 != null && n2 != null ? n1.getCenter(n2) : null; case NONE: default: return null; } } catch (IndexOutOfBoundsException e) { Main.error(e); return null; } } /** * @return The EastNorth of the currently selected pivot. */ public EastNorth getCurrPivotCoord() { if (segment != null) return getPivotCoord(currPivot); return null; } /** * @param clickedPoint * Pivot may be updated in the vicinity of this point * @return true if a pivot is within reach on the segment, false otherwise */ public boolean updatePivot(Point clickedPoint) { // tHQ Done. PivotLocations tmpPivot = findNearbyPivot(clickedPoint); if (tmpPivot != PivotLocations.NONE) { setPivotReference(tmpPivot); return true; } else return false; } private PivotLocations findNearbyPivot(Point clickedPoint) { PivotLocations nearest = PivotLocations.NONE; int snapDistance = NavigatableComponent.PROP_SNAP_DISTANCE.get(); // If no alignee selected yet, there's no point to carry on if (segment == null) return PivotLocations.NONE; for (PivotLocations pl : PivotLocations.values()) { if (pl.equals(PivotLocations.NONE)) { continue; } if (mapview.getPoint(pivotList.get(pl)).distance(clickedPoint) <= snapDistance) { nearest = pl; break; } } return nearest; } /** * Given a Node (usually an endpoint), it will return a collection of way segments that are adjacently * connected to it. The current alignee waysegment is not added to the collection. * * @param node The Node (endpoint) to analyse. * @return The collection of the adjacent waysegments. */ private Collection<WaySegment> determineAdjacentWaysegments(Node node) { Collection<WaySegment> wsSet = new HashSet<>(); final double radius = 10.0; final int stepsOnCircle = 24; final double incrementOnCircle = 2 * Math.PI / stepsOnCircle; Point p = Main.map.mapView.getPoint(node); for (int i = 0; i < stepsOnCircle; i++) { double ang = i * incrementOnCircle; double x = p.getX() + (Math.cos(ang) * radius); double y = p.getY() + (Math.sin(ang) * radius); Point pnew = new Point(); pnew.setLocation(x, y); WaySegment ws = Main.map.mapView.getNearestWaySegment(pnew, OsmPrimitive::isUsable); if (ws != null && !ws.equals(this.segment) && (ws.getFirstNode().equals(node) || ws.getSecondNode().equals(node))) { // We won't want to add a: // - 'no match' (=null) // - segment that is not connected to the alignee endpoint wsSet.add(ws); } } return wsSet; } /** * Returns the collection of adjacent way segments to Node node. * The node is normally a valid endpoint of the segment. * If it isn't, null may be returned. * * @param node The (endpoint) node. * @return Collection of the adjacent way segments. */ public ArrayList<WaySegment> getAdjacentWaySegments(Node node) { return adjWaySegs.get(node); } @Override public void paint(Graphics2D g, MapView mv, Bounds bbox) { super.paint(g, mv, bbox); // Note: segment should never be null here, and its nodes should never be missing. // If they are, it's a bug, possibly related to tracking of DataSet deletions. if (segment.way.getNodesCount() <= segment.lowerIndex + 1) { Main.warn("Not drawing AlignWays pivot points: underlying nodes disappeared"); return; } // Ensure consistency updatePivotsEndpoints(); // Highlight potential pivot points for (PivotLocations pl : PivotLocations.values()) { if (pl != PivotLocations.NONE) { highlightCross(g, mv, pivotList.get(pl)); } } // Highlight active pivot highlightPivot(g, mv, getPivotCoord(currPivot)); } private void highlightPivot(Graphics2D g, MapView mv, EastNorth pivot) { g.setColor(pivotColor); g.setStroke(new BasicStroke()); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Shape pvCentrePoint = new Ellipse2D.Double( mv.getPoint(pivot).getX() - 5.0f, mv.getPoint(pivot).getY() - 5.0f, 10.0f, 10.0f); g.fill(pvCentrePoint); Shape pvPoint = new Ellipse2D.Double(mv.getPoint(pivot).getX() - 8.0f, mv.getPoint(pivot).getY() - 8.0f, 16.0f, 16.0f); g.draw(pvCentrePoint); g.draw(pvPoint); } private void highlightCross(Graphics2D g, MapView mv, EastNorth en) { double crossX = mv.getPoint(en).getX(); double crossY = mv.getPoint(en).getY(); double crossSize = 10.0; Line2D crossV = new Line2D.Double(crossX, crossY - crossSize, crossX, crossY + crossSize); Line2D crossH = new Line2D.Double(crossX - crossSize, crossY, crossX + crossSize, crossY); g.setColor(crossColor); g.setStroke(new BasicStroke()); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.draw(crossV); g.draw(crossH); } }