Ticket #5922: bug5922.patch

File bug5922.patch, 13.5 KB (added by Balaitous, 12 years ago)

patch from r6934

  • src/org/openstreetmap/josm/actions/CreateCircleAction.java

     
    77import java.awt.event.ActionEvent;
    88import java.awt.event.KeyEvent;
    99import java.util.ArrayList;
     10import java.util.Arrays;
    1011import java.util.Collection;
     12import java.util.Comparator;
    1113import java.util.LinkedList;
    1214import java.util.List;
    1315
     
    2527import org.openstreetmap.josm.data.osm.OsmPrimitive;
    2628import org.openstreetmap.josm.data.osm.Way;
    2729import org.openstreetmap.josm.gui.Notification;
     30import org.openstreetmap.josm.tools.Geometry;
    2831import org.openstreetmap.josm.tools.Shortcut;
    2932
    3033/**
     
    3235 * - Create a new circle from three selected nodes--or a way with 3 nodes.
    3336 * - Useful for roundabouts
    3437 *
    35  * Note: If a way is selected, it is changed. If nodes are selected a new way is created.
    36  *       So if you've got a way with nodes it makes a difference between running this on the way or the nodes!
    37  *
     38 * Notes:
     39 *   * If a way is selected, it is changed. If nodes are selected a new way is created.
     40 *     So if you've got a way with nodes it makes a difference between running this on the way or the nodes!
     41 *   * The existing nodes are retained, and additional nodes are inserted regularly
     42 *     to achieve the desired number of nodes (`createcircle.nodecount`).
    3843 * BTW: Someone might want to implement projection corrections for this...
    3944 *
    4045 * @since 996
    4146 *
    4247 * @author Henry Loenwind
    4348 * @author Sebastian Masch
     49 * @author Alain Delplanque
    4450 */
    4551public final class CreateCircleAction extends JosmAction {
    4652
     
    5460        putValue("help", ht("/Action/CreateCircle"));
    5561    }
    5662
    57     private double calcang(double xc, double yc, double x, double y) {
    58         // calculate the angle from xc|yc to x|y
    59         if (xc == x && yc == y)
    60             return 0; // actually invalid, but we won't have this case in this context
    61         double yd = Math.abs(y - yc);
    62         if (yd == 0 && xc < x)
    63             return 0;
    64         if (yd == 0 && xc > x)
    65             return Math.PI;
    66         double xd = Math.abs(x - xc);
    67         double a = Math.atan2(xd, yd);
    68         if (y > yc) {
    69             a = Math.PI - a;
     63    /**
     64     * Distribute nodes according to the algorithm of election with largest remainder.
     65     * @param angles Array of PolarNode ordered by increasing angles
     66     * @param nodesCount Number of nodes to be distributed
     67     * @return Array of number of nodes to put in each arc
     68     */
     69    private int[] distributeNodes(PolarNode[] angles, int nodesCount) {
     70        int[] count = new int[angles.length];
     71        double[] width = new double[angles.length];
     72        double[] remainder = new double[angles.length];
     73        for(int i = 0; i < angles.length; i++) {
     74            width[i] = angles[(i+1) % angles.length].a - angles[i].a;
     75            if(width[i] < 0)
     76                width[i] += 2*Math.PI;
    7077        }
    71         if (x < xc) {
    72             a = -a;
     78        int assign = 0;
     79        for(int i = 0; i < angles.length; i++) {
     80            double part = width[i] / 2.0 / Math.PI * nodesCount;
     81            count[i] = (int) Math.floor(part);
     82            remainder[i] = part - count[i];
     83            assign += count[i];
    7384        }
    74         a = 1.5*Math.PI + a;
    75         if (a < 0) {
    76             a += 2*Math.PI;
     85        while(assign < nodesCount) {
     86            int imax = 0;
     87            for(int i = 1; i < angles.length; i++)
     88                if(remainder[i] > remainder[imax])
     89                    imax = i;
     90            count[imax]++;
     91            remainder[imax] = 0;
     92            assign++;
    7793        }
    78         if (a >= 2*Math.PI) {
    79             a -= 2*Math.PI;
     94        return count;
     95    }
     96   
     97    /**
     98     * Class designed to create a couple between a node and its angle relative to the center of the circle.
     99     */
     100    private class PolarNode {
     101        double a;
     102        Node node;
     103       
     104        PolarNode(EastNorth center, Node n) {
     105            EastNorth pt = n.getEastNorth();
     106            this.a = Math.atan2(pt.north() - center.north(), pt.east() - center.east());
     107            this.node = n;
    80108        }
    81         return a;
    82109    }
     110   
     111    /**
     112     * Comparator used to order PolarNode relative to their angle.
     113     */
     114    private class PolarNodeComparator implements Comparator<PolarNode> {
    83115
     116        @Override
     117        public int compare(PolarNode pc1, PolarNode pc2) {
     118            if(pc1.a < pc2.a)
     119                return -1;
     120            else if(pc1.a == pc2.a)
     121                return 0;
     122            else
     123                return 1;
     124        }
     125    }
     126   
    84127    @Override
    85128    public void actionPerformed(ActionEvent e) {
    86129        if (!isEnabled())
     
    117160                }
    118161        }
    119162
     163        if(nodes.size() < 2 || nodes.size() > 3) {
     164            new Notification(
     165                    tr("Please select exactly two or three nodes or one way with exactly two or three nodes."))
     166                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
     167                    .setDuration(Notification.TIME_LONG)
     168                    .show();
     169            return;
     170        }
     171       
    120172        // now we can start doing things to OSM data
    121173        Collection<Command> cmds = new LinkedList<Command>();
    122 
     174        EastNorth center = null;
     175       
    123176        if (nodes.size() == 2) {
    124177            // diameter: two single nodes needed or a way with two nodes
    125 
    126178            Node   n1 = nodes.get(0);
    127179            double x1 = n1.getEastNorth().east();
    128180            double y1 = n1.getEastNorth().north();
     
    133185            // calculate the center (xc/yc)
    134186            double xc = 0.5 * (x1 + x2);
    135187            double yc = 0.5 * (y1 + y2);
    136 
    137             // calculate the radius (r)
    138             double r = Math.sqrt(Math.pow(xc-x1,2) + Math.pow(yc-y1,2));
    139 
    140             // find where to put the existing nodes
    141             double a1 = calcang(xc, yc, x1, y1);
    142             double a2 = calcang(xc, yc, x2, y2);
    143             if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; }
    144 
    145             // build a way for the circle
    146             List<Node> wayToAdd = new ArrayList<Node>(numberOfNodesInCircle + 1);
    147 
    148             for (int i = 1; i <= numberOfNodesInCircle; i++) {
    149                 double a = a2 + 2*Math.PI*(1.0 - i/(double)numberOfNodesInCircle); // "1-" to get it clock-wise
    150 
    151                 // insert existing nodes if they fit before this new node (999 means "already added this node")
    152                 if ((a1 < 999) && (a1 > a - 1E-9) && (a1 < a + 1E-9)) {
    153                     wayToAdd.add(n1);
    154                     a1 = 999;
    155                 }
    156                 else if ((a2 < 999) && (a2 > a - 1E-9) && (a2 < a + 1E-9)) {
    157                     wayToAdd.add(n2);
    158                     a2 = 999;
    159                 }
    160                 else {
    161                     // get the position of the new node and insert it
    162                     double x = xc + r*Math.cos(a);
    163                     double y = yc + r*Math.sin(a);
    164                     Node n = new Node(Main.getProjection().eastNorth2latlon(new EastNorth(x,y)));
    165                     wayToAdd.add(n);
    166                     cmds.add(new AddCommand(n));
    167                 }
    168             }
    169             wayToAdd.add(wayToAdd.get(0)); // close the circle
    170             if (existingWay == null) {
    171                 Way newWay = new Way();
    172                 newWay.setNodes(wayToAdd);
    173                 cmds.add(new AddCommand(newWay));
    174             } else {
    175                 Way newWay = new Way(existingWay);
    176                 newWay.setNodes(wayToAdd);
    177                 cmds.add(new ChangeCommand(existingWay, newWay));
    178             }
    179 
    180             // the first node may be unused/abandoned if createcircle.nodecount is odd
    181             if (a1 < 999) {
    182                 // if it is, delete it
    183                 List<OsmPrimitive> parents = n1.getReferrers();
    184                 if (parents.isEmpty() || ((parents.size() == 1) && (parents.contains(existingWay)))) {
    185                     cmds.add(new DeleteCommand(n1));
    186                 }
    187             }
    188 
     188            center = new EastNorth(xc, yc);
    189189        } else if (nodes.size() == 3) {
    190190            // triangle: three single nodes needed or a way with three nodes
    191 
    192             // let's get some shorter names
    193             Node   n1 = nodes.get(0);
    194             double x1 = n1.getEastNorth().east();
    195             double y1 = n1.getEastNorth().north();
    196             Node   n2 = nodes.get(1);
    197             double x2 = n2.getEastNorth().east();
    198             double y2 = n2.getEastNorth().north();
    199             Node   n3 = nodes.get(2);
    200             double x3 = n3.getEastNorth().east();
    201             double y3 = n3.getEastNorth().north();
    202 
    203             // calculate the center (xc/yc)
    204             double s = 0.5*((x2 - x3)*(x1 - x3) - (y2 - y3)*(y3 - y1));
    205             double sUnder = (x1 - x2)*(y3 - y1) - (y2 - y1)*(x1 - x3);
    206 
    207             if (sUnder == 0) {
     191            center = Geometry.getCenter(nodes);
     192            if(center == null) {
    208193                notifyNodesNotOnCircle();
    209194                return;
    210195            }
     196        }
    211197
    212             s /= sUnder;
     198        // calculate the radius (r)
     199        EastNorth n1 = nodes.get(0).getEastNorth();
     200        double r = Math.sqrt(Math.pow(center.east()-n1.east(),2) +
     201                Math.pow(center.north()-n1.north(),2));
    213202
    214             double xc = 0.5*(x1 + x2) + s*(y2 - y1);
    215             double yc = 0.5*(y1 + y2) + s*(x1 - x2);
     203        // Order nodes by angle
     204        PolarNode[] angles = new PolarNode[nodes.size()];
     205        for(int i = 0; i < nodes.size(); i++) {
     206            angles[i] = new PolarNode(center, nodes.get(i));
     207        }
     208        Arrays.sort(angles, new PolarNodeComparator());
     209        int[] count = distributeNodes(angles,
     210                numberOfNodesInCircle >= nodes.size() ? numberOfNodesInCircle - nodes.size() : 0);
    216211
    217             // calculate the radius (r)
    218             double r = Math.sqrt(Math.pow(xc-x1,2) + Math.pow(yc-y1,2));
    219 
    220             // find where to put the existing nodes
    221             double a1 = calcang(xc, yc, x1, y1);
    222             double a2 = calcang(xc, yc, x2, y2);
    223             double a3 = calcang(xc, yc, x3, y3);
    224             if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; }
    225             if (a2 < a3) { double at = a2; Node nt = n2; a2 = a3; n2 = n3; a3 = at; n3 = nt; }
    226             if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; }
    227 
    228             // build a way for the circle
    229             List<Node> wayToAdd = new ArrayList<Node>();
    230             for (int i = 1; i <= numberOfNodesInCircle; i++) {
    231                 double a = 2*Math.PI*(1.0 - i/(double)numberOfNodesInCircle); // "1-" to get it clock-wise
    232                 // insert existing nodes if they fit before this new node (999 means "already added this node")
    233                 if (a1 < 999 && a1 > a) {
    234                     wayToAdd.add(n1);
    235                     a1 = 999;
    236                 }
    237                 if (a2 < 999 && a2 > a) {
    238                     wayToAdd.add(n2);
    239                     a2 = 999;
    240                 }
    241                 if (a3 < 999 && a3 > a) {
    242                     wayToAdd.add(n3);
    243                     a3 = 999;
    244                 }
    245                 // get the position of the new node and insert it
    246                 double x = xc + r*Math.cos(a);
    247                 double y = yc + r*Math.sin(a);
     212        // build a way for the circle
     213        List<Node> wayToAdd = new ArrayList<Node>();
     214        for(int i = 0; i < nodes.size(); i++) {
     215            wayToAdd.add(angles[i].node);
     216            double delta = angles[(i+1) % nodes.size()].a - angles[i].a;
     217            if(delta < 0)
     218                delta += 2*Math.PI;
     219            for(int j = 0; j < count[i]; j++) {
     220                double alpha = angles[i].a + (j+1)*delta/(count[i]+1);
     221                double x = center.east() + r*Math.cos(alpha);
     222                double y = center.north() + r*Math.sin(alpha);
    248223                LatLon ll = Main.getProjection().eastNorth2latlon(new EastNorth(x,y));
    249224                if (ll.isOutSideWorld()) {
    250225                    notifyNodesNotOnCircle();
     
    254229                wayToAdd.add(n);
    255230                cmds.add(new AddCommand(n));
    256231            }
    257             wayToAdd.add(wayToAdd.get(0)); // close the circle
    258             if (existingWay == null) {
    259                 Way newWay = new Way();
    260                 newWay.setNodes(wayToAdd);
    261                 cmds.add(new AddCommand(newWay));
    262             } else {
    263                 Way newWay = new Way(existingWay);
    264                 newWay.setNodes(wayToAdd);
    265                 cmds.add(new ChangeCommand(existingWay, newWay));
    266             }
    267 
     232        }
     233        wayToAdd.add(wayToAdd.get(0)); // close the circle
     234        if (existingWay == null) {
     235            Way newWay = new Way();
     236            newWay.setNodes(wayToAdd);
     237            cmds.add(new AddCommand(newWay));
    268238        } else {
    269             new Notification(
    270                     tr("Please select exactly two or three nodes or one way with exactly two or three nodes."))
    271                     .setIcon(JOptionPane.INFORMATION_MESSAGE)
    272                     .setDuration(Notification.TIME_LONG)
    273                     .show();
    274             return;
     239            Way newWay = new Way(existingWay);
     240            newWay.setNodes(wayToAdd);
     241            cmds.add(new ChangeCommand(existingWay, newWay));
    275242        }
    276243
    277244        Main.main.undoRedo.add(new SequenceCommand(tr("Create Circle"), cmds));