Make Quadralaterawhatsit properly undoable
[potlatch2.git] / net / systemeD / potlatch2 / tools / Quadrilateralise.as
1 package net.systemeD.potlatch2.tools {
2   import net.systemeD.halcyon.connection.Way;
3   import net.systemeD.halcyon.connection.Node;
4   import flash.geom.Point;
5   import net.systemeD.halcyon.connection.*;
6
7   public class Quadrilateralise {
8     private static const NUM_STEPS:uint = 1000;
9     private static const TOLERANCE:Number = 1.0e-8;
10
11     /**
12      * Attempts to make all corners of a way right angles. Returns true if it
13      * thought it was successful and false if it failed. If it fails it does not
14      * modify the way.
15      */
16     public static function quadrilateralise(way:Way):Boolean {
17       // needs a closed way to work properly.
18       if (!way.isArea()) {
19         return false;
20       }
21
22       var functor:Quadrilateralise = new Quadrilateralise(way);
23       var score:Number = functor.goodness;
24       for (var i:uint = 0; i < NUM_STEPS; ++i) {
25         functor.step();
26         var newScore:Number = functor.goodness;
27         if (newScore > score) {
28           trace("Quadrilateralise blew up! " + newScore + " > " + score);
29           return false;
30         }
31         score = newScore;
32         if (score < TOLERANCE) {
33           break;
34         }
35       }
36
37       functor.updateWay();
38       return true;
39     }
40
41     private var way:Way;
42     private var points:Array;
43     
44     // i wanted this to be private, but AS3 doesn't allow that. so please don't use it outside this package!
45     public function Quadrilateralise(way_:Way) {
46       way = way_;
47       points = way.sliceNodes(0, way.length - 1).map(function (n:Node, i:int, a:Array) : Point {
48           return new Point(n.lon, n.latp);
49         });
50     }
51
52     /**
53      * returns the score of a particular corner, which is constructed so that all corners
54      * which are straight lines or 90-degree corners score close to zero and other angles
55      * score higher. The goal of this action is to minimise the sum of all scores.
56      */
57     private function scoreOf(a:Point, b:Point, c:Point):Number {
58       var p:Point = a.subtract(b);
59       var q:Point = c.subtract(b);
60       p.normalize(1.0);
61       q.normalize(1.0);
62       var dotp:Number = p.x*q.x + p.y*q.y;
63       // score is constructed so that +1, -1 and 0 are all scored 0, any other angle
64       // is scored higher.
65       var score:Number = 2.0 * Math.min(Math.abs(dotp-1.0), Math.min(Math.abs(dotp), Math.abs(dotp+1)));
66       return score;
67     }
68
69     // get the goodness (sum of scores) of the whole way.
70     private function get goodness():Number {
71       var g:Number = 0.0;
72       for (var i:uint = 1; i < points.length - 1; ++i) {
73         var score:Number = scoreOf(points[i-1], points[i], points[i+1]);
74         g += score;
75       }
76       var startScore:Number = scoreOf(points[points.length-1], points[0], points[1]);
77       var endScore:Number = scoreOf(points[points.length-2], points[points.length-1], points[0]);
78       g += startScore;
79       g += endScore;
80       return g;
81     }
82
83     /**
84      * One step of the solver. Moves points towards their neighbours, or away from them, depending on 
85      * the angle of that corner.
86      */
87     private function step():void {
88       var motions:Array = points.map(function (b:Point, i:int, array:Array) : Point {
89           var a:Point = array[(i-1+array.length) % array.length];
90           var c:Point = array[(i+1) % array.length];
91           var p:Point = a.subtract(b);
92           var q:Point = c.subtract(b);
93           var scale:Number = p.length + q.length;
94           p.normalize(1.0);
95           q.normalize(1.0);
96           var dotp:Number = p.x*q.x + p.y*q.y;
97           // nasty hack to deal with almost-straight segments (angle is closer to 180 than to 90/270).
98           if (dotp < -0.707106781186547) {
99             dotp += 1.0;
100           }
101           var v:Point = p.add(q);
102           v.normalize(0.1 * dotp * scale);
103           return v;
104         });
105       for (var i:uint = 0; i < motions.length; ++i) {
106         points[i] = points[i].add(motions[i]);
107       }
108     }
109
110     /**
111      * call this only when happy with the result - it writes the positions back to the
112      * way, and hence to the DB or whatever.
113      */
114     private function updateWay():void {
115       var moveAction:CompositeUndoableAction = new CompositeUndoableAction("Move Way");
116       for (var i:uint = 0; i < points.length; ++i) {
117         way.getNode(i).setLonLatp( points[i].x, points[i].y, moveAction.push);
118       }
119       MainUndoStack.getGlobalStack().addAction( moveAction );
120     }
121   }
122 }