1 |
|
---|
2 | from .util import utf2unicode
|
---|
3 | from .flight import Flight
|
---|
4 | from .common import fixUnpickled
|
---|
5 |
|
---|
6 | from . import const
|
---|
7 | import pickle as pickle
|
---|
8 | import calendar
|
---|
9 | import datetime
|
---|
10 | import time
|
---|
11 |
|
---|
12 | #------------------------------------------------------------------------------
|
---|
13 |
|
---|
14 | ## @package mlx.pirep
|
---|
15 | #
|
---|
16 | # The PIREP module.
|
---|
17 | #
|
---|
18 | # This module defines only one class, \ref PIREP. It is used to extract and
|
---|
19 | # store the information needed for a PIREP. The saved PIREPs are pickled
|
---|
20 | # instances of this class.
|
---|
21 |
|
---|
22 | #------------------------------------------------------------------------------
|
---|
23 |
|
---|
24 | class PIREP(object):
|
---|
25 | """A pilot's report of a flight."""
|
---|
26 | class Message(object):
|
---|
27 | """A message belonging to the PIREP."""
|
---|
28 | @staticmethod
|
---|
29 | def fromMessageData(messageData):
|
---|
30 | """Construct a message from a JSON message data."""
|
---|
31 | message = messageData["message"]
|
---|
32 | senderPID = messageData["senderPID"]
|
---|
33 | senderName = messageData["senderName"]
|
---|
34 |
|
---|
35 | return PIREP.Message(message, senderPID, senderName)
|
---|
36 |
|
---|
37 | def __init__(self, message, senderPID, senderName):
|
---|
38 | """Construct the message object."""
|
---|
39 | self.message = message
|
---|
40 | self.senderPID = senderPID
|
---|
41 | self.senderName = senderName
|
---|
42 |
|
---|
43 | _flightTypes = { const.FLIGHTTYPE_SCHEDULED : "SCHEDULED",
|
---|
44 | const.FLIGHTTYPE_OLDTIMER : "OT",
|
---|
45 | const.FLIGHTTYPE_VIP : "VIP",
|
---|
46 | const.FLIGHTTYPE_CHARTER : "CHARTER" }
|
---|
47 |
|
---|
48 | @staticmethod
|
---|
49 | def _formatLine(timeStr, line):
|
---|
50 | """Format the given time string and line as needed for the ACARS and
|
---|
51 | some other things."""
|
---|
52 | return "[" + timeStr + "]-[" + line + "]"
|
---|
53 |
|
---|
54 | @staticmethod
|
---|
55 | def formatTimestampForRPC(t):
|
---|
56 | """Format the given timestamp for RPC."""
|
---|
57 | return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(t))
|
---|
58 |
|
---|
59 | @staticmethod
|
---|
60 | def parseTimestampFromRPC(s):
|
---|
61 | """Format the given timestamp for RPC."""
|
---|
62 | dt = datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
|
---|
63 | return calendar.timegm(dt.utctimetuple())
|
---|
64 |
|
---|
65 | @staticmethod
|
---|
66 | def decodeFlightTypeText(s):
|
---|
67 | """Decode the given flight type text."""
|
---|
68 | for (flighType, text) in PIREP._flightTypes.items():
|
---|
69 | if s==text:
|
---|
70 | return flighType
|
---|
71 | return const.FLIGHTTYPE_SCHEDULED
|
---|
72 |
|
---|
73 | @staticmethod
|
---|
74 | def parseLogFromRPC(log):
|
---|
75 | """Parse the given log coming from the RPC."""
|
---|
76 | index = 0
|
---|
77 | entries = []
|
---|
78 |
|
---|
79 | inTimeStr = False
|
---|
80 | inEntry = False
|
---|
81 |
|
---|
82 | timestr = ""
|
---|
83 | entry = ""
|
---|
84 |
|
---|
85 | while index<len(log):
|
---|
86 | c = log[index]
|
---|
87 | index += 1
|
---|
88 |
|
---|
89 | if c==']':
|
---|
90 | if inEntry:
|
---|
91 | entries.append((timestr, entry))
|
---|
92 | timestr = ""
|
---|
93 | entry = ""
|
---|
94 |
|
---|
95 | inTimeStr = False
|
---|
96 | inEntry = False
|
---|
97 | elif not inTimeStr and not inEntry:
|
---|
98 | if c=='[':
|
---|
99 | if timestr:
|
---|
100 | inEntry = True
|
---|
101 | else:
|
---|
102 | inTimeStr = True
|
---|
103 | elif inTimeStr:
|
---|
104 | timestr += c
|
---|
105 | elif inEntry:
|
---|
106 | entry += c
|
---|
107 |
|
---|
108 | return entries
|
---|
109 |
|
---|
110 | @staticmethod
|
---|
111 | def load(path):
|
---|
112 | """Load a PIREP from the given path.
|
---|
113 |
|
---|
114 | Returns the PIREP object, or None on error."""
|
---|
115 | try:
|
---|
116 | with open(path, "rb") as f:
|
---|
117 | pirep = pickle.load(f, fix_imports = True, encoding = "bytes")
|
---|
118 | if "numCabinCrew" not in dir(pirep):
|
---|
119 | if "numCrew" not in dir(pirep):
|
---|
120 | pirep.numCabinCrew = pirep.bookedFlight.numCabinCrew
|
---|
121 | else:
|
---|
122 | pirep.numCabinCrew = pirep.bookedFlight.numCrew
|
---|
123 | if "numPassengers" not in dir(pirep):
|
---|
124 | pirep.numPassengers = pirep.bookedFlight.numPassengers
|
---|
125 | if "numChildren" not in dir(pirep):
|
---|
126 | pirep.numChildren = 0
|
---|
127 | if "numInfants" not in dir(pirep):
|
---|
128 | pirep.numInfants = 0
|
---|
129 | if "bagWeight" not in dir(pirep):
|
---|
130 | pirep.bagWeight = pirep.bookedFlight.bagWeight
|
---|
131 | if "mailWeight" not in dir(pirep):
|
---|
132 | pirep.mailWeight = pirep.bookedFlight.mailWeight
|
---|
133 | return pirep
|
---|
134 | except Exception as e:
|
---|
135 | print("Failed loading PIREP from %s: %s" % (path,
|
---|
136 | utf2unicode(str(e))))
|
---|
137 | return None
|
---|
138 |
|
---|
139 | def __init__(self, flight):
|
---|
140 | """Initialize the PIREP from the given flight."""
|
---|
141 | if flight is None:
|
---|
142 | return
|
---|
143 |
|
---|
144 | self.bookedFlight = flight.bookedFlight
|
---|
145 |
|
---|
146 | self.numCabinCrew = flight.numCabinCrew
|
---|
147 | self.numPassengers = flight.numPassengers
|
---|
148 | self.numChildren = flight.numChildren
|
---|
149 | self.numInfants = flight.numInfants
|
---|
150 | self.bagWeight = flight.bagWeight
|
---|
151 | self.cargoWeight = flight.cargoWeight
|
---|
152 | self.mailWeight = flight.mailWeight
|
---|
153 |
|
---|
154 | self.filedCruiseAltitude = flight.filedCruiseAltitude
|
---|
155 | self.cruiseAltitude = flight.cruiseAltitude
|
---|
156 | self.route = flight.route
|
---|
157 |
|
---|
158 | self.departureMETAR = flight.departureMETAR.upper()
|
---|
159 | self.arrivalMETAR = flight.arrivalMETAR.upper()
|
---|
160 |
|
---|
161 | self.departureRunway = flight.departureRunway.upper()
|
---|
162 | self.sid = flight.sid.upper()
|
---|
163 |
|
---|
164 | self.star = flight.star
|
---|
165 | self.transition = flight.transition
|
---|
166 | self.approachType = flight.approachType.upper()
|
---|
167 | self.arrivalRunway = flight.arrivalRunway.upper()
|
---|
168 |
|
---|
169 | self.flightType = flight.flightType
|
---|
170 | self.online = flight.online
|
---|
171 |
|
---|
172 | self.comments = flight.comments
|
---|
173 | self.flightDefects = flight.flightDefects
|
---|
174 | self.delayCodes = flight.delayCodes
|
---|
175 |
|
---|
176 | self.blockTimeStart = flight.blockTimeStart
|
---|
177 | self.flightTimeStart = flight.flightTimeStart
|
---|
178 | self.flightTimeEnd = flight.flightTimeEnd
|
---|
179 | self.blockTimeEnd = flight.blockTimeEnd
|
---|
180 | self.flownDistance = flight.flownDistance
|
---|
181 | self.fuelUsed = flight.startFuel - flight.endFuel
|
---|
182 |
|
---|
183 | logger = flight.logger
|
---|
184 | self.rating = logger.getRating()
|
---|
185 | self.logLines = logger.lines
|
---|
186 | self.faultLineIndexes = logger.faultLineIndexes
|
---|
187 |
|
---|
188 | self.messages = []
|
---|
189 |
|
---|
190 | def setupFromPIREPData(self, pirepData, bookedFlight):
|
---|
191 |
|
---|
192 | self.bookedFlight = bookedFlight
|
---|
193 |
|
---|
194 | self.numCabinCrew = int(pirepData["numCabinCrew"])
|
---|
195 | self.numPassengers = int(pirepData["numPassengers"])
|
---|
196 | self.numChildren = int(pirepData["numChildren"])
|
---|
197 | self.numInfants = int(pirepData["numInfants"])
|
---|
198 | self.bagWeight = int(pirepData["bagWeight"])
|
---|
199 | self.cargoWeight = int(pirepData["cargoWeight"])
|
---|
200 | self.mailWeight = int(pirepData["mailWeight"])
|
---|
201 |
|
---|
202 | filedCruiseLevel = pirepData["filedCruiseLevel"].strip()
|
---|
203 | if filedCruiseLevel:
|
---|
204 | if filedCruiseLevel.startswith("FL"):
|
---|
205 | filedCruiseLevel = filedCruiseLevel[2:]
|
---|
206 | if filedCruiseLevel:
|
---|
207 | self.filedCruiseAltitude = int(filedCruiseLevel)*100
|
---|
208 | else:
|
---|
209 | self.filedCruiseAltitude = 10000;
|
---|
210 |
|
---|
211 | cruiseLevel = pirepData["cruiseLevel"].strip()
|
---|
212 | if cruiseLevel:
|
---|
213 | if cruiseLevel.startswith("FL"):
|
---|
214 | cruiseLevel = cruiseLevel[2:]
|
---|
215 | if cruiseLevel:
|
---|
216 | self.cruiseAltitude = int(cruiseLevel[2:])*100
|
---|
217 | else:
|
---|
218 | self.cruiseAltitude = self.filedCruiseAltitude
|
---|
219 | self.route = pirepData["route"]
|
---|
220 |
|
---|
221 | self.departureMETAR = pirepData["departureMETAR"]
|
---|
222 | self.arrivalMETAR = pirepData["arrivalMETAR"]
|
---|
223 |
|
---|
224 | self.departureRunway = pirepData["departureRunway"]
|
---|
225 | self.sid = pirepData["sid"]
|
---|
226 |
|
---|
227 | star = pirepData["star"].split(",")
|
---|
228 | self.star = star[0]
|
---|
229 | self.star.strip()
|
---|
230 |
|
---|
231 | if len(star)>1:
|
---|
232 | self.transition = star[1]
|
---|
233 | self.transition.strip()
|
---|
234 | else:
|
---|
235 | self.transition = ""
|
---|
236 | self.approachType = pirepData["approachType"]
|
---|
237 | self.arrivalRunway = pirepData["arrivalRunway"]
|
---|
238 |
|
---|
239 | self.flightType = PIREP.decodeFlightTypeText(pirepData["flightType"])
|
---|
240 | self.online = int(pirepData["online"])!=0
|
---|
241 |
|
---|
242 | self.comments = pirepData["comments"]
|
---|
243 | self.flightDefects = pirepData["flightDefects"]
|
---|
244 | self.delayCodes = pirepData["timeComment"]
|
---|
245 | if self.delayCodes=="UTC":
|
---|
246 | self.delayCodes = []
|
---|
247 | else:
|
---|
248 | self.delayCodes = self.delayCodes.split(", ")
|
---|
249 |
|
---|
250 | flightDate = pirepData["flightDate"] + " "
|
---|
251 |
|
---|
252 | self.blockTimeStart = \
|
---|
253 | PIREP.parseTimestampFromRPC(flightDate + pirepData["blockTimeStart"])
|
---|
254 | self.flightTimeStart = \
|
---|
255 | PIREP.parseTimestampFromRPC(flightDate + pirepData["flightTimeStart"])
|
---|
256 | self.flightTimeEnd = \
|
---|
257 | PIREP.parseTimestampFromRPC(flightDate + pirepData["flightTimeEnd"])
|
---|
258 | self.blockTimeEnd = \
|
---|
259 | PIREP.parseTimestampFromRPC(flightDate + pirepData["blockTimeEnd"])
|
---|
260 | self.flownDistance = float(pirepData["flownDistance"])
|
---|
261 | self.fuelUsed = float(pirepData["fuelUsed"])
|
---|
262 |
|
---|
263 | # logger = flight.logger
|
---|
264 | self.rating = float(pirepData["rating"])
|
---|
265 |
|
---|
266 | log = pirepData["log"]
|
---|
267 |
|
---|
268 | self.logLines = PIREP.parseLogFromRPC(log)[1:]
|
---|
269 | if self.logLines and \
|
---|
270 | (self.logLines[0][0]=="LOGGER NG LOG" or
|
---|
271 | self.logLines[0][0]=="MAVA LOGGER X"):
|
---|
272 | self.logLines = self.logLines[1:]
|
---|
273 | numLogLines = len(self.logLines)
|
---|
274 |
|
---|
275 | lastFaultLineIndex = 0
|
---|
276 | self.faultLineIndexes = []
|
---|
277 | for ratingText in pirepData["ratingText"].splitlines()[:-1]:
|
---|
278 | faultLines = PIREP.parseLogFromRPC(ratingText)
|
---|
279 | for (timeStr, entry) in faultLines:
|
---|
280 | for i in range(lastFaultLineIndex, numLogLines-1):
|
---|
281 | if timeStr>=self.logLines[i][0] and \
|
---|
282 | timeStr<self.logLines[i+1][0]:
|
---|
283 | self.logLines = self.logLines[:i+1] + \
|
---|
284 | [(timeStr, entry)] + self.logLines[i+1:]
|
---|
285 | self.faultLineIndexes.append(i+1)
|
---|
286 | lastFaultLineIndex = i+1
|
---|
287 | numLogLines += 1
|
---|
288 | break
|
---|
289 |
|
---|
290 | self.messages = []
|
---|
291 | for messageData in pirepData["messages"]:
|
---|
292 | self.messages.append(PIREP.Message.fromMessageData(messageData))
|
---|
293 |
|
---|
294 | @property
|
---|
295 | def flightDateText(self):
|
---|
296 | """Get the text version of the booked flight's departure time."""
|
---|
297 | return self.bookedFlight.departureTime.strftime("%Y-%m-%d")
|
---|
298 |
|
---|
299 | @property
|
---|
300 | def flightTypeText(self):
|
---|
301 | """Get the text representation of the flight type."""
|
---|
302 | return PIREP._flightTypes[self.flightType]
|
---|
303 |
|
---|
304 | @property
|
---|
305 | def blockTimeStartText(self):
|
---|
306 | """Get the beginning of the block time in string format."""
|
---|
307 | return PIREP.formatTimestampForRPC(self.blockTimeStart)
|
---|
308 |
|
---|
309 | @property
|
---|
310 | def flightTimeStartText(self):
|
---|
311 | """Get the beginning of the flight time in string format."""
|
---|
312 | return PIREP.formatTimestampForRPC(self.flightTimeStart)
|
---|
313 |
|
---|
314 | @property
|
---|
315 | def flightTimeEndText(self):
|
---|
316 | """Get the end of the flight time in string format."""
|
---|
317 | return PIREP.formatTimestampForRPC(self.flightTimeEnd)
|
---|
318 |
|
---|
319 | @property
|
---|
320 | def blockTimeEndText(self):
|
---|
321 | """Get the end of the block time in string format."""
|
---|
322 | return PIREP.formatTimestampForRPC(self.blockTimeEnd)
|
---|
323 |
|
---|
324 | def getACARSText(self):
|
---|
325 | """Get the ACARS text.
|
---|
326 |
|
---|
327 | This is a specially formatted version of the log without the faults."""
|
---|
328 | text = "[MAVA LOGGER X LOG]-[%s]" % (const.VERSION,)
|
---|
329 | for index in range(0, len(self.logLines)):
|
---|
330 | if index not in self.faultLineIndexes:
|
---|
331 | (timeStr, line) = self.logLines[index]
|
---|
332 | if timeStr is not None:
|
---|
333 | text += PIREP._formatLine(timeStr, line)
|
---|
334 | return text
|
---|
335 |
|
---|
336 | def getRatingText(self):
|
---|
337 | """Get the rating text.
|
---|
338 |
|
---|
339 | This is a specially formatted version of the lines containing the
|
---|
340 | faults."""
|
---|
341 | text = ""
|
---|
342 | for index in self.faultLineIndexes:
|
---|
343 | (timeStr, line) = self.logLines[index]
|
---|
344 | if timeStr is not None:
|
---|
345 | text += PIREP._formatLine(timeStr, line)
|
---|
346 | text += "\n"
|
---|
347 |
|
---|
348 | text += "\n[Flight Rating: %.1f]" % (max(0.0, self.rating),)
|
---|
349 |
|
---|
350 | return text
|
---|
351 |
|
---|
352 | def getTimeComment(self):
|
---|
353 | """Get the time comment.
|
---|
354 |
|
---|
355 | This is basically a collection of the delay codes, if any."""
|
---|
356 | if not self.delayCodes:
|
---|
357 | return "UTC"
|
---|
358 | else:
|
---|
359 | s = ""
|
---|
360 | for code in self.delayCodes:
|
---|
361 | if s: s += ", "
|
---|
362 | s += code
|
---|
363 | return s
|
---|
364 |
|
---|
365 | def getSTAR(self):
|
---|
366 | """Get the STAR and/or the transition."""
|
---|
367 | star = self.star if self.star is not None else ""
|
---|
368 | if self.transition is not None:
|
---|
369 | if star: star += ", "
|
---|
370 | star += self.transition
|
---|
371 | return star.upper()
|
---|
372 |
|
---|
373 | def save(self, path):
|
---|
374 | """Save the PIREP to the given file.
|
---|
375 |
|
---|
376 | Returns whether the saving has succeeded."""
|
---|
377 | try:
|
---|
378 | with open(path, "wb") as f:
|
---|
379 | pickle.dump(self, f)
|
---|
380 | return None
|
---|
381 | except Exception as e:
|
---|
382 | error = utf2unicode(str(e))
|
---|
383 | print("Failed saving PIREP to %s: %s" % (path, error))
|
---|
384 | return error
|
---|
385 |
|
---|
386 | def _serialize(self):
|
---|
387 | """Serialize the PIREP for JSON-RPC."""
|
---|
388 | attrs = {}
|
---|
389 | attrs["log"] = self.getACARSText()
|
---|
390 | attrs["flightDate"] = self.flightDateText
|
---|
391 | attrs["callsign"] = self.bookedFlight.callsign
|
---|
392 | attrs["departureICAO"] = self.bookedFlight.departureICAO
|
---|
393 | attrs["arrivalICAO"] = self.bookedFlight.arrivalICAO
|
---|
394 | attrs["numPassengers"] = self.numPassengers
|
---|
395 | attrs["numChildren"] = self.numChildren
|
---|
396 | attrs["numInfants"] = self.numInfants
|
---|
397 | attrs["numCabinCrew"] = self.numCabinCrew
|
---|
398 | attrs["cargoWeight"] = self.cargoWeight
|
---|
399 | attrs["bagWeight"] = self.bagWeight
|
---|
400 | attrs["mailWeight"] = self.mailWeight
|
---|
401 | attrs["flightType"] = self.flightTypeText
|
---|
402 | attrs["online"] = 1 if self.online else 0
|
---|
403 | attrs["blockTimeStart"] = self.blockTimeStartText
|
---|
404 | attrs["blockTimeEnd"] = self.blockTimeEndText
|
---|
405 | attrs["flightTimeStart"] = self.flightTimeStartText
|
---|
406 | attrs["flightTimeEnd"] = self.flightTimeEndText
|
---|
407 | attrs["timeComment"] = self.getTimeComment()
|
---|
408 | attrs["fuelUsed"] = self.fuelUsed
|
---|
409 | attrs["departureRunway"] = self.departureRunway
|
---|
410 | attrs["arrivalRunway"] = self.arrivalRunway
|
---|
411 | attrs["departureMETAR"] = self.departureMETAR
|
---|
412 | attrs["arrivalMETAR"] = self.arrivalMETAR
|
---|
413 | attrs["filedCruiseLevel"] = self.filedCruiseAltitude / 100.0
|
---|
414 | attrs["cruiseLevel"] = self.cruiseAltitude / 100.0
|
---|
415 | attrs["sid"] = self.sid
|
---|
416 | attrs["route"] = self.route
|
---|
417 | attrs["star"] = self.star
|
---|
418 | attrs["approachType"] = self.approachType
|
---|
419 | attrs["comments"] = self.comments
|
---|
420 | attrs["flightDefects"] = self.flightDefects
|
---|
421 | attrs["ratingText"] = self.getRatingText()
|
---|
422 | attrs["rating"] = max(0.0, self.rating)
|
---|
423 | attrs["flownDistance"] = "%.2f" % (self.flownDistance,)
|
---|
424 | # FIXME: it should be stored in the PIREP when it is sent later
|
---|
425 | attrs["performDate"] = datetime.date.today().strftime("%Y-%m-%d")
|
---|
426 |
|
---|
427 | return ([], attrs)
|
---|
428 |
|
---|
429 | def __setstate__(self, state):
|
---|
430 | """Set the state from the given unpickled dictionary."""
|
---|
431 | self.__dict__.update(fixUnpickled(state))
|
---|