Make JetStream 2
[WebKit-https.git] / PerformanceTests / JetStream2 / RexBench / FlightPlanner / flight_planner.js
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  * Copyright (C) 2016-2017 Michael Saboff. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
15  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
18  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
25  */
26 "use strict";
27
28 let earthRadius = 3440; // In nautical miles.
29 let TwoPI = Math.PI * 2;
30 let degreeCharacter = "\u00b0";
31
32 let regExpOptionalUnicodeFlag;
33 var keywords;
34
35 if (this.useUnicode) {
36     regExpOptionalUnicodeFlag = "u";
37     keywords = UnicodeStrings;
38 } else {
39     regExpOptionalUnicodeFlag = "";
40     keywords = { get: function(str) { return str; } };
41 }
42
43 function status(text)
44 {
45     console.debug("Status: " + text);
46 }
47
48 function error(text)
49 {
50     console.error("Error: " + text);
51 }
52
53 if (typeof(Number.prototype.toRadians) === "undefined") {
54     Number.prototype.toRadians = function() {
55         return this * Math.PI / 180;
56     }
57 }
58
59 if (typeof(Number.prototype.toDegrees) === "undefined") {
60     Number.prototype.toDegrees = function() {
61         return this * 180 / Math.PI;
62     }
63 }
64
65 function distanceFromSpeedAndTime(speed, time)
66 {
67     return speed * time.hours();
68 }
69
70 let LatRE = new RegExp("^([NS\\-])?(90|[0-8]?\\d)(?:( [0-5]?\\d\\.\\d{0,3})'?|(\\.\\d{0,6})|( ([0-5]?\\d)\" ?([0-5]?\\d)'?))?", "i" + regExpOptionalUnicodeFlag);
71
72 function decimalLatitudeFromString(latitudeString)
73 {
74     if (typeof latitudeString != "string")
75         return 0;
76
77     let match = latitudeString.match(LatRE);
78
79     if (!match)
80         return 0;
81
82     let result = 0;
83     let sign = 1;
84
85     if (match[1] && (match[1].toUpperCase() == "S" || match[1] == "-"))
86         sign = -1;
87
88     result = Number(match[2]);
89
90     if (result != 90) {
91         if (match[3]) {
92             // e.g. N37 42.874
93             let minutes = Number(match[3]);
94             result = result + (minutes / 60);
95         } else if (match[4]) {
96             // e.g. N37.30697
97             let decimalDegrees = Number(match[4]);
98             result = result + decimalDegrees;
99         } else if (match[5]) {
100             // e.g. N37 18" 27'
101             let degrees = Number(match[6]);
102             let minutes = Number(match[7]);
103             result = result + (degrees + minutes / 60) / 60;
104         }
105     }
106
107     return result * sign;
108 }
109
110 let LongRE = new RegExp("^([EW\\-]?)(180|(?:1[0-7]|\\d)?\\d)(?:( [0-5]?\\d\\.\\d{0,3})|(\\.\\d{0,6})|( ([0-5]?\\d)\" ?([0-5]?\\d)'?)?)", "i" + regExpOptionalUnicodeFlag);
111
112 function decimalLongitudeFromString(longitudeString)
113 {
114     if (typeof longitudeString != "string")
115         return 0;
116
117     let match = longitudeString.match(LongRE);
118
119     if (!match)
120         return 0;
121
122     let result = 0;
123     let sign = 1;
124
125     if (match[1] && (match[1].toUpperCase() == "W" || match[1] == "-"))
126         sign = -1;
127
128     result = Number(match[2]);
129
130     if (result != 180) {
131         if (match[3]) {
132             // e.g. W121 53.254
133             let minutes = Number(match[3]);
134             result = result + (minutes / 60);
135         } else if (match[4]) {
136             // e.g. W121.8876
137             let decimalDegrees = Number(match[4]);
138             result = result + decimalDegrees;
139         } else if (match[5]) {
140             // e.g. W121 53" 15'
141             let degrees = Number(match[6]);
142             let minutes = Number(match[7]);
143             result = result + (degrees + minutes / 60) / 60;
144         }
145     }
146
147     return result * sign;
148 }
149
150 let TimeRE = new RegExp("^([0-9][0-9]?)(?:\:([0-5][0-9]))?(?:\:([0-5][0-9]))?$");
151
152 class Time
153 {
154     constructor(time)
155     {
156         if (time instanceof Date) {
157             this._seconds = Math.Round(time.valueOf() / 1000);
158             return;
159         }
160
161         if (typeof time == "string") {
162             let match = time.match(TimeRE);
163
164             if (!match) {
165                 this._seconds = 0;
166                 return;
167             }
168
169             if (match[3]) {
170                 let hours = parseInt(match[1].toString());
171                 let minutes = parseInt(match[2].toString());
172                 let seconds = parseInt(match[3].toString());
173
174                 this._seconds = (hours * 60 + minutes) * 60 + seconds;
175             } else if (match[2]) {
176                 let minutes = parseInt(match[1].toString());
177                 let seconds = parseInt(match[2].toString());
178
179                 this._seconds = minutes * 60 + seconds;
180             } else
181                 this._seconds = parseInt(match[1].toString());
182             return;
183         }
184
185         if (typeof time == "number") {
186             this._seconds = Math.round(time);
187             return;
188         }
189
190         this._seconds = 0;
191     }
192
193     add(otherTime)
194     {
195         return new Time(this._seconds + otherTime._seconds);
196     }
197
198     addDate(otherDate)
199     {
200         return new Date(this._seconds * 1000 + otherDate.valueOf());
201     }
202
203     static differenceBetween(time2, time1)
204     {
205         let seconds1;
206         let seconds2;
207         if (time1 instanceof Time)
208             seconds1 = time1.seconds();
209         else
210             seconds1 = Math.Round(time1.valueOf() / 1000);
211
212         if (time2 instanceof Time)
213             seconds2 = time2.seconds();
214         else
215             seconds2 = Math.Round(time2.valueOf() / 1000);
216
217         return new Time(seconds2 - seconds1);
218     }
219
220     seconds()
221     {
222         return this._seconds;
223     }
224
225     minutes()
226     {
227         return this._seconds / 60;
228     }
229
230     hours()
231     {
232         return this._seconds / 3600;
233     }
234
235     toString()
236     {
237         let result = "";
238         let seconds = this._seconds % 60;
239         if (seconds < 0) {
240             result = "-";
241             seconds = -seconds;
242         }
243         let minutes = this._seconds / 60 | 0;
244         let hours = minutes / 60 | 0;
245         minutes = minutes % 60;
246
247         if (hours)
248             result = result + hours + ":";
249         if (minutes < 10 && hours)
250             result = result + "0";
251         result = result + minutes + ":";
252         if (seconds < 10)
253             result = result + "0";
254         result = result + seconds;
255
256         return result;
257     }
258 }
259
260 class GeoLocation
261 {
262     constructor(latitude, longitude)
263     {
264         this.latitude = latitude;
265         this.longitude = longitude;
266     }
267
268     latitudeString()
269     {
270         let latitude = this.latitude;
271         let latitudePrefix = "N";
272         if (latitude < 0) {
273             latitude = -latitude;
274             latitudePrefix = "S"
275         }
276         let latitudeDegrees = Math.floor(latitude);
277         let latitudeMinutes = ((latitude - latitudeDegrees) * 60).toFixed(3);
278         let latitudeMinutesFiller = latitudeMinutes < 10 ? " " : "";
279         return latitudePrefix + latitudeDegrees + degreeCharacter + latitudeMinutesFiller + latitudeMinutes + "'";
280     }
281
282     longitudeString()
283     {
284         let longitude = this.longitude;
285         let longitudePrefix = "E";
286         if (longitude < 0) {
287             longitude = -longitude;
288             longitudePrefix = "W"
289         }
290
291         let longitudeDegrees = Math.floor(longitude);
292         let longitudeMinutes = ((longitude - longitudeDegrees) * 60).toFixed(3);
293         let longitudeMinutesFiller = longitudeMinutes < 10 ? " " : "";
294         return longitudePrefix + longitudeDegrees + degreeCharacter + longitudeMinutesFiller + longitudeMinutes + "'";
295     }
296
297     distanceTo(otherLocation)
298     {
299         let dLat = (otherLocation.latitude - this.latitude).toRadians();
300         let dLon = (otherLocation.longitude - this.longitude).toRadians();
301         let a = Math.sin(dLat/2) * Math.sin(dLat/2) +
302             Math.cos(this.latitude.toRadians()) * Math.cos(otherLocation.latitude.toRadians()) *
303             Math.sin(dLon/2) * Math.sin(dLon/2);
304         let c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
305         return earthRadius * c;
306     }
307     
308     bearingFrom(otherLocation, magneticVariation)
309     {
310         if (magneticVariation == undefined)
311             magneticVariation = 0;
312
313         let dLon = (this.longitude - otherLocation.longitude).toRadians();
314         let thisLatitudeRadians = this.latitude.toRadians();
315         let otherLatitudeRadians = otherLocation.latitude.toRadians();
316         let y = Math.sin(dLon) * Math.cos(this.latitude.toRadians());
317         let x = Math.cos(otherLatitudeRadians) * Math.sin(thisLatitudeRadians) -
318             Math.sin(otherLatitudeRadians) * Math.cos(thisLatitudeRadians) * Math.cos(dLon);
319         return (Math.atan2(y, x).toDegrees() + 720 + magneticVariation) % 360;
320     }
321
322     bearingTo(otherLocation, magneticVariation)
323     {
324         if (magneticVariation == undefined)
325             magneticVariation = 0;
326
327         let dLon = (otherLocation.longitude - this.longitude).toRadians();
328         let thisLatitudeRadians = this.latitude.toRadians();
329         let otherLatitudeRadians = otherLocation.latitude.toRadians();
330         let y = Math.sin(dLon) * Math.cos(otherLocation.latitude.toRadians());
331         let x = Math.cos(thisLatitudeRadians) * Math.sin(otherLatitudeRadians) -
332             Math.sin(thisLatitudeRadians) * Math.cos(otherLatitudeRadians) * Math.cos(dLon);
333         return (Math.atan2(y, x).toDegrees() + 720 + magneticVariation) % 360
334     }
335
336     locationFrom(bearing, distance, magneticVariation)
337     {
338         if (magneticVariation == undefined)
339             magneticVariation = 0;
340
341         let bearingRadians = (bearing - magneticVariation).toRadians();
342         let thisLatitudeRadians = this.latitude.toRadians();
343         let angularDistance = distance / earthRadius;
344         let latitudeRadians = Math.asin(Math.sin(thisLatitudeRadians) * Math.cos(angularDistance) +
345                                  Math.cos(thisLatitudeRadians) * Math.sin(angularDistance) * Math.cos(bearingRadians));
346         let longitudeRadians = this.longitude.toRadians() +
347             Math.atan2(Math.sin(bearingRadians) * Math.sin(angularDistance) * Math.cos(thisLatitudeRadians),
348                        Math.cos(angularDistance) - Math.sin(thisLatitudeRadians) * Math.sin(latitudeRadians));
349
350         return new GeoLocation(latitudeRadians.toDegrees(), longitudeRadians.toDegrees());
351     }
352
353     toString()
354     {
355         return "(" + this.latitudeString() + ", " + this.longitudeString() + ")";
356     }
357 }
358
359 function findFaaWaypoint(waypoint)
360 {
361     return faaWaypoints[waypoint];
362 }
363
364 class FaaWaypoints
365 {
366     constructor()
367     {
368         if (!FaaWaypoints.instance) {
369             FaaWaypoints.instance = this;
370             this.waypoints = _faaWaypoints;
371         }
372
373         return FaaWaypoints.instance;
374     }
375
376     find(waypoint)
377     {
378         return this.waypoints[waypoint];
379     }
380 }
381
382 FaaWaypoints.instance = undefined;
383
384 let faaWaypoints = new FaaWaypoints();
385
386 class FaaAirways
387 {
388     constructor()
389     {
390         if (!FaaAirways.instance) {
391             FaaAirways.instance = this;
392             this.airways = _faaAirways;
393         }
394
395         return FaaAirways.instance;
396     }
397
398     isAirway(identifier)
399     {
400         return !!this.airways[identifier];
401     }
402
403     resolveAirway(airwayID, entryPoint, exitPoint)
404     {
405         let airway = this.airways[airwayID];
406
407         if (!airway)
408             return "";
409
410         let entryIndex = airway.fixes.indexOf(entryPoint);
411         let exitIndex = airway.fixes.indexOf(exitPoint);
412
413         if (entryIndex == -1 || exitIndex == -1)
414             return "";
415
416         let stride = (entryIndex <= exitIndex) ? 1 : -1;
417
418         let route = [];
419
420         for (let idx = entryIndex; idx != exitIndex; idx = idx + stride)
421             route.push(airway.fixes[idx]);
422
423         route.push(airway.fixes[exitIndex]);
424
425         return route;
426     }
427 }
428
429 FaaAirways.instance = undefined;
430
431 let faaAirways = new FaaAirways();
432
433 class UserWaypoints
434 {
435     constructor()
436     {
437         if (!UserWaypoints.instance) {
438             UserWaypoints.instance = this;
439             this.waypoints = {};
440         }
441
442         return UserWaypoints.instance;
443     }
444
445     clear()
446     {
447         this.waypoints = {};
448     }
449
450     find(waypoint)
451     {
452         return this.waypoints[waypoint];
453     }
454
455     update(name, description, latitude, longitude)
456     {
457         if (typeof latitude == "string")
458             latitude = decimalLatitudeFromString(latitude);
459
460         if (typeof longitude == "string")
461             longitude = decimalLongitudeFromString(longitude);
462
463         this.waypoints[name.toUpperCase()] = {
464             "name": name,
465             "description": description,
466             "latitude": latitude,
467             "longitude": longitude
468         };
469     }
470 }
471
472 UserWaypoints.instance = undefined;
473
474 let userWaypoints = new UserWaypoints();
475
476
477 class EngineConfig
478 {
479     constructor(type, fuelFlow, trueAirspeed)
480     {
481         this.type = type;
482         this._fuelFlow = fuelFlow;
483         this._trueAirspeed = trueAirspeed;
484     }
485
486     trueAirspeed()
487     {
488         return this._trueAirspeed;
489     }
490
491     fuelFlow()
492     {
493         return this._fuelFlow;
494     }
495
496     static appendConfig(type, fuelFlow, trueAirspeed)
497     {
498         if (this.allConfigsByType[type]) {
499             status("Duplicate Engine configuration: " + type);
500             return;
501         }
502
503         var newConfig = new EngineConfig(type, fuelFlow, trueAirspeed);
504         this.allConfigs.push(newConfig);
505         this.allConfigsByType[type] = newConfig;
506     }
507
508     static getConfig(n)
509     {
510         if (n >= this.allConfigs.length)
511             return undefined;
512
513         return this.allConfigs[n];
514     }
515 }
516
517 EngineConfig.allConfigs = [];
518 EngineConfig.allConfigsByType = {};
519 EngineConfig.Taxi = 0;
520 EngineConfig.Runup = 1;
521 EngineConfig.Takeoff = 2;
522 EngineConfig.Climb = 3;
523 EngineConfig.Cruise = 4;
524 EngineConfig.Pattern= 5;
525
526 class Waypoint
527 {
528     constructor(name, type, description, latitude, longitude)
529     {
530         this.name = name;
531         this.type = type;
532         this.description = description;
533         this.latitude = latitude;
534         this.longitude = longitude;
535     }
536 }
537
538 class Leg
539 {
540     constructor(fix, location)
541     {
542         this.previous = undefined;
543         this.next = undefined;
544         this.fix = fix;
545         this.location = location;
546         this.course = 0;
547         this.distance = 0;
548         this.trueAirspeed = 0;
549         this.windDirection = 0;
550         this.windSpeed = 0;
551         this.heading = 0;
552         this.estGS = 0;
553         this.startFlightTiming = false;
554         this.stopFlightTiming = false;
555         this.engineConfig = EngineConfig.Cruise;
556         this.fuelFlow = 0;
557         this.distanceRemaining = 0;
558         this.estimatedTimeEnroute = undefined;
559         this.estTimeRemaining = 0;
560         this.estFuel = 0;
561     }
562
563     fixName()
564     {
565         return this.fix;
566     }
567
568     toString()
569     {
570         return this.fix;
571     }
572
573     setPrevious(leg)
574     {
575         this.previous = leg;
576     }
577
578     previousLeg()
579     {
580         return this.previous;
581     }
582
583     setNext(leg)
584     {
585         this.next = leg;
586     }
587
588     nextLeg()
589     {
590         return this.next;
591     }
592
593     setWind(windDirection, windSpeed)
594     {
595         this.windDirection = windDirection;
596         this.windSpeed = windSpeed;
597     }
598
599     isSameWind(windDirection, windSpeed)
600     {
601         return this.windDirection == windDirection && this.windSpeed == windSpeed;
602     }
603
604     windToString()
605     {
606         if (!this.windSpeed)
607             return "";
608
609         return (this.windDirection ? this.windDirection : "360") + "@" + this.windSpeed;
610     }
611
612     setTrueAirspeed(trueAirspeed)
613     {
614         this.trueAirspeed = trueAirspeed;
615     }
616
617     isStandardTrueAirspeed()
618     {
619
620         let engineConfig = EngineConfig.getConfig(this.engineConfig);
621
622         return (this.trueAirspeed == engineConfig.trueAirspeed());
623     }
624
625     trueAirspeedToString()
626     {
627         return this.trueAirspeed + "kts";
628     }
629
630     updateDistanceAndBearing(other)
631     {
632         this.distance = this.location.distanceTo(other);
633         this.course = Math.round(this.location.bearingFrom(other));
634         if (this.estimatedTimeEnroute == undefined && this.estGS != 0) {
635             let estimatedTimeEnrouteInSeconds = Math.round(this.distance * 3600 / this.estGS);
636             this.estimatedTimeEnroute = new Time(estimatedTimeEnrouteInSeconds);
637         }
638
639         if (this.estimatedTimeEnroute.seconds())
640             this.estFuel = this.fuelFlow * this.estimatedTimeEnroute.hours();
641     }
642
643     propagateWind()
644     {
645         let windDirection = this.windDirection;
646         let windSpeed = this.windSpeed;
647
648         windDirection = (windDirection + 360) % 360;
649         if (!windDirection)
650             windDirection = 360;
651
652         for (let currLeg = this; currLeg; currLeg = currLeg.nextLeg()) {
653             currLeg.windDirection = windDirection;
654             currLeg.windSpeed = windSpeed;
655             if (currLeg.stopFlightTiming)
656                 break;
657         }
658     }
659
660     updateForWind()
661     {
662         if (!this.windSpeed || !this.trueAirspeed) {
663             this.heading = this.course;
664             this.estGS = this.trueAirspeed;
665             return;
666         }
667
668         let windDirectionRadians = this.windDirection.toRadians();
669         let courseRadians = this.course.toRadians();
670         let swc = (this.windSpeed / this.trueAirspeed) * Math.sin(windDirectionRadians - courseRadians);
671         if (Math.abs(swc) > 1) {
672             status("Wind to strong to fly!");
673             return;
674         }
675
676         let headingRadians = courseRadians + Math.asin(swc);
677         if (headingRadians < 0)
678             headingRadians += TwoPI;
679         if (headingRadians > TwoPI)
680             headingRadians -= TwoPI
681         let groundSpeed = this.trueAirspeed * Math.sqrt(1 - swc * swc) -
682             this.windSpeed * Math.cos(windDirectionRadians - courseRadians);
683         if (groundSpeed < 0) {
684             status("Wind to strong to fly!");
685             return;
686         }
687
688         this.estGS = groundSpeed;
689         this.heading = Math.round(headingRadians.toDegrees());
690     }
691
692     calculate()
693     {
694         let engineConfig = EngineConfig.getConfig(this.engineConfig);
695
696         if (!this.trueAirspeed)
697             this.trueAirspeed = engineConfig.trueAirspeed();
698         this.fuelFlow = engineConfig.fuelFlow();
699
700         this.updateForWind();
701     }
702
703     updateForward()
704     {
705         if (this.specialUpdateForward)
706             this.specialUpdateForward();
707
708         let previousLeg = this.previousLeg();
709         let havePrevious = true;
710         if (!previousLeg) {
711             havePrevious = false;
712             previousLeg = this;
713             if (!this.estimatedTimeEnroute)
714                 this.estimatedTimeEnroute = new Time(0);
715         }
716
717         let thisLegType = this.type;
718         if (thisLegType == "Climb" && havePrevious)
719             this.location = previousLeg.location;
720         else {
721             this.updateDistanceAndBearing(previousLeg.location);
722             this.updateForWind();
723             let nextLeg = this.nextLeg();
724             let previousLegType = previousLeg.type;
725             if (havePrevious) {
726                 if (previousLegType == "Climb") {
727                     let climbDistance = distanceFromSpeedAndTime(previousLeg.estGS, previousLeg.climbTime);
728                     if (climbDistance < this.distance) {
729                         let climbStartLocation = previousLeg.location;
730                         let climbEndLocation = climbStartLocation.locationFrom(this.course, climbDistance);
731                         previousLeg.location = climbEndLocation;
732                         previousLeg.updateDistanceAndBearing(climbStartLocation);
733                         this.estimatedTimeEnroute = undefined;
734                         this.updateDistanceAndBearing(climbEndLocation);
735                     } else {
736                         status("Not enough distance to climb in leg #" + previousLeg.index);
737                     }
738                 } else if ((thisLegType == "Left" || thisLegType == "Right") && nextLeg && nextLeg.location) {
739                     let standardRateCircumference = this.trueAirspeed / 30;
740                     let standardRateRadius = standardRateCircumference / TwoPI;
741                     let offsetInboundBearing = 360 + previousLeg.course + (thisLegType == "Left" ? -90 : 90);
742                     offsetInboundBearing = Math.round((offsetInboundBearing + 360) % 360);
743                     // Save original location
744                     if (!previousLeg.originalLocation)
745                         previousLeg.originalLocation = previousLeg.location;
746                     let previousLocation = previousLeg.originalLocation;
747                     let inboundLocation = previousLocation.locationFrom(offsetInboundBearing, standardRateRadius);
748                     let bearingToNext = Math.round(nextLeg.location.bearingFrom(previousLocation));
749                     let offsetOutboundBearing = bearingToNext + (thisLegType == "Left" ? 90 : -90);
750                     offsetOutboundBearing = (offsetOutboundBearing + 360) % 360;
751                     let outboundLocation = previousLocation.locationFrom(offsetOutboundBearing, standardRateRadius);
752                     let turnAngle = thisLegType == "Left" ? (360 + bearingToNext - previousLeg.course) : (360 + previousLeg.course - bearingToNext);
753                     turnAngle = (turnAngle + 360) % 360;
754                     let totalDegrees = turnAngle + 360 * this.extraTurns;
755                     let secondsInTurn = Math.round(totalDegrees / 3);
756                     this.estimatedTimeEnroute = new Time(Math.round((turnAngle + 360 * this.extraTurns) / 3));
757                     this.estFuel = this.fuelFlow * this.estimatedTimeEnroute.hours();
758                     this.location = outboundLocation;
759                     this.distance = distanceFromSpeedAndTime(this.trueAirspeed, this.estimatedTimeEnroute);
760                     previousLeg.location = inboundLocation;
761                     let prevPrevLeg = previousLeg.previousLeg();
762                     if (prevPrevLeg && prevPrevLeg.location) {
763                         previousLeg.estimatedTimeEnroute = undefined;
764                         previousLeg.updateDistanceAndBearing(prevPrevLeg.location);
765                     }
766                 }
767             }
768         }
769     }
770
771     updateBackward()
772     {
773         let nextLeg = this.nextLeg();
774
775         let distanceRemaining;
776         let timeRemaining;
777
778         if (nextLeg) {
779             distanceRemaining = nextLeg.distanceRemaining;
780             timeRemaining = nextLeg.estTimeRemaining;
781         } else {
782             distanceRemaining = 0;
783             timeRemaining = new Time(0);
784         }
785
786         if (this.stopFlightTiming || timeRemaining.seconds()) {
787             this.distanceRemaining = distanceRemaining + this.distance;;
788             this.estTimeRemaining = timeRemaining.add(this.estimatedTimeEnroute);
789         } else
790             this.estTimeRemaining = new Time(0);
791     }
792 }
793
794 let RallyLegWithFixRE = new RegExp("^([0-9a-z\.]{3,16})\\|(" + keywords.get("START") + "|" + keywords.get("TIMING") + ")", "i" + regExpOptionalUnicodeFlag);
795
796 class RallyLeg extends Leg
797 {
798     constructor(type, fix, location, engineConfig)
799     {
800         super(fix, location);
801         this.type = type;
802         this.engineConfig = engineConfig;
803     }
804
805     fixName()
806     {
807         return this.type;
808     }
809
810     toString()
811     {
812         return this.fixName();
813     }
814
815     static reset()
816     {
817         RallyLeg.startLocation = undefined;
818         RallyLeg.startFix = "";
819         RallyLeg.totalTaxiTime = new Time(0);
820         RallyLeg.taxiSegments = [];
821     }
822
823     static fixNeeded(fix)
824     {
825         let match = fix.match(RallyLegWithFixRE);
826
827         if (!match)
828             return "";
829
830         return match[1].toString();
831     }
832
833     static getLegWithFix(waypointText, fix, location)
834     {
835         let match = waypointText.match(RallyLegWithFixRE);
836
837         if (!match)
838             return undefined;
839
840         let legType = match[2].toString();
841
842         if (legType == keywords.get("START")) {
843             if (this.startLocation) {
844                 status("Trying to create second start leg");
845                 return undefined;
846             }
847
848             this.startLocation = location;
849             this.startFix = fix;
850             this.totalTaxiTime = new Time(0);
851             this.taxiSegments = [];
852
853             return new StartLeg(waypointText, fix, location);
854         }
855
856         if (legType == keywords.get("TIMING"))
857             return new TimingLeg(waypointText, fix, location);
858
859
860         error("Unhandled Rally Leg type " + legType);
861         return undefined;
862     }
863 }
864
865 RallyLeg.startLocation = undefined;
866 RallyLeg.startFix = "";
867 RallyLeg.totalTaxiTime = new Time(0);
868 RallyLeg.taxiSegments = [];
869
870 class StartLeg extends RallyLeg
871 {
872     constructor(fixText, fix, location)
873     {
874         super("Start", fix, location, EngineConfig.Taxi);
875     }
876
877     fixName()
878     {
879         return this.fix + "|Start";
880     }
881 }
882
883 class TimingLeg extends RallyLeg
884 {
885     constructor(fixText, fix, location)
886     {
887         super("Timing", fix, location, EngineConfig.Cruise);
888         this.stopFlightTiming = true;
889     }
890
891     fixName()
892     {
893         return this.fix + "|Timing";
894     }
895 }
896
897 let RallyLegNoFixRE = new RegExp(keywords.get("TAXI") + "|" + keywords.get("RUNUP") + "|" + keywords.get("TAKEOFF") + "|" + keywords.get("CLIMB") + "|" + keywords.get("PATTERN") + "|" + keywords.get("LEFT") + "|" + keywords.get("RIGHT"), "i" + regExpOptionalUnicodeFlag);
898
899 class RallyLegWithoutFix extends RallyLeg
900 {
901     constructor(type, CommentsAsFix, location, engineConfig)
902     {
903         super(type, CommentsAsFix, location, engineConfig);
904     }
905
906     setPrevious(previous)
907     {
908         if (this.setLocationFromPrevious() && previous)
909             this.location = previous.location;
910
911         super.setPrevious(previous);
912     }
913
914     setLocationFromPrevious()
915     {
916         return false;
917     }
918
919     static isRallyLegWithoutFix(fix)
920     {
921         let barPosition = fix.indexOf("|");
922         let firstPart = barPosition < 0 ? fix : fix.substring(0, barPosition);
923
924         return RallyLegNoFixRE.test(firstPart);
925     }
926
927     static getLegNoFix(waypointText)
928     {
929         let barPosition = waypointText.indexOf("|");
930         let firstPart = barPosition < 0 ? waypointText : waypointText.substring(0, barPosition);
931         firstPart = firstPart.toUpperCase();
932
933         let match = firstPart.match(RallyLegNoFixRE);
934
935         if (!match)
936             return undefined;
937
938         let legType = match[0].toString();
939
940         if (legType == keywords.get("TAXI"))
941             return new TaxiLeg(waypointText);
942
943         if (legType == keywords.get("RUNUP"))
944             return new RunupLeg(waypointText);
945
946         if (legType == keywords.get("TAKEOFF")) {
947             if (!this.startLocation) {
948                 status("Trying to create a Takeoff leg without start leg");
949                 return undefined;
950             }
951
952             return new TakeoffLeg(waypointText);
953         }
954
955         if (legType == keywords.get("CLIMB"))
956             return new ClimbLeg(waypointText);
957
958         if (legType == keywords.get("PATTERN"))
959             return new PatternLeg(waypointText);
960
961         if (legType == keywords.get("LEFT") || legType == keywords.get("RIGHT"))
962             return new TurnLeg(waypointText, legType == keywords.get("RIGHT"));
963
964         error("Unhandled Rally Leg type " + legType);
965         return undefined;
966     }
967 }
968
969 // TAXI[|<time>]  e.g. TAXI|2:30
970 let TaxiLegRE = new RegExp("^" + keywords.get("TAXI") + "(?:\\|([0-9][0-9]?(?:\:[0-5][0-9])?))?$", "i" + regExpOptionalUnicodeFlag);
971
972 class TaxiLeg extends RallyLegWithoutFix
973 {
974     constructor(fixText)
975     {
976         let match = fixText.match(TaxiLegRE);
977
978         super("Taxi", "", new GeoLocation(-1, -1), EngineConfig.Taxi);
979
980         let taxiTimeString = "5:00";
981         if (match[1])
982             taxiTimeString = match[1].toString();
983
984         this.estimatedTimeEnroute = new Time(taxiTimeString);
985     }
986
987     setLocationFromPrevious()
988     {
989         return true;
990     }    
991
992     fixName()
993     {
994         return "Taxi|" + this.estimatedTimeEnroute.toString();
995     }
996 }
997
998 // RUNUP[|<time>]  e.g. RUNUP|0:30
999 let RunupLegRE = new RegExp("^" + keywords.get("RUNUP") + "(?:\\|([0-9][0-9]?(?:\:[0-5][0-9])?))?$", "i" + regExpOptionalUnicodeFlag);
1000
1001 class RunupLeg extends RallyLegWithoutFix
1002 {
1003     constructor(fixText)
1004     {
1005         let match = fixText.match(RunupLegRE);
1006
1007         super("Runup", "", new GeoLocation(-1, -1), EngineConfig.Runup);
1008
1009         let runupTimeString = "30";
1010         if (match[1])
1011             runupTimeString = match[1].toString();
1012
1013         this.estimatedTimeEnroute = new Time(runupTimeString);
1014     }
1015
1016     setLocationFromPrevious()
1017     {
1018         return true;
1019     }    
1020
1021     fixName()
1022     {
1023         return "Runup|" + this.estimatedTimeEnroute.toString();
1024     }
1025 }
1026
1027 // TAKEOFF[|<time>][|<bearing>|<distance>]  e.g. TAKEOFF|2:00|270@3.5
1028 let TakeoffLegRE = new RegExp("^" + keywords.get("TAKEOFF") + "(?:\\|([0-9][0-9]?(?:\:[0-5][0-9])?))?(?:\\|([0-9]{1,2}|[0-2][0-9][0-9]|3[0-5][0-9]|360)(?:@)(\\d{1,2}(?:\\.\\d{1,4})?))?$", "i" + regExpOptionalUnicodeFlag);
1029
1030 class TakeoffLeg extends RallyLegWithoutFix
1031 {
1032     constructor(fixText)
1033     {
1034         let match = fixText.match(TakeoffLegRE);
1035
1036         let bearingFromStart = 0;
1037         let distanceFromStart = 0;
1038         let takeoffEndLocation = RallyLeg.startLocation;
1039         if (match && match[2] && match[3]) {
1040             bearingFromStart = parseInt(match[2].toString()) % 360;
1041             distanceFromStart = parseFloat(match[3].toString());
1042             takeoffEndLocation = RallyLeg.startLocation.locationFrom(bearingFromStart, distanceFromStart);
1043         }
1044
1045         super("Takeoff", "", takeoffEndLocation, EngineConfig.Takeoff);
1046
1047         this.bearingFromStart = bearingFromStart;
1048         this.distanceFromStart = distanceFromStart;
1049
1050         let takeoffTimeString = "2:00";
1051         if (match[1])
1052             takeoffTimeString = match[1].toString();
1053
1054         this.estimatedTimeEnroute = new Time(takeoffTimeString);
1055         this.startFlightTiming = true;
1056     }
1057
1058     fixName()
1059     {
1060         let result = "Takeoff";
1061
1062         if (this.estimatedTimeEnroute.seconds() != 120)
1063             result += "|" + this.estimatedTimeEnroute.toString();
1064         if (this.distanceFromStart)
1065             result += "|" + this.bearingFromStart + "@" + this.distanceFromStart;
1066
1067         return result;
1068     }
1069 }
1070
1071 // CLIMB|<alt>|<time>  e.g. CLIMB|5000|7:00
1072 let ClimbLegRE = new RegExp("^" + keywords.get("CLIMB") + "(?:\\|)(\\d{3,5})(?:\\|([0-9][0-9]?(?:\:[0-5][0-9])?))$", "i" + regExpOptionalUnicodeFlag);
1073
1074 class ClimbLeg extends RallyLegWithoutFix
1075 {
1076     constructor(fixText)
1077     {
1078         let match = fixText.match(ClimbLegRE);
1079
1080         let altitude = 5500;
1081         if (match && match[1])
1082             altitude = match[1].toString();
1083
1084         super("Climb", altitude + "\"", undefined, EngineConfig.Climb);
1085
1086         let timeToClimb = "8:00";
1087         if (match && match[2])
1088             timeToClimb = match[2].toString();
1089
1090         this.altitude = altitude;
1091         this.climbTime = this.estimatedTimeEnroute = new Time(timeToClimb);
1092     }
1093
1094     setLocationFromPrevious()
1095     {
1096         return true;
1097     }    
1098
1099     fixName()
1100     {
1101         return "Climb|" + this.altitude + "|" + this.estimatedTimeEnroute.toString();
1102     }
1103 }
1104
1105 // PATTERN|<time>  e.g. PATTERN|0:30
1106 let PatternLegRE = new RegExp("^" + keywords.get("PATTERN") + "(?:\\|([0-9][0-9]?(?:\:[0-5][0-9])?))$", "i" + regExpOptionalUnicodeFlag);
1107
1108 class PatternLeg extends RallyLegWithoutFix
1109 {
1110     constructor(fixText)
1111     {
1112         super("Pattern", "", undefined, EngineConfig.Pattern);
1113
1114         let match = fixText.match(PatternLegRE);
1115         let patternTimeString = match[1].toString();
1116         this.estimatedTimeEnroute = new Time(patternTimeString);
1117     }
1118
1119     setLocationFromPrevious()
1120     {
1121         return true;
1122     }    
1123
1124     fixName()
1125     {
1126         return "Pattern|" + this.estimatedTimeEnroute.toString();
1127     }
1128 }
1129
1130 // {LEFT,RIGHT}[|+<extra_turns>]  e.g. LEFT|2
1131 let TurnLegRE = new RegExp("^(" + keywords.get("LEFT") + "|" + keywords.get("RIGHT") + ")(?:\\|\\+(\\d))?$", "i");
1132
1133 class TurnLeg extends RallyLegWithoutFix
1134 {
1135     constructor(fixText, isRightTurn)
1136     {
1137         let match = fixText.match(TurnLegRE);
1138
1139         let direction = "Left";
1140         if (match && match[1])
1141             direction = match[1].toString().toUpperCase() == keywords.get("LEFT") ? "Left" : "Right";
1142
1143         let engineConfig = EngineConfig.Cruise;
1144
1145         super(direction, "", new GeoLocation(-1, -1), engineConfig);
1146
1147         this.extraTurns = (match && match[2]) ? parseInt(match[2]) : 0;
1148     }
1149
1150     fixName()
1151     {
1152         let result = this.type;
1153         if (this.extraTurns)
1154             result += ("|+" + this.extraTurns);
1155
1156         return result;
1157     }
1158 }
1159
1160 let LegModifier = new RegExp("(360|3[0-5][0-9]|[0-2][0-9]{2}|[0-9]{1,2})@([0-9]{1,3})|([1-9][0-9]{1,2}|0)kts", "i");
1161
1162 class FlightPlan
1163 {
1164     constructor(name, route)
1165     {
1166         this._name = name;
1167         this._route = route;
1168         this._firstLeg = undefined;
1169         this._lastLeg = undefined;
1170         this._legCount = 0;
1171         this._defaultWindDirection = 0;
1172         this._defaultWindSpeed = 0;
1173         this._trueAirspeedOverride = 0;
1174         this._timeToGate = undefined;
1175         this._estimatedTimeEnroute = undefined;
1176         this._totalFuel = 0;
1177         this._totalTime = new Time();
1178         this._gateTime = new Time();
1179
1180         RallyLeg.reset();   //  Refactor to make this more OO
1181     }
1182
1183     clear()
1184     {
1185     }
1186
1187     appendLeg(leg)
1188     {
1189         if (!this._firstLeg)
1190             this._firstLeg = leg;
1191         if (this._lastLeg)
1192             this._lastLeg.setNext(leg);
1193         leg.setPrevious(this._lastLeg);
1194         leg.setNext(undefined);
1195
1196         if (this._trueAirspeedOverride) {
1197             leg.setTrueAirspeed(this._trueAirspeedOverride);
1198             this.clearTrueAirspeedOverride(0);
1199         }
1200
1201         if (this._defaultWindSpeed)
1202             leg.setWind(this._defaultWindDirection, this._defaultWindSpeed);
1203
1204         this._lastLeg = leg;
1205         this._legCount++;
1206     }
1207
1208     setDefaultWind(windDirection, windSpeed)
1209     {
1210         this._defaultWindDirection = windDirection;
1211         this._defaultWindSpeed = windSpeed;
1212     }
1213
1214     clearTrueAirspeedOverride()
1215     {
1216         this._trueAirspeedOverride = 0;
1217     }
1218
1219     setTrueAirspeedOverride(trueAirspeed)
1220     {
1221         this._trueAirspeedOverride = trueAirspeed;
1222     }
1223
1224     isLegModifier(fix)
1225     {
1226         return LegModifier.test(fix);
1227     }
1228
1229     processLegModifier(fix)
1230     {
1231         let match = fix.match(LegModifier);
1232
1233         if (match) {
1234             if (match[1] && match[2]) {
1235                 let windDirection = parseInt(match[1].toString()) % 360;
1236                 let windSpeed = parseInt(match[2].toString());
1237
1238                 this.setDefaultWind(windDirection, windSpeed);
1239             } else if (match[3]) {
1240                 let trueAirspeed = parseInt(match[3].toString());
1241                 this.setTrueAirspeedOverride(trueAirspeed);
1242             }
1243         }
1244     }
1245
1246     resolveWaypoint(waypointText)
1247     {
1248         if (this.isLegModifier(waypointText))
1249             this.processLegModifier(waypointText);
1250         else if (RallyLegWithoutFix.isRallyLegWithoutFix(waypointText)) {
1251             let rallyLeg = RallyLegWithoutFix.getLegNoFix(waypointText);
1252             if (rallyLeg)
1253                 this.appendLeg(rallyLeg);
1254         } else {
1255             let fixName = RallyLeg.fixNeeded(waypointText);
1256             let isRallyWaypoint = false;
1257
1258             if (fixName)
1259                 isRallyWaypoint = true;
1260             else
1261                 fixName = waypointText;
1262
1263             let waypoint = userWaypoints.find(fixName);
1264             if (!waypoint)
1265                 waypoint = faaWaypoints.find(fixName);
1266             if (!waypoint) {
1267                 error("Couldn't find waypoint \"" + waypointText + "\"");
1268                 return;
1269             }
1270
1271             let location = new GeoLocation(waypoint.latitude, waypoint.longitude);
1272
1273             if (isRallyWaypoint) {
1274                 let rallyLeg = RallyLeg.getLegWithFix(waypointText, fixName, location);
1275                 this.appendLeg(rallyLeg);
1276             } else
1277                 this.appendLeg(new Leg(waypoint.name, location));
1278         }
1279     }
1280
1281     parseRoute()
1282     {
1283         let waypointsToLookup = this._route.split(/ +/);
1284         let priorWaypoint = "";
1285
1286         for (let waypointIndex = 0; waypointIndex < waypointsToLookup.length; waypointIndex++) {
1287             let currentWaypoint = waypointsToLookup[waypointIndex].toUpperCase();
1288             if (faaAirways.isAirway(currentWaypoint) && (waypointIndex + 1) < waypointsToLookup.length) {
1289                 let exitWaypointFix = waypointsToLookup[waypointIndex + 1];
1290                 let airwayFixes = faaAirways.resolveAirway(currentWaypoint, priorWaypoint, exitWaypointFix);
1291
1292                 // We skip the entry and exit fixes, because they are handled in the prior / next
1293                 // iterations of the outer loop.
1294                 for (let airwayFixIndex = 1; airwayFixIndex < airwayFixes.length - 1; airwayFixIndex++)
1295                     this.resolveWaypoint(airwayFixes[airwayFixIndex]);
1296             } else
1297                 this.resolveWaypoint(currentWaypoint);
1298             
1299             priorWaypoint = currentWaypoint;
1300         }
1301     }
1302
1303     calculate()
1304     {
1305         if (!this._firstLeg)
1306             return;
1307
1308         let haveStartTiming = false;
1309         let haveStopTiming = false;
1310         for (let thisLeg = this._firstLeg; thisLeg; thisLeg = thisLeg.nextLeg()) {
1311             thisLeg.calculate();
1312             if (thisLeg.startFlightTiming) {
1313                 if (haveStartTiming)
1314                     status("Have duplicate Start timing leg in row " + thisLeg.toString());
1315                 haveStartTiming = true;
1316             }
1317             if (thisLeg.stopFlightTiming) {
1318                 if (haveStopTiming)
1319                     status("Have duplicate Timing leg in row " + thisLeg.toString());
1320                 haveStopTiming = true;
1321             }
1322         }
1323
1324         if (!haveStartTiming)
1325             this._firstLeg.startFlightTiming = true;
1326         if (!haveStopTiming)
1327             this._lastLeg.stopFlightTiming = true;
1328
1329         for (let thisLeg = this._firstLeg; thisLeg; thisLeg = thisLeg.nextLeg())
1330             thisLeg.updateForward();
1331
1332         for (let thisLeg = this._lastLeg; thisLeg; thisLeg = thisLeg.previousLeg())
1333             thisLeg.updateBackward();
1334
1335         for (let thisLeg = this._firstLeg; thisLeg; thisLeg = thisLeg.nextLeg()) {
1336             if (thisLeg.startFlightTiming)
1337                 this._gateTime = thisLeg.estTimeRemaining;
1338         }
1339
1340         this._totalTime = this._firstLeg.estTimeRemaining;
1341     }
1342
1343     resolvedRoute()
1344     {
1345         let result = "";
1346         let lastWindDirection = 0;
1347         let lastWindSpeed = 0;
1348
1349         let legIndex = 0;
1350         let currentLeg = this._firstLeg;
1351         
1352         for (; currentLeg; currentLeg = currentLeg.nextLeg(), legIndex++) {
1353
1354             if (legIndex)
1355                 result = result + " ";
1356
1357             if (!currentLeg.isSameWind(lastWindDirection, lastWindSpeed)) {
1358                 result = result + currentLeg.windToString() + " ";
1359                 lastWindDirection = currentLeg.windDirection;
1360                 lastWindSpeed = currentLeg.windSpeed;
1361             }
1362
1363             if (!currentLeg.isStandardTrueAirspeed())
1364                 result = result + currentLeg.trueAirspeedToString() + " ";
1365
1366             result = result + currentLeg.toString();
1367         }
1368
1369         return result;
1370     }
1371
1372     name()
1373     {
1374         return this._name;
1375     }
1376
1377     totalTime()
1378     {
1379         return this._totalTime;
1380     }
1381     
1382     gateTime()
1383     {
1384         return this._gateTime;
1385     }
1386     
1387     toString()
1388     {
1389         let result = "";
1390         let lastWindDirection = 0;
1391         let lastWindSpeed = 0;
1392
1393         let legIndex = 0;
1394         let currentLeg = this._firstLeg;
1395         
1396         for (; currentLeg; currentLeg = currentLeg.nextLeg(), legIndex++) {
1397
1398             if (legIndex)
1399                 result = result + " ";
1400
1401             if (!currentLeg.isSameWind(lastWindDirection, lastWindSpeed)) {
1402                 result = result + currentLeg.windToString() + " ";
1403                 lastWindDirection = currentLeg.windDirection;
1404                 lastWindSpeed = currentLeg.windSpeed;
1405             }
1406
1407             if (!currentLeg.isStandardTrueAirspeed())
1408                 result = result + currentLeg.trueAirspeedToString() + " ";
1409
1410             result = result + currentLeg.toString();
1411             result = result + " " + currentLeg.location + " " + currentLeg.distance.toFixed(2) + "nm " + currentLeg.estGS.toFixed(2) + "kts " + currentLeg.estimatedTimeEnroute + " ";
1412         }
1413
1414         if (this._gateTime)
1415             result = result + " gate time " + this._gateTime;
1416
1417         result = result + " total time " + this._firstLeg.estTimeRemaining;
1418         return result;
1419     }
1420 }
1421
1422 EngineConfig.appendConfig("Taxi", 2, 0);
1423 EngineConfig.appendConfig("Runup", 8, 0);
1424 EngineConfig.appendConfig("Takeoff", 27, 105);
1425 EngineConfig.appendConfig("Climb", 22, 125);
1426 EngineConfig.appendConfig("Cruise", 15, 142);
1427 EngineConfig.appendConfig("Pattern", 11, 95);
1428
1429 class ExpectedFlightPlan
1430 {
1431     constructor(name, route, expectedRoute, expectedTotalTime, expectedGateTime)
1432     {
1433         this._name = name;
1434         this._route = route;
1435         this._expectedRoute = expectedRoute;
1436         this._expectedTotalTime = expectedTotalTime;
1437         this._expectedGateTime = expectedGateTime;
1438     }
1439
1440     reset()
1441     {
1442         this._flightPlan = new FlightPlan(this._name, this._route);
1443     }
1444
1445     resolveRoute()
1446     {
1447         this._flightPlan.parseRoute();
1448     }
1449
1450     calculate()
1451     {
1452         this._flightPlan.calculate();
1453     }
1454
1455     checkExpectations()
1456     {
1457         if (this._expectedRoute) {
1458             let computedRoute = this._flightPlan.resolvedRoute();
1459             if (this._expectedRoute != computedRoute)
1460                 error("Flight plan " + this._flightPlan.name() + " route different than expected (\"" +
1461                       this._expectedRoute + "\"), got (\"" + computedRoute + "\")");
1462         }
1463
1464         if (this._expectedTotalTime) {
1465             let computedTotalTime = this._flightPlan.totalTime();
1466             let deltaTime = Math.abs(Time.differenceBetween(this._expectedTotalTime, computedTotalTime).seconds());
1467             if (deltaTime > 5)
1468                 error("Flight plan " + this._flightPlan.name() + " total time different than expected (" +
1469                       this._expectedTotalTime + "), got (" + computedTotalTime + ")");
1470         }
1471
1472         if (this._expectedGateTime) {
1473             let computedGateTime = this._flightPlan.gateTime();
1474             let deltaTime = Math.abs(Time.differenceBetween(this._expectedGateTime, computedGateTime).seconds());
1475             if (deltaTime > 5)
1476                 error("Flight plan " + this._flightPlan.name() + " gate time different than expected (" +
1477                       this._expectedGateTime + "), got (" + computedGateTime + ")");
1478         }
1479     }
1480 }
1481
1482 function setupUserWaypoints()
1483 {
1484     userWaypoints.clear();   
1485     userWaypoints.update("Oilcamp", "Oil storage in the middle of no where", "36.68471", "-120.50277");
1486     userWaypoints.update("I5.Westshields", "I5 & West Shields", "36.77774", "-120.72426");
1487     userWaypoints.update("I5.165", "Intersection of I5 and CA165", "36.93022", "-120.84068");
1488     userWaypoints.update("I5.VOLTA", "I5 & Volta Road", "37.01419", "-120.92878");
1489     userWaypoints.update("PT.ALPHA", "Intersection of I5 and CA152", "37.05665", "-120.96990");
1490     userWaypoints.update("Jellysferry", "Jelly's Ferry bridge across Sacramento River", "N40 19.037", "W122 11.359");
1491     userWaypoints.update("Howie", "RDD Timing Point", "N40 21.893", "W122 13.042");
1492     userWaypoints.update("Hale", "2014 Leg 1 Timing", "N39 33.621", "W119 14.438");
1493     userWaypoints.update("Winnie", "2014 Leg 2 Timing", "N40 50.499", "W114 12.595");
1494     userWaypoints.update("WindRiver", "2014 Leg 3 Timing", "N42 43.733", "W108 38.800");
1495     userWaypoints.update("Buff", "2014 Leg 4 Timing", "N43 59.455", "W103 16.171");
1496     userWaypoints.update("Omega", "2014 Leg 5 Timing", "N44 53.388", "W95 38.935");
1497     userWaypoints.update("Paul", "2014 Leg 6 Timing", "N43 22.027", "W89 37.111");
1498     userWaypoints.update("MicrowaveSt", "2014 Microwave Station", "N40 24.89", "W117 12.37");
1499     userWaypoints.update("RanchTower", "2014 Ranch/Tower", "N41 6.16", "W115 5.43");
1500     userWaypoints.update("FremontIsland", "2014 Fremont Island", "N41 10.49", "W112 20.64");
1501     userWaypoints.update("Tremonton", "2014 Tremonton", "N41 42.86", "W112 11.05");
1502     userWaypoints.update("RandomPoint", "2014 Random Point", "N42", "W111 03");
1503     userWaypoints.update("Farson", "2014 Farson", "N42 6.40", "W109 26.95");
1504     userWaypoints.update("Midwest", "2014 Midwest", "N43 24.49", "W109 16.68");
1505     userWaypoints.update("Bill", "2014 Bill", "N43 13.96", "W105 15.60");
1506     userWaypoints.update("MMNHS", "2014 MMNHS", "N43 52.67", "W101 57.65");
1507     userWaypoints.update("Tracks", "2014 Tracks", "N44 21", "W100 22");
1508     userWaypoints.update("Towers", "2014 Towers", "N43 34.25", "W92 25.64");
1509     userWaypoints.update("IsletonBridge", "Isleton Bridge", "N38 10.32", "W121 35.62");
1510     userWaypoints.update("Mystery15", "2015 Mystery", "N38 46.22", "W122 34.25");
1511     userWaypoints.update("Paskenta", "Paskenta Town", "N39 53.13", "W122 32.36");
1512     userWaypoints.update("Bonanza", "Bonanza Town", "N42 12.15", "W121 24.53");
1513     userWaypoints.update("Silverlake", "Silverlake", "N43 07.41", "W121 03.74");
1514     userWaypoints.update("Millican", "Bend Timing Start", "N43 52.75", "W120 55.13");
1515     userWaypoints.update("Goering", "Bend Timing", "N44 05.751", "W120 56.834");
1516     userWaypoints.update("Constantia2", "Our Constantia Wpt", "N39 56.068", "W120 0.831");
1517     userWaypoints.update("Hallelujah2", "Reno Timing", "N39 46.509", "W120 2.336");
1518     userWaypoints.update("Redding.Pond", "Pond 6nm North of KRDD", "N40 36", "W122 17");
1519     userWaypoints.update("Thunderhill", "Thunder Hill Race Track", "N39 32.36", "W122 19.83");
1520     userWaypoints.update("CascadeHighway", "Cascade Wonderland Highway", "N40 46.63", "W122 19.12");
1521     userWaypoints.update("Eagleville", "Eagleville closed airport", "N41 18.73", "W120 3.00");
1522     userWaypoints.update("DuckLakePass", "Saddle near Duck Lake", "N41 3.00", "W120 3.00");
1523 }
1524
1525 function createTestRoutes()
1526 {
1527     let flightPlans = [
1528         { name: "Rally Practice 1", route: "C83|Start Taxi|6:00 Runup|0:30 Taxi|1:00 Takeoff Climb|5500|5:00 CA67 0CN1 28CA 126kts I5.165 PT.ALPHA KLSN|Timing Pattern|0:45 Taxi|2:00" },
1529         { name: "Rally Practice 2", route: "C83|Start Taxi|6:00 Runup|0:30 Taxi|1:00 Takeoff Climb|5500|5:00 ECA 343@4 5CL3 67CA 314@12 126kts I5.165 126kts PT.ALPHA|Timing  126kts KLSN Pattern|0:45 Taxi|2:00" },
1530         { name: "Rally Practice 3", route: "KTCY|Start Taxi|6:00 Runup|0:30 Taxi|1:00 Takeoff Climb|3500|5:00 O27 67CA OILCAMP I5.WESTSHIELDS I5.165 I5.VOLTA PT.ALPHA|Timing KLSN Pattern|0:45 Taxi|2:00" },
1531         { name: "Rally Practice 4", route: "C83|Start Taxi|6:00 Runup|0:30 Taxi|1:00 Takeoff Climb|5500|5:00 62@3 9CL0 342@9 CL84 28CA 315@10 126kts I5.165 126kts PT.ALPHA|Timing  126kts KLSN Pattern|0:45 Taxi|2:00" },
1532         { name: "Rally Practice 5", route: "C83|Start Taxi|6:00 Runup|0:30 Taxi|1:00 Takeoff Climb|5500|7:00 O27 9CL0 CL01 I5.165 PT.ALPHA KLSN Pattern|0:45 Taxi|2:00" },
1533         { name: "2014 HWD Rally Leg 1", route: "KHWD|Start Taxi|6:00 Runup|0:30 Taxi|1:00 Takeoff Climb|5500|4:35 VPDUB Climb|8500|6:25 2CL9 Left|+1 09CL Left|+1 O02 Left|+1 77NV Left 126kts HALE|Timing KSPZ Pattern|0:45 Taxi|2:00"},
1534         { name: "2014 HWD Rally Leg 2", route: "KSPZ|Start Taxi|4:00 Runup|0:30 Taxi|1:00 Takeoff Climb|7500|9:00 NV30 Left MicrowaveSt Left DOBYS Left RanchTower Left 126kts Winnie|Timing KENV Pattern|0:45 Taxi|2:00"},
1535         { name: "2014 HWD Rally Leg 3", route: "KENV|Start Taxi|5:00 Runup|0:30 Taxi|1:00 Takeoff Climb|7500|8:30 FremontIsland Left|+1 Tremonton Left|+1 RandomPoint Left|+1 Farson Left 126kts WindRiver|Timing KLND Pattern|0:45 Taxi|2:00"},
1536         { name: "2014 HWD Rally Leg 4", route: "KLND|Start Taxi|6:00 Runup|0:30 Taxi|1:00 Takeoff Climb|7500|10:00 Midwest Left Bill Right|+1 WY09 Left KCUT Left 126kts BUFF|Timing KRAP Pattern|0:45 Taxi|2:00"},
1537         { name: "2014 HWD Rally Leg 5", route: "KRAP|Start Taxi|5:00 Runup|0:30 Taxi|3:00 Takeoff Climb|7500|9:00 MMNHS Left|+1 Tracks Left|+1 8D7 Left 5H3 Left 126kts Omega KMVE Pattern|0:45 Taxi|2:00"},
1538         { name: "2014 HWD Rally Leg 6", route: "KMVE|Start Taxi|6:00 Runup|0:30 Taxi|2:00 Takeoff Climb|5500|6:00 1D6 Left 68Y Right|+1 Towers Left KCHU Left 126kts Paul|Timing KMSN Pattern|0:45 Taxi|2:00"},
1539         { name: "2015 HWD Rally Leg 1", route: "KHWD|Start Taxi|6:00 Runup|0:30 Taxi|1:00 Takeoff Climb|5500|4:35 VPDUB Climb|5500|3:25 IsletonBridge Mystery15 Left|+1 71CL Left|+1 Paskenta Left|+1 O37 Left|+1 JELLYSFERRY HOWIE|Timing KRDD Pattern|0:45 Taxi|2:00"},
1540         { name: "2015 HWD Rally Leg 2", route: "KRDD|Start Taxi|6:00 Runup|0:30 Taxi|2:00 Takeoff Climb|8500|1:20 REDDING.POND Climb|8500|7:40 A26 O81 Left 340@6 Bonanza Left|+1 Silverlake Left|+1 Millican 126kts Goering|Timing KBDN pattern|30 taxi|2:00"},
1541         { name: "2016 HWD Rally Leg 1", route: "KHWD|Start Taxi|6:00 Runup|0:30 Taxi|1:00 Takeoff Climb|5500|4:35 VPDUB Climb|5500|3:25 65CN 3CN7 57CN Left|+1 2CL1 Left|+1 O37 Left|+1 JELLYSFERRY HOWIE|Timing KRDD Pattern|0:45 Taxi|2:00"},
1542         { name: "2016 HWD Rally Leg 2", route: "KRDD|Start Taxi|6:00 Runup|0:30 Taxi|2:00 Takeoff Climb|8500|1:20 REDDING.POND Climb|8500|7:40 O89 KAAT Left 1Q2 KSVE Left H37 Left CONSTANTIA2 HALLELUJAH2|Timing KRTS pattern|30 taxi|2:00"},
1543         { name: "2017 HWD Rally Leg 1", route: "KHWD|Start Taxi|6:00 Runup|0:30 Taxi|1:00 Takeoff Climb|4500|8:00 10@6 9CL9 63CL THUNDERHILL 60@10 90CL 0O4 350@10 126kts JELLYSFERRY 126kts HOWIE|Timing KRDD Pattern|0:45 Taxi|4:00" },
1544         { name: "2017 HWD Rally Leg 2", route: "KRDD|Start Taxi|6:00 Runup|0:30 Taxi|2:00 Takeoff Climb|8500|6:40 CASCADEHIGHWAY Climb|8500|8:20 KLKV 210@2 EAGLEVILLE DUCKLAKEPASS 210@5 O39 209@8 AHC 148@6 126kts CONSTANTIA2 126kts Hallelujah2|Timing KRTS Pattern|0:30 Taxi|2:00" },
1545         { name: "San Jose to San Diego", route: "KSJC LICKE SNS V25 REDIN KMYF" },
1546         { name: "San Diego to San Jose", route: "KMYF JOPDO CARIF V23 LAX V299 VTU V25 PRB SARDO RANCK V485 LICKE KSJC" },
1547         { name: "San Jose to Renton", route: "KSJC SUNOL RBL V23 SEA KRNT" },
1548         { name: "Renton to Willows", route: "KRNT SEA V495 BTG V23 RBL KWLW" },
1549         { name: "SJC to SEA #1", route: "ksjc sunol 135kts 300@12 rdd 0kts pdx ksea" },
1550         { name: "SJC to SEA #2", route: "KSJC SJC V334 SAC V23 BTG V495 SEA KSEA" },
1551         { name: "SJC to SEA #3", route: "KSJC SJC V334 SAC V23 FJS V495 SEA KSEA" },
1552         { name: "Roseburg to San Jose", route: "KRBG RBG KOLER V495 FJS V23 RBL SUNOL KSJC" },
1553         { name: "SAN to DEN", route: "KSAN HAILE V514 LYNSY V8 JNC V134 BINBE KDEN" },
1554         { name: "Denver to Minneapolis", route: "KDEN DVV V8 AKO V80 FSD V148 RWF V412 FCM KMSP" },
1555         { name: "Reno to Chicago", route: "KRNO FMG V6 LLC V32 CEVAR V200 STACO V32 FBR V6 MBW V100 RFD V171 SIMMN V172 DPA KORD" },
1556         { name: "Denver to Oklahoma City", route: "KDEN AVNEW V366 HGO V263 LAA V304 LBL V507 MMB V17 IRW KOKC" },
1557         { name: "Stockton to Hawthorne 1", route: "KSCK MOD V113 PXN V107 AVE V137 GMN V23 LAX KHHR" },
1558         { name: "Stockton to Hawthorne 2", route: "ksck patyy v113 rom v485  exert v25 lax khhr" },
1559         { name: "Long View to Corpus Christi", route: "KGGG PIPES V289 LFK V13 WORRY KCRP" },
1560         { name: "Austin Exec to Beaumont 1", route: "KEDC HOOKK V306 TNV V574 IAH V222 SHINA KBPT" },
1561         { name: "Austin Exec to Beaumont 2", route: "KEDC HOOKK V306 DAS KBPT" },
1562         { name: "Austin Exec to Beaumont 3", route: "KEDC HOOKK V306 TNV DAS KBPT" },
1563         { name: "Savannah to Daytona Beach", route: "KSAV KELER V437 COKES KDAB" },
1564         { name: "Philly to Hartford 1", route: "KPNE ARD V276 DIXIE V16 JFK V229 SNIVL KHFD" },
1565         { name: "Philly to Hartford 2", route: "KPNE ARD V276 MANTA V139 RICED MAD KHFD" },
1566         { name: "Philly to Hartford 3", route: "KPNE DITCH V312 DRIFT V139 SARDI V308 ORW KHFD" },
1567         { name: "Philly to Hartford 4", route: "KPNE ZIDET V479 ARD V433 LGA V99 YALER KHFD" },
1568         { name: "West Georgie Regional to Frankfort, KY 1", route: "kctj noone nello v5 gqo kfft" },
1569         { name: "West Georgie Regional to Frankfort, KY 2", route: "kctj felto v243 gqo v333 hyk v512 clegg kfft" },
1570         { name: "West Georgie Regional to Frankfort, KY 3", route: "kctj rmg v333 hyk v512 clegg kfft" },
1571         { name: "West Georgie Regional to Frankfort, KY 4", route: "kctj rmg v333 hch v51 lvt v493 hyk kfft" },
1572         { name: "Raleigh / Durham to Baltimore Martin State 1", route: "KRDU aimhi  KMTN" },
1573         { name: "Raleigh / Durham to Baltimore Martin State 2", route: "KRDU rdu v155 lvl ric ott KMTN" },
1574         { name: "Raleigh / Durham to Baltimore Martin State 3", route: "KRDU rdu v155 mange v157 colin v33 ott v433 paleo KMTN" },
1575         { name: "Raleigh / Durham to Baltimore Martin State 4", route: "KRDU rdu v155 LVL V157 RIC V16 PXT V93 GRACO KMTN" },
1576         { name: "Roswell, NM to Longview, TX", route: "KROW Climb|9500|6:00 070@14 HOB V68 PIZON 050@16 V16 ABI V62 JEN V94 OTTIF KGGG" },
1577         { name: "Lubbock to Longview 1", route: "KLBB JEN CQY KGGG" },
1578         { name: "Lubbock to Longview 2", route: "KLBB ralls v102 gth v278 byp v114 awlar KGGG" },
1579         { name: "Lubbock to Longview 3", route: "KLBB hydro v62 jen v94 ottif KGGG" },
1580         { name: "Stockton, CA to North Las Vegas 1", route: "KSCK ECA V244 OAL V105 LUCKY KVGT" },
1581         { name: "Stockton, CA to North Las Vegas 2", route: "KSCK ehf v197 pmd v12 basal v394 oasys KVGT" },
1582         { name: "Bakersfield to Santa Rosa 1", route: "KBFL EHF V248 AVE OAK SAU KSTS" },
1583         { name: "Bakersfield to Santa Rosa 2", route: "KBFL EHF V248 AVE PXN V301 SUNOL KSTS" },
1584         { name: "Bakersfield to Santa Rosa 3", route: "KBFL SCRAP V248 AVE V107 OAK V195 CROIT V108 STS KSTS" },
1585         { name: "Bakersfield to Santa Rosa 4", route: "KBFL EHF V23 SAC V494 SNUPY KSTS" },
1586         { name: "Bakersfield to Santa Rosa 5", route: "KBFL EHF V248 AVE V137 SNS V230 SHOEY V27 PYE KSTS" },
1587         { name: "Bakersfield to Santa Rosa 6", route: "KBFL EHF V23 CZQ MOD OAKEY KSTS" },
1588         { name: "Bakersfield to Santa Rosa 7", route: "KBFL EHF V23 LIN CCR CROIT SGD KSTS" },
1589         { name: "Bakersfield to Santa Rosa 8", route: "KBFL EHF V248 AVE V107 PXN SGD KSTS" },
1590         { name: "Great Falls to Boeing King Field 1", route: "KGTF GTF V120 MLP V2 GEG V120 NORMY KBFI" },
1591         { name: "Great Falls to Boeing King Field 2", route: "KGTF GTF V120 MLP J70 SEA KBFI" },
1592         { name: "Great Falls to Boeing King Field 3", route: "KGTF GTF V187 ELN V2 SEA KBFI" },
1593         { name: "Great Falls to Boeing King Field 4", route: "KGTF KEETA Q144 ZIRAN KBFI" },
1594         { name: "Boise to Centenial 1", route: "KBOI BOI V4 LAR RAMMS NIWOT KAPA" },
1595         { name: "Boise to Centenial 2", route: "KBOI ROARR PIH J20 OCS J154 AVVVS KAPA" },
1596         { name: "Boise to Centenial 3", route: "KBOI CANEK V4 OCS V328 DOBEE V356 ELORE KAPA" },
1597         { name: "Boise to Centenial 4", route: "KBOI BOI J54 PIH J20 OCS J52 FQF KAPA" },
1598         { name: "St. Louis to Birmingham 1", route: "KSTL SPUDZ V125 DUEAS V540 CNG V67 SYI V321 BOAZE V115 COLIG KBHM" },
1599         { name: "St. Louis to Birmingham 2", route: "KSTL ODUJY FAM CGI JKS MSL VUZ KBHM" },
1600         { name: "Chattanoga to Augusta 1", route: "KCHA ODF AHN V417 MSTRS KAGS" },
1601         { name: "Chattanoga to Augusta 2", route: "KCHA GQO NELLO CCATT T292 JACET KAGS" },
1602         { name: "Chattanoga to Augusta 3", route: "KCHA GQO ATL ANNAN KAGS" },
1603         { name: "Chattanoga to Augusta 4", route: "KCHA HOCHE V5 AHN V417 IRQ KAGS" },
1604         { name: "Concord, NC to Richmond, VA 1", route: "KJQF SBV V20 RIC KRIC" },
1605         { name: "Concord, NC to Richmond, VA 2", route: "KJQF GIZMO V143 GSO V266 SBV V20 RIC KRIC" },
1606         { name: "Concord, NC to Richmond, VA 3", route: "KJQF GSO SBV V20 RIC KRIC" },
1607         { name: "Concord, NC to Richmond, VA 4", route: "KJQF GIZMO V454 LVL V157 RIC KRIC" },
1608         { name: "Buffalo, NY to Portland, ME 1", route: "KBUF BUF V2 UCA V496 NEETS V39 LIMER KPWM" },
1609         { name: "Buffalo, NY to Portland, ME 2", route: "KBUF HANKK AUDIL PUPPY GFL KPWM" },
1610         { name: "Buffalo, NY to Portland, ME 3", route: "KBUF BUF V14 GGT V428 UCA V496 ENE KPWM" },
1611         { name: "Buffalo, NY to Portland, ME 4", route: "KBUF JOSSY Q935 PONCT ARIME CDOGG ENE KPWM" },
1612         { name: "Moline, IL to Battle Creek, MI 1", route: "KMLI GENSO V8 NOMES V156 AZO KBTL" },
1613         { name: "Moline, IL to Battle Creek, MI 2", route: "KMLI OBK J547 PMM KBTL" },
1614         { name: "Moline, IL to Battle Creek, MI 3", route: "KMLI PLL OBK ELX KBTL" },
1615         { name: "Moline, IL to Battle Creek, MI 4", route: "KMLI PLL RFD V100 ELX AZO KBTL" },
1616         { name: "Green Bay, WI to Indianapolis, IN 1", route: "KGRB OKK V305 WELDO KIND" },
1617         { name: "Green Bay, WI to Indianapolis, IN 2", route: "KGRB GRB J101 BAE J89 OBK EON V24 VHP KIND" },
1618         { name: "Green Bay, WI to Indianapolis, IN 3", route: "KGRB WAFLE V7 BVT V399 ADVAY KIND" },
1619         { name: "Green Bay, WI to Indianapolis, IN 4", route: "KGRB WAFLE V7 BVT VHP KIND" },
1620         { name: "Anoka County, MN to Springfield, IL 1", route: "KANE KANAC V97 ODI V129 GROWL KSPI" },
1621         { name: "Anoka County, MN to Springfield, IL 2", route: "KANE GEP PRIOR FGT V411 RST V67 ULAXY KSPI" },
1622         { name: "Anoka County, MN to Springfield, IL 3", route: "KANE GEP PRIOR FGT V411 RST V503 CID V67 ULAXY KSPI" },
1623         { name: "Anoka County, MN to Springfield, IL 4", route: "KANE WAGNR V510 ODI V129 SPI KSPI" },
1624         { name: "Little Rock to Souix City 1", route: "KLIT MCI J41 OVR KSUX" },
1625         { name: "Little Rock to Souix City 2", route: "KLIT ROLAN V534 HAAWK V71 SGF V159 SUX KSUX" },
1626         { name: "Little Rock to Souix City 3 ", route: "KLIT ROLAN V534 SCRAN V527 RZC V13 BUM V71 PANNY KSUX" },
1627         { name: "Little Rock to Souix City 4", route: "KLIT ROLAN V534 SCRAN V527 RZC V13 EOS V307 CNU V131 TOP PWE SUX KSUX" }
1628     ];
1629
1630     setupUserWaypoints();
1631
1632     print("let expectedFlightPlans = [");
1633
1634     for (let i = 0; i < flightPlans.length; ++i) {
1635         let flightPlanName = flightPlans[i].name;
1636         let flightPlanRoute = flightPlans[i].route;
1637         let flightPlan = new FlightPlan(flightPlanName, flightPlanRoute);
1638         flightPlan.parseRoute();
1639         flightPlan.calculate();
1640         let totalTime = flightPlan.totalTime();
1641         let gateTime = flightPlan.gateTime();
1642         let expectedGateTimeString;
1643         if (Math.abs(Time.differenceBetween(totalTime, gateTime).seconds()) == 0)
1644             expectedGateTimeString = "undefined";
1645         else
1646             expectedGateTimeString = "new Time(\"" + gateTime + "\")";
1647
1648             print("    new ExpectedFlightPlan(\"" + flightPlanName + "\", \"" + flightPlanRoute + "\", \"" +
1649                   flightPlan.resolvedRoute() + "\", new Time(\"" + totalTime + "\"), " + expectedGateTimeString + "),");
1650     }
1651
1652     print("];");
1653
1654     print("# Created " + flightPlans.length + " flight plans");
1655 }
1656
1657 // createTestRoutes();