Merge branch 'master' of github.com:systemed/potlatch2
[potlatch2.git] / net / systemeD / halcyon / connection / Trace.as
1 package net.systemeD.halcyon.connection {
2
3     import flash.events.EventDispatcher;
4     import flash.utils.Dictionary;
5     import flash.events.*;
6     import flash.net.URLLoader;
7
8     import net.systemeD.halcyon.connection.*;
9     import net.systemeD.halcyon.Map;
10     import net.systemeD.halcyon.Globals;
11     import net.systemeD.halcyon.MapPaint;
12
13     /**
14     * Implements trace objects loaded from the OSM API.
15     * See also potlatch2's utils GpxImporter.as and Importer.as classes, which can handle
16     * loading GPX files (and other formats) from arbitrary urls.
17     */
18     public class Trace extends EventDispatcher {
19         private var _id:Number; // id of the trace, as reported by the server
20         private var _description:String; // description, as reported by the server
21         private var tags:Array = []; // N.B. trace tags are NOT k/v pairs
22         private var _isLoaded:Boolean; // Flag for when the data has been downloaded and parsed
23         private var _filename:String; // The original name of the file, as reported by the server
24         private var _traceData:String; // the trace data, saved as a string
25         private var map:Map;
26         private var _layer:MapPaint;
27         private var masterConnection:Connection; // The authenticated connection
28         private var _connection:Connection; // The one we store our fake nodes/ways in.
29         private var simplify:Boolean = false;
30
31         private static const STYLESHEET:String="stylesheets/gpx.css";
32
33         /** Create a new trace.
34         * @param masterConnection The authenticated connection to communicate with the server
35         */
36         public function Trace(masterConnection:Connection, id:int=0) {
37             this.masterConnection = masterConnection;
38             map = Globals.vars.root; // REFACTOR this prevents traces being added to arbitrary maps
39                         if (id!=0) _id=id;
40         }
41
42         /** Create a new trace, from the XML description given by the user/traces call.
43         * This only creates the object itself, the actual trace contents (trkseg etc) are
44         * lazily downloaded later. */
45         public function fromXML(xml:XML):Trace {
46             _id = Number(xml.@id);
47             _filename = xml.@name;
48             _description = xml.description;
49             tags = [];
50             for each(var tag:XML in xml.tag) {
51               tags.push(String(tag));
52             }
53             return this;
54         }
55
56         public function get id():Number {
57             return _id;
58         }
59
60         public function get description():String {
61             return _description;
62         }
63
64         public function get filename():String {
65             return _filename;
66         }
67
68         public function get tagsText():String {
69             return tags.join(", ");
70         }
71
72         private function fetchFromServer():void {
73             // todo - needs proper error handling
74             masterConnection.fetchTrace(id, saveTraceData);
75             dispatchEvent(new Event("loading_data"));
76         }
77
78         private function saveTraceData(event:Event):void {
79             _traceData = String(URLLoader(event.target).data);
80             dispatchEvent(new Event("loaded_data"));
81         }
82
83         private function get connection():Connection {
84             if (!_connection) {
85                 // create a new connection so that the ids don't impact the main layer.
86                 _connection = new Connection(filename, null, null, null);
87             }
88             return _connection
89         }
90
91         private function get layer():MapPaint {
92             if (!_layer) {
93                 // create a new layer for every trace, so they can be turned on/off individually
94                 _layer = map.addLayer(connection, STYLESHEET);
95             }
96             return _layer;
97         }
98
99         public function addToMap():void {
100             // this allows adding and removing traces from the map, without re-downloading
101             // the data from the server repeatedly.
102             if (!_isLoaded) {
103               addEventListener("loaded_data", processEvent);
104               fetchFromServer();
105               return;
106             } else {
107               process();
108             }
109         }
110
111         public function removeFromMap():void {
112             //todo
113         }
114
115         private function processEvent(e:Event):void {
116             removeEventListener("loaded_data", processEvent);
117             _isLoaded=true;
118             process();
119         }
120
121         private function process():void {
122             var file:XML = new XML(_traceData);
123             var action:CompositeUndoableAction = new CompositeUndoableAction("add trace objects");
124                         for each (var ns:Namespace in file.namespaceDeclarations()) {
125                                 if (ns.uri.match(/^http:\/\/www\.topografix\.com\/GPX\/1\/[01]$/)) {
126                                         default xml namespace = ns;
127                                 }
128                         }
129
130                         Trace.parseTrkSegs(file,connection,action,false);
131                         
132             for each (var wpt:XML in file.wpt) {
133                 var tags:Object = {};
134                 for each (var tag:XML in wpt.children()) {
135                     tags[tag.name().localName]=tag.toString().substr(0,255);
136                 }
137                 var node:Node = connection.createNode(tags, wpt.@lat, wpt.@lon, action.push);
138                                 connection.registerPOI(node);
139             }
140
141             action.doAction(); /* just do it, don't add to undo stack */
142                         default xml namespace = new Namespace("");
143             layer.updateEntityUIs(true, false);
144         }
145
146                 /* Draw ways from <trkseg>s, with elementary filter to remove points within 3 metres of each other. 
147                    Optionally split way if more than 50m from previous point.
148                    FIXME: do auto-joining of dupes as per Importer. */
149
150                 public static function parseTrkSegs(file:XML, connection:Connection, action:CompositeUndoableAction, smartSplitting:Boolean=false):void {
151                         for each (var ns:Namespace in file.namespaceDeclarations()) {
152                                 if (ns.uri.match(/^http:\/\/www\.topografix\.com\/GPX\/1\/[01]$/)) { default xml namespace = ns; }
153                         }
154                         for each (var trkseg:XML in file..trkseg) {
155                                 var pts:Array = [];                             // XML can fuck off
156                                 for each (var trkpt:XML in trkseg.children()) { pts.push([trkpt.@lat, trkpt.@lon]); }
157                                 if (trkseg.trkpt[0].time.length()>0) {
158                                         Trace.createFromOrdered(pts,connection,action,smartSplitting);
159                                 } else {
160                                         Trace.createFromUnordered(pts,connection,action);
161                                 }
162                         }
163                 }
164                 public static function createFromOrdered(pts:Array,connection:Connection,action:CompositeUndoableAction,smartSplitting:Boolean):void {
165                         var nodestring:Array = [];
166                         var lat:Number = NaN, lastlat:Number = NaN;
167                         var lon:Number = NaN, lastlon:Number = NaN;
168                         var dist:Number=0;
169                         for each (var pt:Array in pts) {
170                                 lat = pt[0];
171                                 lon = pt[1];
172                                 if (isNaN(lastlat)) { lastlat = lat; lastlon = lon; }
173                                 dist=Trace.greatCircle(lat, lon, lastlat, lastlon);
174                                 if (dist>3) {
175                                         if ((dist>50 && smartSplitting) || nodestring.length>500) {
176                                                 if (dist<=50 || !smartSplitting) nodestring.push(connection.createNode({}, lat, lon, action.push));
177                                                 if (nodestring.length>1) connection.createWay({}, nodestring, action.push);
178                                                 nodestring=[];
179                                         }
180                                         nodestring.push(connection.createNode({}, lat, lon, action.push));
181                                         lastlat=lat; lastlon=lon;
182                                 }
183                         }
184                         if (nodestring.length > 1) { connection.createWay({}, nodestring, action.push); }
185                 }
186                 public static function createFromUnordered(pts:Array,connection:Connection,action:CompositeUndoableAction):void {
187                         var active:int = pts.length;
188                         var lat:Number = NaN;
189                         var lon:Number = NaN;
190                         var nodestring:Array = [];
191                         while (true) {
192                                 var created:Boolean = false;
193                                 for (var i:int=0; i<pts.length; i++) {
194                                         if (pts[i]==null) { continue; }
195                                         lat = pts[i][0];
196                                         lon = pts[i][1];
197                                         // Find the nearest
198                                         var bestDist:Number = 20;
199                                         var bestIndex:int = -1;
200                                         for (var j:int=i+1; j<=Math.min(i+5,pts.length-1); j++) {
201                                                 if (pts[j]==null) { continue; }
202                                                 var dist:Number = Trace.greatCircle(lat,lon,pts[j][0],pts[j][1]);
203                                                 if (dist<bestDist) { bestDist=dist; bestIndex=j; }
204                                         }
205                                         if (bestIndex==-1) {
206                                                 if (nodestring.length > 1) { created=true; connection.createWay({}, nodestring, action.push); }
207                                                 nodestring = [];
208                                                 break;
209                                         } else {
210                                                 if (i==bestIndex) return;
211                                                 nodestring.push(connection.createNode({}, lat, lon, action.push));
212                                                 pts[i]=null;
213                                                 active--;
214                                                 i=bestIndex-1;
215                                         }
216                                 } // end for i
217                                 if (nodestring.length > 1) {
218                                         created=true; connection.createWay({}, nodestring, action.push);
219                                         nodestring=[];
220                                 }
221                                 if (!created || active==0) { return; }
222                         } // end while
223                 } // end function
224
225                 public static function greatCircle(lat1:Number,lon1:Number,lat2:Number,lon2:Number):Number {
226                         var dlat:Number=(lat2-lat1)*Math.PI/180;
227                         var dlon:Number=(lon2-lon1)*Math.PI/180;
228                         var a:Number=Math.pow(Math.sin(dlat / 2),2) + 
229                                      Math.cos(lat1*Math.PI/180) * 
230                                      Math.cos(lat2*Math.PI/180) * 
231                                      Math.pow(Math.sin(dlon / 2),2);
232                         a=2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
233                         return a*3958.75*1609;
234                 }
235                 
236     }
237 }