-
Notifications
You must be signed in to change notification settings - Fork 4
/
FlightFiles.py
1534 lines (1400 loc) · 46.6 KB
/
FlightFiles.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
File: FlightFiles.py
-----------------------
Objects and functions necessary for route planning.
Airplanes, weights, environments, airports, cities,
routes, and basic calculations.
@class
Airplane
@class
Weight
@class
Environment
@class
Point_Of_Interest
@class
Route
@author: Andrew M.
@date: 2015-2016
"""
from __future__ import division
import os
import re
import sys
import math
import copy
import urllib
import pygmaps
import random
import geopy
from Elevations import *
from downloadmap import *
from geomag import mag_heading
from fractions import Fraction
from geopy.distance import vincenty
from geopy.geocoders import Nominatim
from bs4 import BeautifulSoup
from geographiclib.geodesic import Geodesic
from flask_caching import Cache
from flask import current_app
"""
General conversion constants.
"""
km_to_nm = 0.539957
km_to_miles = 0.621371
nm_to_km = 1.852
feet_to_nm = 0.000164579
meters_to_feet = 3.28084
sm_per_deg = 69.0000
nm_per_sm = 0.868976
nm_per_deg = sm_per_deg * nm_per_sm
SHORT_TIMEOUT = 1.3
"""
An airplane is used to store relevant information for weight and balance calculations.
"""
class Airplane:
"""
Initialize the plane with type and list of weights.
@type plane_type: string
@param plane_type: string of plane type (ex. "C172SP NAV III")
@type weights: list
@param weights: list of weights with moments and arms
"""
def __init__(self, plane_type, weights):
self.plane_type = plane_type
self.weights = weights
for x in range(len(self.weights)):
self.weights[x].num = "Moment %i" % (x+1)
self.calcCG()
# ** NOTE ** need to log the tail number in database (can be done in App.py)
return
"""
Print the airplane type and CG as the representation of the airplane object.
"""
def __repr__(self):
return "Airplane of type: {" + self.plane_type + "} and CG=" + str(self.cg) + "."
"""
Calculates the center of gravity of an airpoane given each weight parameter
"""
def calcCG(self):
weight = 0
moment = 0
for item in self.weights:
weight += item.weight
moment += item.moment
if weight == 0:
self.cg = 0
return
self.cg = float("{0:.2f}".format(float(moment/weight)))
self.totalweight = weight
self.totalmoment = moment
return
"""
Returns rows of performance data formatted in HTML for use in the weight and balance PDF.
"""
def calcPerformance(self):
return
"""
Calculates the maximum range for airplane
** NOTE ** must include 30 - 45 min reserve fuel.
"""
def calcMaxRange(self):
return
"""
Generalized weight class with weight (lbs) and an arm (in) for CG calculations in any
airplane type.
"""
class Weight:
"""
Initializes weight with moment, arm, and optional identifier.
@type weight: float
@param weight: weight (in lbs)
@type arm: float
@param arm: moment arm (in)
"""
def __init__(self, weight, arm, num=0):
self.weight = weight
self.arm = arm
self.moment = self.weight*self.arm
return
"""
Shows weight, arm, and moment information.
"""
def __repr__(self):
return "Weight: w=%s, a=%s, m=%s" % (self.weight, self.arm, self.moment)
"""
Creates environment for PDF of weather.
@type icao: str
@param icao: airport code
@rtype Environment
@return environment with built in data
"""
def createEnvironment(icao):
metar = getWeather(icao)
return Environment(icao, metar)
"""
An environment can be used for weather, weight, balance, and performance calculations.
"""
class Environment:
"""
Set the environment parameters for given location.
@type location: str
@param location: airport code
@type metar: str
@param metar: predetermined metar
"""
def __init__(self, location, metar=""):
self.location = location # A point of interest (ex. airport)
self.metar = metar if not metar=="" else getWeather(location) # set METAR
if not self.metar == '':
self.weather = 'METAR'
else:
self.weather = 'NONE'
self.skyCond = 'unknown'
return
self.winddir, self.wind = getWind(self.location, self.metar)
self.time = Environment.getTime(self.metar)
self.altimeter = Environment.getAltimeter(self.metar)
self.visibility = Environment.getVisibility(self.metar)
self.clouds = Environment.getClouds(self.metar) # CB clouds are only type shown
self.elevation = getFieldElevation(self.location)
self.wx = Environment.getWx(self.metar, self.clouds, self.visibility)
self.skyCond = Environment.getSkyCond(self.metar, self.clouds, self.visibility, self.wx)
self.temp, self.dp = Environment.getTempDP(self.metar)
self.pa = Environment.getPA(self.elevation, self.altimeter)
self.da = Environment.getDA(self.pa, self.temp, self.elevation)
return
"""
Given METAR calculate the temperature and dewpoint.
@type metar: string
@param metar: METAR information
@rType tuple
@return temperature and dewpoint
"""
@classmethod
def getTempDP(cls, metar):
for item in metar.split():
if "/" in item and "FT" not in item and "SM" not in item:
temp = item.split("/")[0]
dp = item.split("/")[1]
return (temp, dp)
return (0, 0)
"""
Given METAR calculate the observation time.
@type metar: string
@param metar: METAR information
@rType string
@return the time of the METAR observation
"""
@classmethod
def getTime(cls, metar):
for item in metar.split():
if 'Z' == item[-1:]:
return item
return "000000Z"
"""
Given METAR return weather conditions/advisories.
@type metar: string
@param metar: METAR information
@type clouds: string
@param clouds: cloud conditions
@type visibility: int
@param visibility: visibility in SM
@rType list
@return list of weather advisories
"""
@classmethod
def getWx(cls, metar, clouds, visibility):
for item in metar.split():
if '-' in item or '+' in item:
return item
visInd = -1
for x in range(len(metar.split())):
if 'SM' in metar.split()[x]:
visInd = x
cloudInd = -1
for x in range(len(metar.split())):
if clouds[0] == metar.split()[x]:
cloudInd = x
if visInd + 1 == cloudInd:
return ""
# the weather is between visibility and cloud conditions (always present)
wx = metar.split()[visInd+1:cloudInd]
for item in wx:
if "R" in item[0] or "A" in item[0] or "RMK" in item:
wx.remove(item)
if len(wx) > 1:
print('length of wx > 1: %s; metar=%s' % (wx, metar))
return ""
return wx
"""
Determine whether the weather is within VFR requirements.
@type metar: string
@param metar: METAR information
@type clouds: string
@param clouds: cloud conditions
@type visibility: int
@param visibility: visibility in SM
@type wx: string
@param wx: weather advisories (ex. TS = thunderstorms)
@rType string
@return current weather conditions
"""
# incorporate airspace!!!
@classmethod
def getSkyCond(cls, metar, clouds, visibility, wx):
if 'TS' in wx:
return 'IFR' # should not fly VFR in vicinity of TS
if 'CLR' in str(clouds[0]) or 'SKC' in str(clouds[0]):
clouds[0] += "999" # makes the rest of determining the ceiling easier
if clouds == "CLR" and visibility > 3:
return 'VFR'
ceil = 100000
for item in clouds:
if 'BKN' in item or 'OVC' in item:
ceil = float(item[3:].replace('CB', ''))*100
break # ceiling is FIRST broken or overcast layer
if ceil > 3000 and visibility > 3:
return 'VFR'
elif ceil < 3000 and ceil > 1000 and \
visibility > 3 and visibility < 5:
return 'SVFR'
return 'IFR' # all other weather types are IFR or LIFR
"""
Given METAR return the altimeter.
@type metar: string
@param metar: METAR information
@rType float
@return altimeter setting
"""
@classmethod
def getAltimeter(cls, metar):
for item in metar.split():
if 'A' in item[0] and item[1:].isdigit():
return float(item[1:3] + "." + item[3:5])
return 29.92
"""
Given METAR return the visibility.
@type metar: string
@param metar: METAR information
@rType float
@return visibility
"""
@classmethod
def getVisibility(cls, metar):
for item in metar.split():
if 'SM'in item[-2:]:
try:
return int(float((item[:-2])))
except:
x = Fraction(item[:-2])
return int(x)
return 0
"""
Given METAR return the cloud type and ceiling.
@type metar: string
@param metar: METAR information
@rType float
@return cloud conditions
"""
@classmethod
def getClouds(cls, metar):
clouds = []
if "RMK" in metar:
usable = metar.split("RMK")[0]
else:
usable = metar
for item in usable.split():
if 'SKC' in item or 'CLR' in item or 'FEW' in item or \
'SCT' in item or 'BKN' in item or 'OVC' in item:
clouds.append(item)
if len(clouds) > 0:
return clouds
return "DATA ERROR"
"""
Calculate the pressure altitude.
@type elev: float
@param elev: field elevation
@rType float
@return the pressure altitude
"""
@classmethod
def getPA(cls, elev, altimeter):
press_diff = (altimeter - 29.92)*1000 # simple pressure altitude formula
return float(elev + press_diff)
"""
Calculate the density altitude.
See http://www.flyingmag.com/technique/tip-week/calculating-density-altitude-pencil.
@type PA: float
@param PA: pressure altitude
@type temp: float
@param temp: air temperature (degrees C)
@type alt: float
@param alt: altitude for calculation (usually field elevation)
"""
@classmethod
def getDA(cls, PA, temp, alt):
ISA = 15 - math.floor(float(alt)/1000)*2
return float(PA + 120*(float(temp)-ISA))
"""
Returns METAR (with almost all necessary information).
"""
def __repr__(self):
return self.metar
"""
A point of interest can be an airport, city, or latitude/longitude location. It is used as origin and destination info for Segments.
"""
class Point_Of_Interest:
def __init__(self, name, lat, lon, dist=-1, data="", setting="normal"):
self.name = name
self.dist = dist
self.lat = lat
self.lon = lon
self.priority = 0
try:
self.latlon = geopy.point.Point(lat, lon)
except:
print('Creating geopy point failed, trying float')
self.latlon = geopy.point.Point(float(lat), float(lon))
self.data = data
self.setting = setting
return
# an initial qualification
def hasFuel(self):
try:
if(self.unicom != ""):
self.hasFuel = True
except:
self.hasFuel = False
def __repr__(self):
return str(self.name) + ": " + str(self.dist)
"""
A route contains a list of segments and airplane parameters replated to a particular flight.
"""
class Route:
def __init__(self, course, origin, destination, routeType="direct", night = False, custom=[], \
cruising_alt=3500, cruise_speed=110, climb_speed=75, climb_dist=5, gph=10, descent_speed=90, doWeather=True, region="NORTHEAST"):
self.reset(course, origin, destination, routeType, night, custom, cruising_alt, cruise_speed, \
climb_speed, climb_dist, gph, descent_speed, doWeather=doWeather, region=region)
return
def reset(self, course, origin, destination, routeType, night, custom, cruising_alt, cruise_speed, \
climb_speed, climb_dist, gph, descent_speed, climb_done=False, doWeather=False, region="NORTHEAST"):
self.origin = origin
self.destination = destination
self.climb_speed = climb_speed
self.climb_dist = climb_dist # nm, depends on cruising altitude - should become dynamic
self.gph = gph
self.fuelTaxi = 1.4
self.routeType = routeType
self.night = night
self.errors = []
self.cruising_alt = cruising_alt
self.cruise_speed = cruise_speed
self.descent_speed = descent_speed
self.region = region
# perform route calculations
self.course = course
self.landmarks = custom
if(routeType.lower() is not "direct" or climb_done):
print('Not direct')
print('Creating segments')
self.courseSegs = createSegments(self.origin, self.destination, self.course, self.cruising_alt, self.cruise_speed, \
self.climb_speed, self.descent_speed, custom=custom, isCustom=True, doWeather=doWeather, region=self.region)
# using custom route or route with climb
else:
print('Direct')
self.courseSegs = createSegments(self.origin, self.destination, self.course, self.cruising_alt, self.cruise_speed, \
self.climb_speed, self.descent_speed, custom=custom, doWeather=doWeather, region=self.region)
for seg in self.courseSegs:
if seg.from_poi.name == seg.to_poi.name:
self.courseSegs.remove(seg)
time = 0
for item in self.courseSegs:
time += item.time
self.time = time
self.minutes = float('{0:.2f}'.format((self.time - math.floor(self.time))*60))
self.hours = math.floor(self.time)
self.calculateFuelTime()
return
"""
Takes a route and puts a climb in it
* TODO: insert climb before creating route.
"""
def insertClimb(self):
if(self.course[0] < self.climb_dist): # someone
print('Short course')
self.errors.append("Climb distance longer than route. Ignoring climb parameters.")
print("Climb distance longer than route. Ignoring climb parameters.")
# still adding landmarks
newLandmarks = []
newLandmarks.append(self.origin)
for x in range(len(self.courseSegs)):
newLandmarks.append(self.courseSegs[x].to_poi)
self.landmarks = newLandmarks
return
currentAlt = 0
currentDist = 0
remove = []
print('courseSegs check')
if(self.courseSegs[0].length < self.climb_dist):
for x in range(len(self.courseSegs)):
if(currentDist > self.climb_dist):
break
# climb distance is the LATERAL distance
currentDist += self.courseSegs[x].length
if "custom" not in self.courseSegs[x].to_poi.setting: # TODO: check if this should also be from_poi
remove.append(x)
newLandmarks = []
newLandmarks.append(self.origin)
# now add TOC
heading = self.courseSegs[0].course[1]
print('Heading = {}'.format(heading))
print(self.courseSegs[0])
print(self.courseSegs[0].course)
v_d = geopy.distance.VincentyDistance(kilometers = float(self.climb_dist)*nm_to_km)
offset_pt = v_d.destination(point=self.origin.latlon, bearing=heading)
offset = str((offset_pt.latitude, offset_pt.longitude))[1:-1]
offsetLatLon = (float(offset.split(", ")[0]), float(offset.split(", ")[1]))
offsetObj = Point_Of_Interest("TOC", offsetLatLon[0], offsetLatLon[1])
newLandmarks.append(offsetObj)
for x in range(len(self.courseSegs)):
if x not in remove:
newLandmarks.append(self.courseSegs[x].to_poi)
self.reset(self.course, self.origin, self.destination, self.routeType, self.night, newLandmarks, self.cruising_alt, \
self.cruise_speed, self.climb_speed, self.climb_dist, self.gph, self.descent_speed, climb_done = True, doWeather = True, region=self.region)
return
"""
String representation. Used for storing in database.
"""
def __repr__(self):
rep = ""
for item in self.courseSegs:
rep += item
"""
Calculates necessary amount of fuel for a flight.
* TODO: this method should be in Airplane class
"""
def calculateFuelTime(self): # fuel includes taxi; time does not
# needs refinement
self.fuelRequired = 0
self.time = 0
self.totalDist = 0
if self.night:
self.fuelRequired += 0.75*self.gph # 45 minute minimum reserve for night flights
else:
self.fuelRequired += 0.5*self.gph # 30 minute minimum reserve for day flights
for leg in self.courseSegs:
self.time += leg.time
self.fuelRequired += leg.time*self.gph
self.totalDist += leg.length
self.fuelRequired += self.fuelTaxi
return
"""
Segments, which comprise a route, contain individual altitudes, headings, origins, destinations, wind, and other relevant pieces of data.
"""
class Segment:
def __init__(self, from_poi, to_poi, true_hdg, alt, tas, isOrigin = False, isDest = False, num=0, aloft="0000+00"):
# initialize arguments
self.from_poi = from_poi # Airport object
self.to_poi = to_poi # Airport object
self.true_hdg = true_hdg # Float
self.course = getDistHeading(from_poi.latlon, to_poi.latlon)
self.true_hdg = self.course[1] # actual true heading!
self.alt = alt # Float
self.tas = tas # Float
self.isOrigin = isOrigin # Boolean
self.isDest = isDest # Boolean
self.num=num # integer
self.aloft = aloft
# initialize complex data
self.length = geopy.distance.distance(from_poi.latlon, to_poi.latlon).kilometers * km_to_nm # important! convert to miles
self.magCorrect()
self.getWindS()
self.setCorrectedCourse()
self.setGS()
# time
self.time = self.length/self.gs # distance/rate=time; in hours
self.minutes = float("{0:.2f}".format((self.time - math.floor(self.time))*60))
self.hours = math.floor(self.time)
self.totMinutes = self.time*60
self.seg_hdg = float("{0:.2f}".format(getGeopyHeading(from_poi.latlon, to_poi.latlon)))
if(self.seg_hdg < 0):
self.seg_hdg += 360
return
"""
Correct for magnetic deviation.
"""
def magCorrect(self):
self.mag_hdg = mag_heading(float(self.true_hdg), float(self.from_poi.lat), float(self.from_poi.lon)) # Get the magnetic heading
self.mag_var = float("{0:.2f}".format(getHeadingDiff(self.true_hdg, self.mag_hdg)))
"""
Calculates and sets wind correction angle.
"""
def setCorrectedCourse(self):
wca = Segment.calcWindCorrectionAngle(self.true_hdg, self.tas, self.w, self.vw)
self.wca = float("{0:.2f}".format(wca))
self.hdg = float("{0:.2f}".format(self.mag_hdg + wca))
return
"""
Calculates ground speed.
"""
def setGS(self):
self.gs = Segment.calcGroundSpeed(self.true_hdg, self.tas, self.w, self.vw)
"""
Gets the wind for the segment.
"""
def getWindS(self):
if(self.isOrigin or self.alt == 0):
self.w, self.vw = getWind(self.from_poi.name)
else:
aloft = str(self.aloft)
self.w = 10*float(aloft[:2]) # only 2 digits
self.vw = float(aloft[2:4])
if(len(aloft) > 4):
self.temp = float(aloft[4:])
return
"""
Gets segment data to display to user.
"""
def getData(self):
return [self.from_poi.name, self.to_poi.name, str("{0:.2f}".format(self.length)), str(self.alt), str(self.tas), "{0:.2f}".format(float(self.gs)), str("{0:.2f}".format(self.totMinutes))]
"""
Converts segment to table entry.
@type num: int
@param num: the segment number (used for form)
@rtype string
@return string representation of segment
"""
def convertToString(self, num): # for custom route planning
try:
return "<td>" + self.from_poi.name + "</td><td>→</td><td>" + "<form action=\"/update\" method=\"post\"><input type='text' value='" + \
self.to_poi.name + "' name=\"to\" readonly='false' ondblclick=\"this.readOnly='';\"> <input type=\"hidden\" name=\"num\" value=\"" + \
str(num) + "\"> </form> " + "</td><td>" + str("{0:.2f}".format(self.length*km_to_nm))+ "</td><td>" + str(self.alt) + "</td><td>" + \
str(self.tas) + "</td><td>" + str(self.gs) + "</td><td>" + str(self.hdg) + "</td>"
except Exception as e:
print(str(e))
"""
Visual representation of segment.
"""
def __repr__(self):
return self.from_poi.name + " -> " + self.to_poi.name + " (" + str("{0:.2f}".format(self.length*km_to_nm)) + " mi, " + str(self.time) + " hrs); " + str(self.alt) + " @ " + str(self.tas) + " kt. GS=" + str(self.gs) + "; CH=" + str(self.hdg) + "."
@classmethod
def calcWindCorrectionAngle(self, d, va, w, vw): # d is desired course, va true airspeed, w wind direction, vw wind speed
# https://en.wikipedia.org/wiki/E6B
va = float(va)
vw = float(vw)
d = float(d)
w = float(w)
ratio = vw/va
return math.degrees(math.asin(ratio*math.sin(math.radians(w-d))))
@classmethod
def calcGroundSpeed(self, d, va, w, vw):
va = float(va)
vw = float(vw)
d = float(d)
w = float(w)
# https://en.wikipedia.org/wiki/E6B
return math.sqrt(math.pow(va, 2) + math.pow(vw, 2) - 2*va*vw*math.cos(math.pi*(d-w+self.calcWindCorrectionAngle(d, va, w, vw))/180))
"""
Retreives the METAR information from a particular airport.
@type loc: Point_Of_Interest
@param loc: Airport used to get weather
@rtype str
@return full METAR information
"""
def getWeather(loc):
if loc == "":
return ""
try:
url = 'http://www.aviationweather.gov/adds/metars/?station_ids=%s&std_trans=standard&chk_metars=on&hoursStr=most+recent+only&submitmet=Submit' % (loc)
print('Weather here: {}'.format(loc))
page = urllib.request.urlopen(url, timeout=SHORT_TIMEOUT).read()
soup = BeautifulSoup(page, features="html5lib")
found = soup.find_all('font') # METAR data within font tag
print('Done')
if len(found) == 1:
found = found[0] # will only work if so
else:
return ''
wx = str(found).split('>')[1].split('<')[0]
return wx
except Exception as e:
print('Weather fail: {}'.format(e))
return ''
"""
Finds the wind at a particular airport.
@type loc: Point_Of_Interest
@param loc: Airport used to get weather
@rtype tuple
@return tuple of wind direction and strength
"""
def getWind(loc, metar=""):
if not metar=="":
for item in metar.split():
if "KT" in item:
winddir = item[0:3]
windstrength = item[3:5]
return (winddir, windstrength)
weather = getWeather(loc)
if weather == "":
return (0, 0)
wind = ()
for item in weather.split():
if "CALM" in item:
return (0, 0)
if "KT" in item:
winddir = item[0:3]
windstrength = item[3:5]
if ("VRB" in winddir): # cannot set particular direction or speed for variable wind
return (0, 0)
wind = (winddir, windstrength)
return wind
"""
Pulls from all winds aloft sources on aviationweather.gov.
@type lat: float
@param lon: Latitude to find winds aloft
@type lat: float
@param lon: Longitude to find winds aloft
@type alt: float
@param alt: Altitude to find winds aloft
@rtype str
@return winds aloft (direction and velocity)
"""
def getWindsAloft(lat, lon, alt, region):
loc = Point_Of_Interest("windLoc", lat, lon)
# base url for winds aloft: 'https://aviationweather.gov/products/nws/__location__'
# all urls: ['https://aviationweather.gov/products/nws/boston', 'https://aviationweather.gov/products/nws/chicago',
# 'https://aviationweather.gov/products/nws/saltlakecity', 'https://aviationweather.gov/products/nws/sanfrancisco',
# 'https://aviationweather.gov/products/nws/miami', 'https://aviationweather.gov/products/nws/ftworth']
found = []
if "NORTHEAST" in region:
urls = ['https://aviationweather.gov/products/nws/boston']
elif "SOUTHEAST" in region:
urls = ['https://aviationweather.gov/products/nws/miami']
elif "GULF" in region:
urls = ['https://aviationweather.gov/products/nws/ftworth']
elif "WEST" in region:
urls = ['https://aviationweather.gov/products/nws/sanfrancisco']
elif "WESTCENT" in region:
urls = ['https://aviationweather.gov/products/nws/saltlakecity']
elif "LAKES" in region:
urls = ['https://aviationweather.gov/products/nws/chicago']
else:
return "0000"
# all winds aloft information
for url in urls:
page = urllib.request.urlopen(url, timeout=SHORT_TIMEOUT).read()
soup = BeautifulSoup(page, features="html5lib")
found += soup.find_all('pre')
windLocs = []
for line in str(found).split("\n"):
if "pre" in line or "VALID" in line:
continue
counter = 0
# ignore winds aloft that do not have the full data by counting the number of pieces of data
for item in line.split(" "):
if(item.strip() is not ""):
counter += 1
if(counter < 10):
continue
try:
airpt = str(line.split()[0])
latlon = getLatLon(airpt)
# put the winds aloft data in the airport object (speeds up later)
windLocs.append(Point_Of_Interest(airpt, latlon[0], latlon[1], data=line))
except:
continue
for item in windLocs:
item.dist = geopy.distance.distance(item.latlon, loc.latlon).kilometers * km_to_nm
sortedAirports = sorted(windLocs, key=lambda x: x.dist, reverse=False)
print('WIND')
dataLine = sortedAirports[0].data.split(" ")
alt = float(alt)
# information for winds aloft data - these are the altitude thresholds for each observation
# FT 3000 6000 9000 12000 18000 24000 30000 34000 39000
data = ""
if alt >= 0 and alt < 4500: # 3000
data = dataLine[1]
elif alt >= 4500 and alt < 7500: # 6000
data = dataLine[2]
elif alt >= 7500 and alt < 10500: # 9000
data = dataLine[3]
elif alt >= 10500 and alt < 15000: # 12000
data = dataLine[4]
elif alt >= 15000 and alt < 21000: # 18000
data = dataLine[5]
elif alt >= 21000 and alt < 27000: # 24000
data = dataLine[6]
elif alt >= 27000 and alt < 32000: # 30000
data = dataLine[7]
elif alt >= 32000 and alt < 36500: #34000
data = dataLine[8]
elif alt >= 36500 and alt < 40000: #34000
data = dataLine[9]
else:
data = "0000"
if "9900" in data: # light and variable
data = "0000"
print(data)
return data
"""
Finds the distance and heading between two locations.
@type poi1: Point_Of_Interest
@param poi1: Origin point
@type poi2: Point_Of_Interest
@param poi2: Destination point
@rtype tuple
@return distance and heading in tuple
"""
def getDistHeading(poi1, poi2):
try:
# d = geopy.distance.distance(poi1, poi2).kilometers * km_to_nm
d = geopy_cache_dist(poi1, poi2)
h = getGeopyHeading(poi1, poi2)
if h < 0:
h += 360 # sometimes gives negative headings which screws things up
return (d, h)
except Exception as e:
print('error getting headings')
print(e)
return (float("inf"), 0) #should be out of range, but need better fix
"""
Finds the distance between two airports (not POIs).
@type icao1: string
@param icao1: origin airport
@type icao2: string
@param icao2: destination airport
@rtype float
@return distance
"""
def getDist(icao1, icao2):
ll1 = getLatLon(icao1)
ll2 = getLatLon(icao2)
latlon1 = geopy.Point(ll1[0], ll1[1])
latlon2 = geopy.Point(ll2[0], ll2[1])
# d = geopy.distance.distance(latlon1, latlon2).kilometers * km_to_nm
d = geopy_cache_dist(latlon1, latlon2)
print("route d: " + str(d))
return d
"""
Finds latitude and longitude of airport from file.
@type icao: str
@param icao: airport code
@rtype tuple
@return latitude and longitude
"""
def getLatLon(icao):
coords = ()
with open("data/airports.txt") as f: # search in all airports, but use lare ones for landmarks
lines = f.readlines()
for line in lines:
data = line.split(", ")
if icao in data[0]: #check vs ==
coords = (data[1],data[2])
else:
continue
return coords
"""
Finds the landmarks that are in range of an origin point.
@type origin: Point_Of_Interest
@param origin: origin location
@type dest: Point_Of_Interest
@param dest: destination location
@type course: tuple
@param course: course heading and distance
@rtype list
@return list of Point_Of_Interest
"""
def getDistancesInRange2(originLoc, max_dist):
distances = []
with open('data/newairports_2.txt') as f:
lines = f.readlines()
for line in lines:
data = line.split(", ")
if(len(data) < 2):
continue
data[2] = data[2].replace('\n', '')
data_ll = (float(data[1]), float(data[2]))
lat_diff_deg = float(data[1]) - originLoc.latitude
lon_diff_deg = float(data[2]) - originLoc.longitude
if abs(lat_diff_deg * nm_per_deg) > max_dist \
or abs(lon_diff_deg * nm_per_deg) > max_dist:
continue
temp = geopy.point.Point(data_ll[0], data_ll[1])
tempDist = geopy_cache_dist(originLoc, temp)
if (tempDist < max_dist):
distances.append(Point_Of_Interest(data[0], data[1], data[2], tempDist))
with open('data/cities.txt') as f:
lines = f.readlines()
city_names = set()
for line in lines:
data = line.split(", ")
city_name = data[0]
if city_name in city_names:
continue
city_names.add(city_name)
data[2] = data[2].replace('\n', '')
if (len(data) < 3):
continue
lat_diff_deg = float(data[1]) - originLoc.latitude
lon_diff_deg = float(data[2]) - originLoc.longitude
if abs(lat_diff_deg * nm_per_deg) > max_dist \
or abs(lon_diff_deg * nm_per_deg) > max_dist:
continue
temp = geopy.Point(data[1], data[2])
tempDist = geopy_cache_dist(originLoc, temp)
if (tempDist < max_dist):
distances.append(Point_Of_Interest(data[0], data[1], data[2], tempDist))
return distances
"""
Calculates the difference between two headings.
@type h1: float
@param h1: first heading
@type h2: float
@param h2: second heading
@rtype tuple
@return latitude and longitude
"""
def getHeadingDiff(h1, h2):
diff = h2 - h1
absDiff = abs(diff)
if(absDiff <= 180):
if(absDiff == 180):
return absDiff
return diff
elif (h2 > h1):
return absDiff - 360
return 360 - absDiff
"""
Return heading from poi1 (geopy Point) to poi2.
"""
def getGeopyHeading(poi1, poi2):
cache = current_app.cache
cache_str = 'hdg_from_{}_to_{}'.format(poi1, poi2)
res = cache.get(cache_str)
if res:
return res
lon2 = poi2.longitude
lon1 = poi1.longitude
lat2 = poi2.latitude
lat1 = poi1.latitude
dLon = lon2 - lon1
hdg = Geodesic.WGS84.Inverse(lat1, lon1, lat2, lon2)['azi1']
cache.set(cache_str, hdg, timeout=300)
return hdg
def geopy_cache_dist(poi1, poi2):
cache = current_app.cache
cache_str = 'dist_from_{}_to_{}'.format(poi1, poi2)
res = cache.get(cache_str)
if res:
return res
tempDist = geopy.distance.distance(poi1, poi2).kilometers * km_to_nm
cache.set(cache_str, tempDist, timeout=300)
return tempDist
"""
Determines if a location can be used as a subsequent landmark from a base point (ex. origin to first waypoint).
@type base: Point_Of_Interest
@param base: base location to check if landmark is valid
@type poi: Point_Of_Interest
@param poi: landmark to check
@type course: tuple
@param course: distance and heading of entire course
@type tolerance: float
@param tolerance: tolerance (slowly increased) for finding landmarks
@rtype boolean
@return whether landmark is valid
"""
def isValidLandmark(base, poi, course, tolerance):
l1 = base.latlon
l2 = poi.latlon
# tempDist = geopy_cache_dist(l1, l2)
heading = getGeopyHeading(l1, l2)
# base = 10 if course[0] > 250 else 40 # tuning parameter
# if (tempDist < base * (1 / tolerance) or tempDist > (base * 2.5) * tolerance): # check tolerance math