2 # -*- coding: utf-8 -*-
4 from __future__ import division
9 from datetime import datetime, timedelta, date, time
10 from fcntl import lockf, LOCK_EX, LOCK_UN, LOCK_SH
13 from ais.ntools import *
15 from ais.area import load_area_from_kml_polygon
16 from ais.earth3d import dist3_latlong_ais, dist3_xyz, latlon_to_xyz_deg, latlon_to_xyz_ais
19 'DB_STARTDATE', 'DBPATH',
20 'COUNTRIES_MID', 'STATUS_CODES', 'SHIP_TYPES',
21 'AIS_STATUS_NOT_AVAILABLE',
22 'AIS_ROT_HARD_LEFT', 'AIS_ROT_HARD_RIGHT', 'AIS_ROT_NOT_AVAILABLE',
23 'AIS_LATLON_SCALE', 'AIS_LON_NOT_AVAILABLE', 'AIS_LAT_NOT_AVAILABLE',
24 'AIS_COG_SCALE', 'AIS_COG_NOT_AVAILABLE',
26 'AIS_SOG_SCALE', 'AIS_SOG_NOT_AVAILABLE', 'AIS_SOG_FAST_MOVER', 'AIS_SOG_MAX_SPEED',
27 #'_hash3_pathfilename',
28 'db_bydate_addrecord',
29 'db_lastinfo_setrecord_ifnewer',
46 'fleetname_to_fleetid',
51 'filter_knownposition',
55 DB_STARTDATE = datetime(2008, 6, 1)
57 # This is the location of the filesystem database
58 DBPATH = '/var/lib/ais/db'
60 # see make-countries.py
69 208: u'Vatican City State',
87 231: u'Faroe Islands',
88 232: u'United Kingdom',
89 233: u'United Kingdom',
90 234: u'United Kingdom',
91 235: u'United Kingdom',
107 252: u'Liechtenstein',
121 267: u'Slovak Republic',
124 270: u'Czech Republic',
127 273: u'Russian Federation',
128 274: u'The Former Yugoslav Republic of Macedonia',
136 304: u'Antigua and Barbuda',
137 305: u'Antigua and Barbuda',
138 306: u'Netherlands Antilles',
147 319: u'Cayman Islands',
151 327: u'Dominican Republic',
158 338: u'United States of America',
160 341: u'Saint Kitts and Nevis',
175 361: u'Saint Pierre and Miquelon',
176 362: u'Trinidad and Tobago',
177 364: u'Turks and Caicos Islands',
178 366: u'United States of America',
179 367: u'United States of America',
180 368: u'United States of America',
181 369: u'United States of America',
185 375: u'Saint Vincent and the Grenadines',
186 376: u'Saint Vincent and the Grenadines',
187 377: u'Saint Vincent and the Grenadines',
188 378: u'British Virgin Islands',
189 379: u'United States Virgin Islands',
191 403: u'Saudi Arabia',
201 423: u'Azerbaijani Republic',
206 434: u'Turkmenistan',
213 445: u"Democratic People's Republic of Korea",
216 451: u'Kyrgyz Republic',
224 468: u'Syrian Arab Republic',
225 470: u'United Arab Emirates',
229 478: u'Bosnia and Herzegovina',
233 508: u'Brunei Darussalam',
239 516: u'Christmas Island',
240 518: u'Cook Islands',
245 531: u"Lao People's Democratic Republic",
247 536: u'Northern Mariana Islands',
248 538: u'Marshall Islands',
249 540: u'New Caledonia',
252 546: u'French Polynesia',
254 553: u'Papua New Guinea',
255 555: u'Pitcairn Island',
256 557: u'Solomon Islands',
257 559: u'American Samoa',
267 578: u'Wallis and Futuna Islands',
268 601: u'South Africa',
271 607: u'Saint Paul and Amsterdam Islands',
272 608: u'Ascension Island',
276 612: u'Central African Republic',
281 618: u'Crozet Archipelago',
282 619: u"Côte d'Ivoire",
287 626: u'Gabonese Republic',
290 630: u'Guinea-Bissau',
291 631: u'Equatorial Guinea',
293 633: u'Burkina Faso',
295 635: u'Kerguelen Islands',
298 642: u"Socialist People's Libyan Arab Jamahiriya",
314 665: u'Saint Helena',
315 666: u'Somali Democratic Republic',
316 667: u'Sierra Leone',
317 668: u'Sao Tome and Principe',
320 671: u'Togolese Republic',
324 676: u'Democratic Republic of the Congo',
328 701: u'Argentine Republic',
334 740: u'Falkland Islands',
345 0: 'Under way using engine',
347 2: 'Not under command',
348 3: 'Restricted manoeuverability',
349 4: 'Constrained by her draught',
352 7: 'Engaged in Fishing',
353 8: 'Under way sailing',
354 9: '9 - Reserved for future amendment of Navigational Status for HSC',
355 10: '10 - Reserved for future amendment of Navigational Status for WIG',
356 11: '11 - Reserved for future use',
357 12: '12 - Reserved for future use',
358 13: '13 - Reserved for future use',
359 14: '14 - Reserved for future use', # Land stations
360 15: 'Not defined', # default
364 0: 'Not available (default)',
365 1: 'Reserved for future use',
366 2: 'Reserved for future use',
367 3: 'Reserved for future use',
368 4: 'Reserved for future use',
369 5: 'Reserved for future use',
370 6: 'Reserved for future use',
371 7: 'Reserved for future use',
372 8: 'Reserved for future use',
373 9: 'Reserved for future use',
374 10: 'Reserved for future use',
375 11: 'Reserved for future use',
376 12: 'Reserved for future use',
377 13: 'Reserved for future use',
378 14: 'Reserved for future use',
379 15: 'Reserved for future use',
380 16: 'Reserved for future use',
381 17: 'Reserved for future use',
382 18: 'Reserved for future use',
383 19: 'Reserved for future use',
384 20: 'Wing in ground (WIG), all ships of this type',
385 21: 'Wing in ground (WIG), Hazardous category A',
386 22: 'Wing in ground (WIG), Hazardous category B',
387 23: 'Wing in ground (WIG), Hazardous category C',
388 24: 'Wing in ground (WIG), Hazardous category D',
389 25: 'Wing in ground (WIG), Reserved for future use',
390 26: 'Wing in ground (WIG), Reserved for future use',
391 27: 'Wing in ground (WIG), Reserved for future use',
392 28: 'Wing in ground (WIG), Reserved for future use',
393 29: 'Wing in ground (WIG), Reserved for future use',
396 32: 'Towing: length exceeds 200m or breadth exceeds 25m',
397 33: 'Dredging or underwater ops',
401 37: 'Pleasure Craft',
404 40: 'High speed craft (HSC), all ships of this type',
405 41: 'High speed craft (HSC), Hazardous category A',
406 42: 'High speed craft (HSC), Hazardous category B',
407 43: 'High speed craft (HSC), Hazardous category C',
408 44: 'High speed craft (HSC), Hazardous category D',
409 45: 'High speed craft (HSC), Reserved for future use',
410 46: 'High speed craft (HSC), Reserved for future use',
411 47: 'High speed craft (HSC), Reserved for future use',
412 48: 'High speed craft (HSC), Reserved for future use',
413 49: 'High speed craft (HSC), No additional information',
415 51: 'Search and Rescue vessel',
418 54: 'Anti-pollution equipment',
419 55: 'Law Enforcement',
420 56: 'Spare - Local Vessel',
421 57: 'Spare - Local Vessel',
422 58: 'Medical Transport',
423 59: 'Ship according to RR Resolution No. 18',
424 60: 'Passenger, all ships of this type',
425 61: 'Passenger, Hazardous category A',
426 62: 'Passenger, Hazardous category B',
427 63: 'Passenger, Hazardous category C',
428 64: 'Passenger, Hazardous category D',
429 65: 'Passenger, Reserved for future use',
430 66: 'Passenger, Reserved for future use',
431 67: 'Passenger, Reserved for future use',
432 68: 'Passenger, Reserved for future use',
433 69: 'Passenger, No additional information',
434 70: 'Cargo', # 'Cargo, all ships of this type',
435 71: 'Cargo, Hazardous category A',
436 72: 'Cargo, Hazardous category B',
437 73: 'Cargo, Hazardous category C',
438 74: 'Cargo, Hazardous category D',
439 75: 'Cargo', # 'Cargo, Reserved for future use',
440 76: 'Cargo', # 'Cargo, Reserved for future use',
441 77: 'Cargo', # 'Cargo, Reserved for future use',
442 78: 'Cargo', # 'Cargo, Reserved for future use',
443 79: 'Cargo', # 'Cargo, No additional information',
444 80: 'Tanker', # 'Tanker, all ships of this type',
445 81: 'Tanker, Hazardous category A',
446 82: 'Tanker, Hazardous category B',
447 83: 'Tanker, Hazardous category C',
448 84: 'Tanker, Hazardous category D',
449 85: 'Tanker', # 'Tanker, Reserved for future use',
450 86: 'Tanker', # 'Tanker, Reserved for future use',
451 87: 'Tanker', # 'Tanker, Reserved for future use',
452 88: 'Tanker', # 'Tanker, Reserved for future use',
453 89: 'Tanker, No additional information',
454 90: 'Other Type, all ships of this type',
455 91: 'Other Type, Hazardous category A',
456 92: 'Other Type, Hazardous category B',
457 93: 'Other Type, Hazardous category C',
458 94: 'Other Type, Hazardous category D',
459 95: 'Other Type, Reserved for future use',
460 96: 'Other Type, Reserved for future use',
461 97: 'Other Type, Reserved for future use',
462 98: 'Other Type, Reserved for future use',
463 99: 'Other Type, no additional information',
464 100: 'Default Navaid',
465 101: 'Reference point',
467 103: 'Offshore Structure',
469 105: 'Light, without sectors',
470 106: 'Light, with sectors',
471 107: 'Leading Light Front',
472 108: 'Leading Light Rear',
473 109: 'Beacon, Cardinal N',
474 110: 'Beacon, Cardinal E',
475 111: 'Beacon, Cardinal S',
476 112: 'Beacon, Cardinal W',
477 113: 'Beacon, Port hand',
478 114: 'Beacon, Starboard hand',
479 115: 'Beacon, Preferred Channel port hand',
480 116: 'Beacon, Preferred Channel starboard hand',
481 117: 'Beacon, Isolated danger',
482 118: 'Beacon, Safe water',
483 119: 'Beacon, Special mark',
484 120: 'Cardinal Mark N',
485 121: 'Cardinal Mark E',
486 122: 'Cardinal Mark S',
487 123: 'Cardinal Mark W',
488 124: 'Port hand Mark',
489 125: 'Starboard hand Mark',
490 126: 'Preferred Channel Port hand',
491 127: 'Preferred Channel Starboard hand',
492 128: 'Isolated danger',
494 130: 'Manned VTS / Special Mark',
495 131: 'Light Vessel / LANBY',
498 AIS_STATUS_NOT_AVAILABLE = 15
499 AIS_ROT_HARD_LEFT = -127
500 AIS_ROT_HARD_RIGHT = 127
501 AIS_ROT_NOT_AVAILABLE = -128 # not like gpsd
503 AIS_LATLON_SCALE = 600000.0
504 AIS_LON_NOT_AVAILABLE = 0x6791AC0
505 AIS_LAT_NOT_AVAILABLE = 0x3412140
507 AIS_COG_NOT_AVAILABLE = 3600
510 AIS_SOG_NOT_AVAILABLE = 1023
511 AIS_SOG_FAST_MOVER = 1022
512 AIS_SOG_MAX_SPEED = 1021
515 def _hash3_pathfilename(filename):
517 Returns a level 3 directory hashed filename on that basis:
518 123456789 -> 1/12/123/123456789
520 return os.path.join(filename[0], filename[:2], filename[:3], filename)
523 def db_bydate_addrecord(basefilename, record, timestamp):
524 strdt = datetime.utcfromtimestamp(timestamp).strftime('%Y%m%d')
525 filename = os.path.join(DBPATH, 'bydate', strdt, _hash3_pathfilename(basefilename))
526 f = open_with_mkdirs(filename, 'ab')
528 #f.seek(0,2) # go to EOF
529 assert f.tell() % len(record) == 0, 'Invalid length for %s' % filename
534 def db_lastinfo_setrecord_ifnewer(basefilename, record, timestamp):
536 Overwrite last information if date is newer
537 Input record must be complete
539 filename = DBPATH+'/last/'+_hash3_pathfilename(basefilename)
542 f = open(filename, 'r+b')
543 except IOError, ioerr:
546 # File was not found? Ok, create it. FIXME: we should lock something...
547 f = open_with_mkdirs(filename, 'wb')
553 oldrecord = f.read(4)
554 assert len(oldrecord) == 4
555 oldtimestamp = struct.unpack('I', oldrecord)[0]
558 if timestamp > oldtimestamp:
560 assert f.tell() == len(record), \
561 "tell=%s size=%s" % (f.tell(), len(record))
569 def _sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
570 dim_bow, dim_stern, dim_port, dim_starboard, \
571 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
572 ''' Don't call directly '''
574 sqlinfo['mmsi'] = strmmsi_to_mmsi(strmmsi)
575 sqlinfo['updated'] = datetime.utcfromtimestamp(timestamp)
576 sqlinfo['imo'] = imo or None
577 sqlinfo['name'] = name or None
578 sqlinfo['callsign'] = callsign or None
579 sqlinfo['type'] = type
580 sqlinfo['dim_bow'] = dim_bow
581 sqlinfo['dim_stern'] = dim_stern
582 sqlinfo['dim_port'] = dim_port
583 sqlinfo['dim_starboard'] = dim_starboard
584 sqlinfo['destination'] = None
585 eta = '%02d%02d%02d%02d' % ( eta_M, eta_D, eta_h, eta_m)
586 if eta == '00000000':
587 # FIXME tempory hack for corrupted db/latest/*.nmea5 file
591 destination = destination.replace('\0', ' ').rstrip(' @\0')
592 sqlinfo['destination'] = destination or None
593 sqlinfo['source'] = source
594 sqlexec(u'''INSERT INTO vessel (mmsi, updated) SELECT %(mmsi)s, '1970-01-01T00:00:00' WHERE NOT EXISTS (SELECT * FROM vessel WHERE mmsi=%(mmsi)s)''', sqlinfo)
596 sqlexec(u'UPDATE vessel SET imo = %(imo)s WHERE mmsi=%(mmsi)s AND (imo IS NULL OR updated<%(updated)s)', sqlinfo)
598 sqlexec(u'UPDATE vessel SET name = %(name)s WHERE mmsi=%(mmsi)s AND (name IS NULL OR updated<%(updated)s)', sqlinfo)
599 if sqlinfo['callsign']:
600 sqlexec(u'UPDATE vessel SET callsign = %(callsign)s WHERE mmsi=%(mmsi)s AND (callsign IS NULL OR updated<%(updated)s)', sqlinfo)
602 sqlexec(u'UPDATE vessel SET type = %(type)s WHERE mmsi=%(mmsi)s AND (type IS NULL OR updated<%(updated)s)', sqlinfo)
603 if sqlinfo['dim_bow'] or sqlinfo['dim_stern']:
604 sqlexec(u'UPDATE vessel SET dim_bow = %(dim_bow)s, dim_stern = %(dim_stern)s WHERE mmsi=%(mmsi)s AND ((dim_port = 0 OR dim_stern=0) OR updated<%(updated)s)', sqlinfo)
605 if sqlinfo['dim_port'] or sqlinfo['dim_starboard']:
606 sqlexec(u'UPDATE vessel SET dim_port = %(dim_port)s, dim_starboard = %(dim_starboard)s WHERE mmsi=%(mmsi)s AND ((dim_port = 0 OR dim_starboard=0) OR updated<%(updated)s)', sqlinfo)
607 if sqlinfo['destination'] or sqlinfo['eta'] != '00002460':
608 sqlexec(u"UPDATE vessel SET destination = %(destination)s, eta = %(eta)s WHERE mmsi=%(mmsi)s AND (destination IS NULL OR eta = '00002460' OR updated<%(updated)s)", sqlinfo)
609 sqlexec(u'UPDATE vessel SET (updated, source) = (%(updated)s, %(source)s) WHERE mmsi=%(mmsi)s AND updated<%(updated)s', sqlinfo)
615 AIVDM_RECORD123_FORMAT = 'IBbhiiII4s'
616 AIVDM_RECORD123_LENGTH = struct.calcsize(AIVDM_RECORD123_FORMAT)
617 AIVDM_RECORD5_FORMAT = 'II20s7sBHHBBBBBBH20s4s'
618 AIVDM_RECORD5_LENGTH = struct.calcsize(AIVDM_RECORD5_FORMAT)
621 def add_nmea1(strmmsi, timestamp, status, rot, sog, \
622 latitude, longitude, cog, heading, source):
624 Input is raw data, unscaled
625 FIXME: lat & lon are inverted compared to raw aivdm structure
627 record = struct.pack(AIVDM_RECORD123_FORMAT, timestamp, status, rot, sog, latitude, longitude, cog, heading, source)
629 filename = strmmsi+'.nmea1'
630 db_bydate_addrecord(filename, record, timestamp)
631 # There's no need to be smart: all the information are taken, or none.
632 return db_lastinfo_setrecord_ifnewer(filename, record, timestamp)
635 def add_nmea5_full(strmmsi, timestamp, imo, name, callsign, type, \
636 dim_bow, dim_stern, dim_port, dim_starboard, \
637 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
639 Input is raw data, unscaled
640 All fields are set, and can be upgraded if the record is newer
641 FIXME: name & callsign are inverted compared to raw aivdm structure
643 record = struct.pack(AIVDM_RECORD5_FORMAT, timestamp, imo, name, callsign, \
644 type, dim_bow, dim_stern, dim_port, dim_starboard, \
645 eta_M, eta_D, eta_h, eta_m, draught, destination, source)
647 filename = strmmsi+'.nmea5'
648 db_bydate_addrecord(filename, record, timestamp)
649 updated = db_lastinfo_setrecord_ifnewer(filename, record, timestamp)
651 _sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
652 dim_bow, dim_stern, dim_port, dim_starboard, \
653 eta_M, eta_D, eta_h, eta_m, draught, destination, source)
656 def add_nmea5_partial(strmmsi, timestamp, imo, name, callsign, type, \
657 dim_bow, dim_stern, dim_port, dim_starboard, \
658 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
660 Input is raw data, unscaled
661 All fields are not set. Only some of them can be upgraded, if they're newer
663 record = struct.pack(AIVDM_RECORD5_FORMAT, \
664 timestamp, imo, name, callsign, type, \
665 dim_bow, dim_stern, dim_port, dim_starboard, \
666 eta_M, eta_D, eta_h, eta_m, draught, destination, \
669 filename = strmmsi + '.nmea5'
670 db_bydate_addrecord(filename, record, timestamp)
673 filename = os.path.join(DBPATH, 'last', _hash3_pathfilename(filename))
675 f = open(filename, 'r+b')
676 except IOError, ioerr:
679 # File was not found? Ok, create it. FIXME: we should lock something...
680 f = open_with_mkdirs(filename, 'wb')
687 oldrecord = f.read(AIVDM_RECORD5_LENGTH)
688 oldtimestamp, oldimo, oldname, oldcallsign, oldtype, \
689 olddim_bow, olddim_stern, olddim_port, olddim_starboard, \
690 oldeta_M, oldeta_D, oldeta_h, oldeta_m, \
691 olddraught, olddestination, oldsource \
692 = struct.unpack(AIVDM_RECORD5_FORMAT, oldrecord)
693 if timestamp > oldtimestamp:
694 # we have incoming recent information
700 callsign = oldcallsign
706 dim_stern = olddim_stern
708 dim_port = olddim_port
709 if dim_starboard == 0:
710 dim_starboard = olddim_starboard
711 if eta_M == 0 or eta_D == 0 or eta_h == 24 or eta_m == 60 \
712 or destination == '':
717 destination = olddestination
720 record = struct.pack(AIVDM_RECORD5_FORMAT, \
721 timestamp, imo, name, callsign, type, \
722 dim_bow, dim_stern, dim_port, dim_starboard, \
723 eta_M, eta_D, eta_h, eta_m, draught, \
729 # we received an obsolete info, but maybe there are some new things in it
730 if oldimo == 0 and imo != 0:
733 if oldname == '' and name != '':
736 if oldcallsign == '' and callsign != '':
737 oldcallsign = callsign
739 if oldtype == 0 and type != 0:
742 if olddim_bow == 0 and dim_bow != 0:
745 if olddim_stern == 0 and dim_stern != 0:
746 olddim_stern = dim_stern
748 if olddim_port == 0 and dim_port != 0:
749 olddim_port = dim_port
751 if olddim_starboard == 0 and dim_starboard != 0:
752 olddim_starboard = dim_starboard
755 if (oldeta_M == 0 or oldeta_D == 0 or olddestination == '') \
756 and ((eta_M != 0 and eta_D != 0) or destination!=''):
761 olddestination = destination
763 if olddraught == 0 and draught != 0:
768 record = struct.pack(AIVDM_RECORD5_FORMAT, \
769 oldtimestamp, oldimo, oldname, \
770 oldcallsign, oldtype, \
771 olddim_bow, olddim_stern, \
772 olddim_port, olddim_starboard, \
773 oldeta_M, oldeta_D, oldeta_h, oldeta_m, \
774 olddraught, olddestination, oldsource)
778 # keep the file locked during SQL updates
780 _sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
781 dim_bow, dim_stern, dim_port, dim_starboard, \
782 eta_M, eta_D, eta_h, eta_m, draught, destination, source)
788 def strmmsi_to_mmsi(strmmsi):
790 Convert from str mmsi to sql-int mmsi
791 Special treatment manal input
793 if strmmsi.isdigit():
796 assert strmmsi[3:5] == 'MI'
797 strmmsi = strmmsi[:3]+'00'+strmmsi[5:]
798 return int('-'+strmmsi)
801 def mmsi_to_strmmsi(mmsi):
803 Convert from sql-into mmsi to str mmsi
804 Special treatment manal input
808 strmmsi = "%08d" % -mmsi
809 assert strmmsi[3:5] == '00'
810 strmmsi = strmmsi[:3]+'MI'+strmmsi[5:]
814 __misources__ = {} # cache of manual source names
815 def _get_mi_sourcename(id):
817 Get the nice name for sources whose id4 starts with 'MI'
820 if not __misources__:
821 sqlexec(u'SELECT id, name FROM mi_source')
823 row = get_common_cursor().fetchone()
826 __misources__[row[0]] = row[1]
827 result = __misources__.get(id, None)
829 return u"Manual input #%s" % id
834 def __init__(self, timestamp, status=AIS_STATUS_NOT_AVAILABLE, rot=AIS_ROT_NOT_AVAILABLE, sog=AIS_SOG_NOT_AVAILABLE, latitude=AIS_LAT_NOT_AVAILABLE, longitude=AIS_LON_NOT_AVAILABLE, cog=AIS_COG_NOT_AVAILABLE, heading=AIS_NO_HEADING, source='\x00\x00\x00\x00'):
835 self.timestamp_1 = timestamp
839 self.latitude = latitude
840 self.longitude = longitude
842 self.heading = heading
843 self.source_1 = source
845 from_values = __init__
848 return self.timestamp_1, self.status, self.rot, self.sog, self.latitude, self.longitude, self.cog, self.heading, self.source_1
850 def from_record(self, record):
851 values = struct.unpack(AIVDM_RECORD123_FORMAT, record)
852 Nmea1.__init__(self, *values)
855 def new_from_record(record):
856 values = struct.unpack(AIVDM_RECORD123_FORMAT, record)
857 return Nmea1(*values)
860 return struct.pack(AIVDM_RECORD123_FORMAT, *Nmea1.to_values())
862 def from_file(self, file):
863 record = file.read(AIVDM_RECORD123_LENGTH)
864 Nmea1.from_record(self, record)
867 def new_from_file(file):
868 record = file.read(AIVDM_RECORD123_LENGTH)
869 return Nmea1.new_from_record(record)
871 def from_lastinfo(self, strmmsi):
872 filename_nmea1 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea1')
874 f = file(filename_nmea1, 'rb')
876 logging.debug("file %s doesn't exists" % filename_nmea1)
879 Nmea1.from_file(self, f)
883 def new_from_lastinfo(strmmsi):
884 filename_nmea1 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea1')
886 f = file(filename_nmea1, 'rb')
888 logging.debug("file %s doesn't exists" % filename_nmea1)
891 record = f.read(AIVDM_RECORD123_LENGTH)
893 return Nmea1.new_from_record(record)
896 def dump_to_stdout(self):
898 Prints content to stdout
900 print datetime.utcfromtimestamp(self.timestamp_1),
901 for i in (self.status, self.rot, self.sog, self.latitude/AIS_LATLON_SCALE, self.longitude/AIS_LATLON_SCALE, self.cog, self.heading, self.source_1):
909 return txt.replace('\0','').replace('@', '').strip()
911 def get_status(self, default='Unknown'):
912 return STATUS_CODES.get(self.status, default)
914 def get_sog_str(self, default='Unknown'):
915 if self.sog == AIS_SOG_NOT_AVAILABLE:
917 if self.sog == AIS_SOG_FAST_MOVER:
918 return 'over 102.2 kts'
919 return '%.1f kts' % (self.sog/AIS_SOG_SCALE)
921 def get_rot_str(self, default='Unknown'):
922 if self.rot == AIS_ROT_NOT_AVAILABLE:
934 result = '%d %% to ' % rot*100./127
938 def _decimaldegree_to_dms(f, emispheres):
944 result = '%d°' % int(f)
946 result += '%02.05f\' ' % f
950 def get_latitude_str(self, default='Unknown'):
951 if self.latitude == AIS_LAT_NOT_AVAILABLE:
953 return Nmea1._decimaldegree_to_dms(self.latitude / AIS_LATLON_SCALE, 'NS')
955 def get_longitude_str(self, default='Unknown'):
956 if self.longitude == AIS_LON_NOT_AVAILABLE:
958 return Nmea1._decimaldegree_to_dms(self.longitude / AIS_LATLON_SCALE, 'EW')
960 def get_cog_str(self, default='Unknown'):
961 if self.cog == AIS_COG_NOT_AVAILABLE:
963 return '%.1f°' % (self.cog/10.)
965 def get_heading_str(self, default='Unknown'):
966 if self.heading == AIS_NO_HEADING:
968 return '%s°' % self.heading
970 def get_source_1_str(self):
971 return Nmea.format_source(self.source_1)
974 def __init__(self, timestamp, imo=0, name='', callsign='', type=0, dim_bow=0, dim_stern=0, dim_port=0, dim_starboard=0, eta_M=0, eta_D=0, eta_h=24, eta_m=60, draught=0, destination='', source=''):
975 self.timestamp_5 = timestamp
978 self.callsign = callsign
980 self.dim_bow = dim_bow
981 self.dim_stern = dim_stern
982 self.dim_port = dim_port
983 self.dim_starboard = dim_starboard
988 self.draught = draught
989 self.destination = destination
990 self.source_5 = source
992 from_values = __init__
994 def merge_from_values(self, timestamp, imo=0, name='', callsign='', type_=0, dim_bow=0, dim_stern=0, dim_port=0, dim_starboard=0, eta_M=0, eta_D=0, eta_h=24, eta_m=60, draught=0, destination='', source=''):
996 if self.imo == 0 or imo != 0:
999 if self.name == '' or name != '':
1002 if self.callsign == '' or callsign != '':
1003 self.callsign = callsign
1005 if self.type == 0 or type_ != 0:
1008 if self.dim_bow == 0 or dim_bow != 0:
1009 self.dim_bow = dim_bow
1011 if self.dim_stern == 0 or dim_stern != 0:
1012 self.dim_stern = dim_stern
1014 if self.dim_port == 0 or dim_port != 0:
1015 self.dim_port = dim_port
1017 if self.dim_starboard == 0 or dim_starboard != 0:
1018 self.dim_starboard = dim_starboard
1020 if (self.eta_M == 0 and self.eta_D == 0 and self.eta_h == 24 and self.eta_m == 60) or eta_M != 0 or eta_D != 0 or eta_h != 24 or eta_m != 60:
1026 if self.draught == 0 or draught != 0:
1027 self.draught = draught
1029 if self.destination == '' or destination != '':
1030 self.destination = destination
1033 self.timestamp_5 = timestamp
1034 self.source_5 = source
1037 def to_values(self):
1038 return self.timestamp_5, self.imo, self.name, self.callsign, self.type, self.dim_bow, self.dim_stern, self.dim_port, self.dim_starboard, self.eta_M, self.eta_D, self.eta_h, self.eta_m, self.draught, self.destination, self.source_5
1040 def from_record(self, record):
1041 values = struct.unpack(AIVDM_RECORD5_FORMAT, record)
1042 Nmea5.__init__(self, *values)
1045 def new_from_record(record):
1046 values = struct.unpack(AIVDM_RECORD5_FORMAT, record)
1047 return Nmea5(*values)
1049 def to_record(self):
1050 return struct.pack(AIVDM_RECORD5_FORMAT, *Nmea5.to_values(self))
1052 def from_file(self, file):
1053 record = file.read(AIVDM_RECORD5_LENGTH)
1054 Nmea5.from_record(self, record)
1057 def new_from_file(file):
1058 record = file.read(AIVDM_RECORD5_LENGTH)
1059 return Nmea5.new_from_record(record)
1061 def from_lastinfo(self, strmmsi):
1062 filename_nmea5 = os.path.join(DBPATH,
1064 _hash3_pathfilename(strmmsi+'.nmea5'))
1066 f = file(filename_nmea5, 'rb')
1068 logging.debug("file %s doesn't exists" % filename_nmea5)
1071 Nmea5.from_file(self, f)
1075 def new_from_lastinfo(strmmsi):
1076 filename_nmea5 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea5')
1078 f = file(filename_nmea5, 'rb')
1080 logging.debug("file %s doesn't exists" % filename_nmea5)
1083 record = f.read(AIVDM_RECORD5_LENGTH)
1085 return Nmea5.new_from_record(record)
1088 def _clean_str(txt):
1091 return txt.replace('\0','').replace('@', '').strip()
1093 def get_name(self, default='Unknown'):
1094 result = self._clean_str(self.name)
1099 def get_callsign(self, default='Unknown'):
1100 return self._clean_str(self.callsign) or default
1102 def get_shiptype(self, default='Unknown'):
1103 return SHIP_TYPES.get(self.type, default)
1105 def get_length(self):
1106 return self.dim_bow + self.dim_stern
1108 def get_width(self):
1109 return self.dim_port + self.dim_starboard
1111 _monthes = 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(',')
1112 def get_eta_str(self, default='Unknown'):
1113 if not self.eta_M and not self.eta_D:
1117 if self.eta_M <= len(Nmea5._monthes):
1118 result += Nmea5._monthes[self.eta_M - 1]
1120 result += '%02d' % self.eta_M
1125 result += '%02d' % self.eta_D
1128 if self.eta_h != 24:
1129 result += ' %02d' % self.eta_h
1130 if self.eta_m == 60:
1133 result += ':%02d' % self.eta_m
1136 def get_draught_str(self, default='Unknown'):
1137 if not self.draught:
1139 return '%.1f meters' % (self.draught/10.)
1141 def get_destination(self, default='Unknown'):
1142 return self._clean_str(self.destination) or default
1144 def get_source_5_str(self):
1145 return Nmea.format_source(self.source_5)
1147 class Nmea(Nmea1, Nmea5):
1149 This is nmea info, a merge of nmea1 and nmea5 packets
1151 def __init__(self, strmmsi):
1152 self.strmmsi = strmmsi
1153 Nmea1.__init__(self, timestamp=0)
1154 Nmea5.__init__(self, timestamp=0)
1156 ########################
1157 # Because of multiple inheritance some functions are unavailable:
1158 def _nmea_not_implemented(*args, **kargs):
1159 # used to avoid conflicting inherited members
1160 raise NotImplementedError
1161 from_values = _nmea_not_implemented
1162 to_values = _nmea_not_implemented
1163 from_record = _nmea_not_implemented
1164 new_from_record = _nmea_not_implemented
1165 to_record = _nmea_not_implemented
1166 from_file = _nmea_not_implemented
1167 new_from_file = _nmea_not_implemented
1168 ########################
1170 def from_lastinfo(self, strmmsi):
1171 Nmea1.from_lastinfo(self, strmmsi)
1172 Nmea5.from_lastinfo(self, strmmsi)
1175 def new_from_lastinfo(strmmsi):
1176 # better than unimplemented, but not optimal
1177 nmea = Nmea(strmmsi)
1178 nmea.from_lastinfo(strmmsi)
1182 def get_flag(self, default=u'Unknown'):
1183 if self.strmmsi.startswith('00') and self.strmmsi[3:5]!='MI':
1184 ref_mmsi = self.strmmsi[2:]
1186 ref_mmsi = self.strmmsi
1187 country_mid = int(ref_mmsi[0:3])
1188 country_name = COUNTRIES_MID.get(country_mid, default)
1191 def get_mmsi_public(self, default='Unknown'):
1192 if self.strmmsi.isdigit():
1196 def get_title(self):
1198 Returns the name of the ship if available
1201 return self.get_name(None) or self.get_mmsi_public()
1203 def get_last_timestamp(self):
1205 Returns the most recent of update from timestamp1, timestamp5
1207 if self.timestamp_1 > self.timestamp_5:
1208 return self.timestamp_1
1210 return self.timestamp_5
1212 def get_last_updated_delta_str(self):
1214 Returns a pretty formated update data as a string
1216 lastupdate = self.get_last_timestamp()
1219 dt_lastupdate = datetime.utcfromtimestamp(lastupdate)
1220 delta = datetime.utcnow() - dt_lastupdate
1221 return nice_timedelta_str(delta) + u' ago'
1223 def get_last_updated_str(self):
1225 Returns a pretty formated update data as a string
1227 lastupdate = self.get_last_timestamp()
1230 dt_lastupdate = datetime.utcfromtimestamp(lastupdate)
1231 delta = datetime.utcnow() - dt_lastupdate
1232 return dt_lastupdate.strftime('%Y-%m-%d %H:%M:%S GMT') + ' (' + nice_timedelta_str(delta) + ' ago)'
1235 def format_source(infosrc):
1236 if infosrc == '\0\0\0\0':
1238 elif infosrc.startswith('MI'):
1239 if len(infosrc) == 4:
1240 return _get_mi_sourcename(struct.unpack('<2xH', infosrc)[0])
1242 return u'Manual input'
1243 elif infosrc.startswith('U'):
1244 return u'User input'
1245 elif infosrc.startswith('NM'):
1246 return u'NMEA packets from '+xml_escape(infosrc[2:])
1247 elif infosrc.startswith('SP'):
1248 return u"ShipPlotter user %s" % infosrc[2:]
1249 elif infosrc.startswith('ST'):
1250 return u"Spot track %s" % infosrc[2:]
1251 elif infosrc == u'MTWW':
1252 return u'MarineTraffic.com web site'
1253 elif infosrc == u'MTTR':
1254 return u'MarineTraffic.com track files'
1259 Maps the csv header name to matching function to call to get the data.
1261 csv_name_to_function = {
1262 'mmsi': lambda nmea: nmea.strmmsi,
1264 'name': Nmea5.get_name,
1265 'imo': lambda nmea: str(nmea.imo),
1266 'callsign': Nmea5.get_callsign,
1267 'type': lambda nmea: str(nmea.type) + '-' + nmea.get_shiptype(),
1268 'length':lambda nmea: str(nmea.get_length()),
1269 'width': lambda nmea: str(nmea.get_width()),
1270 'datetime': lambda nmea: datetime.utcfromtimestamp(nmea.get_last_timestamp()).strftime('%Y-%m-%dT%H:%M:%SZ'),
1271 'status': Nmea1.get_status,
1272 'sog': Nmea1.get_sog_str,
1273 'latitude': Nmea1.get_latitude_str,
1274 'longitude': Nmea1.get_longitude_str,
1275 'cog': Nmea1.get_cog_str,
1276 'heading': Nmea1.get_heading_str,
1277 'destination': Nmea5.get_destination,
1278 'eta': Nmea5.get_eta_str,
1279 'draught': Nmea5.get_draught_str,
1282 def get_dump_row(self, fieldnames):
1284 for fieldname in fieldnames:
1285 f = self.csv_name_to_function[fieldname]
1286 result.append(f(self))
1289 #def get_dump_row(self):
1294 # return txt.replace('\0','').replace('@', '').strip()
1296 # result.append(self.strmmsi)
1297 # result.append(self.get_flag().encode('utf-8'))
1298 # result.append(self.get_name())
1299 # result.append(str(self.imo))
1300 # result.append(_clean(self.callsign))
1301 # result.append(str(self.type) + '-' + SHIP_TYPES.get(self.type, 'unknown'))
1302 # d = self.dim_bow + self.dim_stern
1306 # result.append(None)
1307 # d = self.dim_port + self.dim_starboard
1311 # result.append(None)
1312 # result.append(datetime.utcfromtimestamp(self.timestamp_1).strftime('%Y-%m-%dT%H:%M:%SZ'))
1313 # result.append(STATUS_CODES.get(self.status, 'unknown'))
1314 # if self.sog != AIS_SOG_NOT_AVAILABLE:
1315 # result.append(str(self.sog/AIS_SOG_SCALE))
1317 # result.append(None)
1318 # if self.latitude != AIS_LAT_NOT_AVAILABLE:
1319 # result.append(str(self.latitude/AIS_LATLON_SCALE))
1321 # result.append(None)
1322 # if self.longitude != AIS_LON_NOT_AVAILABLE:
1323 # result.append(str(self.longitude/AIS_LATLON_SCALE))
1325 # result.append(None)
1326 # if self.cog != AIS_COG_NOT_AVAILABLE:
1327 # result.append(str(self.cog/10.))
1329 # result.append(None)
1330 # if self.heading != AIS_NO_HEADING:
1331 # result.append(str(self.heading))
1333 # result.append(None)
1334 # result.append(self.get_destination(''))
1335 # result.append(self.get_eta_str(''))
1336 # result.append(self.draught)
1337 # result.append(self.source_5)
1341 class BankNmea1(list):
1343 That class handle a .nmea1 archive file
1345 def __init__(self, strmmsi, dt):
1347 self.strmmsi = strmmsi
1348 if isinstance(dt, date):
1349 dt = dt.strftime('%Y%m%d')
1352 def get_filename(self):
1353 return os.path.join(DBPATH, 'bydate', self.date, _hash3_pathfilename(self.strmmsi+'.nmea1'))
1355 def __load_from_file(self, file):
1357 Adds all record from opened file in this bank
1358 File must be locked before call
1361 record = file.read(AIVDM_RECORD123_LENGTH)
1364 self.append(Nmea1.new_from_record(record))
1366 def _write_in_file(self, file):
1368 Write all records from that bank in opened file
1369 File must be locked before call
1370 File should be truncated after call
1373 file.write(nmea1.to_record())
1377 file = open(self.get_filename(), 'rb')
1378 lockf(file, LOCK_SH)
1379 except IOError, ioerr:
1380 if ioerr.errno == 2: # No file
1383 self.__load_from_file(file)
1388 Each call reload the file
1391 self.sort_by_date_reverse()
1392 return list.__iter__(self)
1394 def packday(remove_manual_input=False):
1396 #print "MMSI", strmmsi
1398 self = BankNmea1(self.strmmsi, self.date)
1399 filename = self.get_filename()
1401 file = open(filename, 'r+b') # read/write binary
1402 except IOError, ioerr:
1403 if ioerr.errno != 2: # No file
1405 return self # no data
1406 lockf(file, LOCK_EX)
1407 self.__load_from_file(file)
1410 file_has_changed = False
1411 file_must_be_unlinked = False
1414 file_has_changed = self.remove_duplicate_timestamp() or file_has_changed
1416 if remove_manual_input:
1417 #print "REMOVING MANUAL INPUT..."
1418 file_has_changed = self.remove_manual_input() or file_has_changed
1420 if file_has_changed:
1422 self._write_in_file(file)
1424 if file.tell() == 0:
1425 file_must_be_unlinked = True
1429 if file_must_be_unlinked:
1430 # FIXME we release the lock before unlinking
1431 # another process might encounter an empty file (not handled)
1432 logging.warning('file was truncated to size 0. unlinking')
1433 os.unlink(filename) # we have the lock (!)
1435 def dump_to_stdout(self):
1437 Print contents to stdout
1440 nmea1.dump_to_stdout()
1442 def sort_by_date(self):
1443 self.sort(lambda n1, n2: n1.timestamp_1 - n2.timestamp_1)
1445 def sort_by_date_reverse(self):
1446 self.sort(lambda n1, n2: n2.timestamp_1 - n1.timestamp_1)
1448 def remove_duplicate_timestamp(self):
1449 file_has_changed = False
1451 return file_has_changed
1452 last_timestamp = self[0].timestamp_1
1454 while i < len(self):
1455 if self[i].timestamp_1 == last_timestamp:
1457 file_has_changed = True
1459 last_timestamp = self[i].timestamp_1
1461 return file_has_changed
1463 def remove_manual_input(self):
1464 file_has_changed = False
1466 while i < len(self):
1467 if self[i].source_1[:2] == 'MI':
1469 file_has_changed = True
1472 return file_has_changed
1476 Yields all nmea1 packets between two given datetimes
1477 in REVERSE order (recent information first)
1479 def __init__(self, strmmsi, datetime_end, datetime_begin=None, max_count=0):
1480 self.strmmsi = strmmsi
1481 assert datetime_end is not None
1482 self.datetime_end = datetime_end
1483 self.datetime_begin = datetime_begin or DB_STARTDATE
1484 self.max_count = max_count
1487 dt_end = self.datetime_end
1488 d_end = dt_end.date()
1489 ts_end = datetime_to_timestamp(dt_end)
1490 if self.datetime_begin:
1491 dt_begin = self.datetime_begin
1492 d_begin = dt_begin.date()
1493 ts_begin = datetime_to_timestamp(dt_begin)
1502 if d_begin is not None and d < d_begin:
1504 bank = BankNmea1(self.strmmsi, d)
1506 if ts_begin is not None and nmea1.timestamp_1 < ts_begin:
1508 if nmea1.timestamp_1 > ts_end:
1514 if self.max_count and count >= self.max_count:
1519 class BankNmea5(list):
1521 That class handle a .nmea5 archive file
1523 def __init__(self, strmmsi, dt):
1525 self.strmmsi = strmmsi
1526 if isinstance(dt, date):
1528 dt = dt.strftime('%Y%m%d')
1530 logging.critical('dt=%s', dt)
1534 def get_filename(self):
1535 return os.path.join(DBPATH, 'bydate', self.date, _hash3_pathfilename(self.strmmsi+'.nmea5'))
1537 def __load_from_file(self, file):
1539 Adds all record from opened file in this bank
1540 File must be locked before call
1543 record = file.read(AIVDM_RECORD5_LENGTH)
1546 self.append(Nmea5.new_from_record(record))
1548 def _write_in_file(self, file):
1550 Write all records from that bank in opened file
1551 File must be locked before call
1552 File should be truncated after call
1555 file.write(nmea5.to_record())
1559 file = open(self.get_filename(), 'rb')
1560 lockf(file, LOCK_SH)
1561 except IOError, ioerr:
1562 if ioerr.errno == 2: # No file
1565 self.__load_from_file(file)
1570 Each call reload the file
1573 self.sort_by_date_reverse()
1574 return list.__iter__(self)
1576 def sort_by_date(self):
1577 self.sort(lambda n1, n2: n1.timestamp_5 - n2.timestamp_5)
1579 def sort_by_date_reverse(self):
1580 self.sort(lambda n1, n2: n2.timestamp_5 - n1.timestamp_5)
1584 Yields all nmea5 packets between two given datetimes
1585 in REVERSE order (recent information first)
1587 def __init__(self, strmmsi, datetime_end, datetime_begin=None, max_count=0):
1588 self.strmmsi = strmmsi
1589 assert datetime_end is not None
1590 self.datetime_end = datetime_end
1591 self.datetime_begin = datetime_begin or DB_STARTDATE
1592 self.max_count = max_count
1595 dt_end = self.datetime_end
1596 d_end = dt_end.date()
1597 ts_end = datetime_to_timestamp(dt_end)
1598 if self.datetime_begin:
1599 dt_begin = self.datetime_begin
1600 d_begin = dt_begin.date()
1601 ts_begin = datetime_to_timestamp(dt_begin)
1610 if d_begin is not None and d < d_begin:
1612 bank = BankNmea5(self.strmmsi, d)
1614 if ts_begin is not None and nmea1.timestamp_5 < ts_begin:
1616 if nmea1.timestamp_5 > ts_end:
1622 if self.max_count and count >= self.max_count:
1629 Yields nmea packets matching criteria.
1631 def __init__(self, strmmsi, datetime_end, datetime_begin=None, filters=None, granularity=1, max_count=None):
1632 if granularity <= 0:
1633 logging.warning('Granularity=%d generates duplicate entries', granularity)
1634 self.strmmsi = strmmsi
1635 assert datetime_end is not None
1636 self.datetime_end = datetime_end
1637 self.datetime_begin = datetime_begin or DB_STARTDATE
1638 self.filters = filters or []
1639 self.granularity = granularity
1640 self.max_count = max_count
1643 nmea = Nmea(self.strmmsi)
1644 if self.datetime_begin:
1645 nmea5_datetime_begin = self.datetime_begin - timedelta(30) # go back up to 30 days to get a good nmea5 packet
1647 nmea5_datetime_begin = None
1648 nmea5_iterator = Nmea5Feeder(self.strmmsi, self.datetime_end, nmea5_datetime_begin).__iter__()
1649 nmea5 = Nmea5(self.strmmsi, sys.maxint)
1652 lasttimestamp = sys.maxint
1653 for nmea1 in Nmea1Feeder(self.strmmsi, self.datetime_end, self.datetime_begin):
1654 Nmea1.from_values(nmea, *nmea1.to_values())
1656 # try to get an nmea5 paket older
1657 nmea5_updated = False
1658 while nmea5 is not None and nmea5.timestamp_5 > nmea1.timestamp_1:
1660 nmea5 = nmea5_iterator.next()
1661 nmea5_updated = True
1662 except StopIteration:
1665 if nmea5_updated and nmea5 is not None:
1666 Nmea5.merge_from_values(nmea, *nmea5.to_values())
1668 filtered_out = False
1669 for is_ok in self.filters:
1676 if nmea.timestamp_1 <= lasttimestamp - self.granularity:
1679 if self.max_count and count >= self.max_count:
1681 lasttimestamp = nmea.timestamp_1
1684 def nice_timedelta_str(delta):
1686 disprank = None # first item type displayed
1688 strdelta += str(delta.days)
1690 strdelta += ' days '
1694 delta_s = delta.seconds
1695 delta_m = delta_s // 60
1696 delta_s -= delta_m * 60
1697 delta_h = delta_m // 60
1698 delta_m -= delta_h * 60
1701 strdelta += str(delta_h)
1703 strdelta += ' hours '
1705 strdelta += ' hour '
1706 if disprank is None:
1708 if delta_m and (disprank is None or disprank >= 1):
1709 strdelta += str(delta_m)
1711 strdelta += ' minutes '
1713 strdelta += ' minute '
1714 if disprank is None:
1716 if delta_s and (disprank is None or disprank >= 2):
1717 strdelta += str(delta_s)
1719 strdelta += ' seconds '
1721 strdelta += ' second '
1722 if disprank is None:
1725 strdelta = 'less than a second '
1728 def all_mmsi_generator():
1730 Returns an array of all known strmmsi.
1732 for dirname, dirs, fnames in os.walk(os.path.join(DBPATH, 'last')):
1733 for fname in fnames:
1734 if fname[-6:] == '.nmea1':
1738 def load_fleet_to_uset(fleetid):
1740 Loads a fleet by id.
1741 Returns an array of strmmsi.
1744 sqlexec(u"SELECT mmsi FROM fleet_vessel WHERE fleet_id=" + unicode(fleetid))
1745 cursor = get_common_cursor()
1747 row = cursor.fetchone()
1751 result.append(mmsi_to_strmmsi(mmsi))
1752 logging.debug('fleet=%s', result)
1756 def fleetname_to_fleetid(fleetname):
1757 sqlexec(u"SELECT id FROM fleet WHERE name=%(fleetname)s", {'fleetname': fleetname})
1758 cursor = get_common_cursor()
1759 row = cursor.fetchone()
1763 def filter_area(nmea, area):
1765 Returns false if position is out of area.
1767 if nmea.latitude == AIS_LAT_NOT_AVAILABLE or nmea.longitude == AIS_LON_NOT_AVAILABLE:
1769 if not area.contains((nmea.latitude/AIS_LATLON_SCALE, nmea.longitude/AIS_LATLON_SCALE)):
1773 def filter_close_to(nmea, lat, lon, miles=1.0):
1775 Returns true if position is closer than miles from (lat, lon)
1777 return dist3_xyz(latlon_to_xyz_deg(lat, lon), latlon_to_xyz_ais(nmea.latitude, nmea.longitude)) <= miles
1780 def filter_far_from(nmea, lat, lon, miles=1.0):
1782 Returns true if position is farther than miles from (lat, lon)
1784 return dist3_xyz(latlon_to_xyz_deg(lat, lon), latlon_to_xyz_ais(nmea.latitude, nmea.longitude)) >= miles
1787 def filter_sog_le(nmea, max_knts):
1789 Returns true if speed over ground is less than max_knts
1791 return nmea.sog/AIS_SOG_SCALE <= max_knts
1794 def filter_knownposition(nmea):
1796 Returns false if position is not fully known
1798 # we are filtering out latitude=0 and longitude=0, that is not supposed to be necessary...
1799 return nmea.latitude != AIS_LAT_NOT_AVAILABLE and nmea.longitude != AIS_LON_NOT_AVAILABLE and nmea.latitude != 0 and nmea.longitude != 0
1802 _filter_positioncheck_last_mmsi = None
1803 def filter_speedcheck(nmea, max_mps):
1805 mps is miles per seconds
1807 global _filter_positioncheck_last_mmsi
1808 global _filter_positioncheck_last_time
1809 global _filter_positioncheck_last_time_failed
1810 global _filter_positioncheck_last_lat
1811 global _filter_positioncheck_last_lon
1812 global _filter_positioncheck_error_count
1813 if nmea.strmmsi != _filter_positioncheck_last_mmsi:
1814 _filter_positioncheck_last_time = None
1815 _filter_positioncheck_last_mmsi = nmea.strmmsi
1816 _filter_positioncheck_error_count = 0
1817 if _filter_positioncheck_last_time is not None:
1818 seconds = _filter_positioncheck_last_time - nmea.timestamp_1
1819 distance = dist3_latlong_ais((_filter_positioncheck_last_lat, _filter_positioncheck_last_lon), (nmea.latitude, nmea.longitude))
1821 speed = distance/seconds
1823 if _filter_positioncheck_error_count < 10:
1824 logging.debug("Ignoring point: distance = %s, time = %s, speed = %s kt, source = %s", distance, seconds, distance/seconds*3600, repr(nmea.source_1))
1825 if _filter_positioncheck_error_count == 0 or _filter_positioncheck_last_time_failed != nmea.timestamp_1:
1826 _filter_positioncheck_error_count += 1
1827 _filter_positioncheck_last_time_failed = nmea.timestamp_1
1830 logging.warning("Discontinous position accepted after too many failures: %.2f nm in %s s (%.0f kt), source = %s", distance, seconds, distance/seconds*3600, repr(nmea.source_1))
1831 _filter_positioncheck_error_count = 0
1832 _filter_positioncheck_last_time = nmea.timestamp_1
1833 _filter_positioncheck_last_lat = nmea.latitude
1834 _filter_positioncheck_last_lon = nmea.longitude
1840 Perform various operation on the database
1841 For usage, see "ais --help"
1843 from optparse import OptionParser, OptionGroup
1846 parser = OptionParser(usage='%prog [options] { mmsi | @fleetname | ^fleetid }+ | all')
1848 parser.add_option('-d', '--debug',
1849 action='store_true', dest='debug', default=False,
1852 parser.add_option('-e', '--end',
1853 action='store', dest='sdt_end', metavar="'YYYYMMDD HHMMSS'",
1854 help='End data processing on that GMT date time.'
1856 ' If a date is provided without time, time defaults to 235959.')
1857 parser.add_option('-s', '--start',
1858 action='store', dest='sdt_start', metavar="'YYYYMMDD HHMMSS'",
1859 help='Start data processing on that date.'
1860 ' Using that option enables multiple output of the same boat.'
1861 ' Disabled by default.'
1862 ' If a date is provided without time, time default to 000000.'
1863 ' If other options enable multiple output, default to 1 day before'
1864 ' --end date/time.')
1865 parser.add_option('--duration',
1866 action='store', dest='sdt_duration', metavar="DURATION",
1867 help='Duration of reference period.'
1868 ' Last character may be S for seconds, M(inutes), D(ays), W(eeks)'
1869 ' Default is seconds.'
1870 ' This is the time length bewteen --start and --end above.'
1871 ' If you want multiple output of the same boat, you may use '
1872 ' --start, --end or --duration, 2 of them, but not 3 of them.')
1873 parser.add_option('-g', '--granularity',
1874 action='store', type='int', dest='granularity', metavar='SECONDS',
1875 help='Dump only one position every granularity seconds.'
1876 ' Using that option enables multiple output of the same boat.'
1877 ' If other options enable multiple output, defaults to 600'
1879 parser.add_option('--max',
1880 action='store', type='int', dest='max_count', metavar='NUMBER',
1881 help='Dump a maximum of NUMBER positions every granularity seconds.'
1882 'Using that option enables multiple output of the same boat.')
1884 parser.add_option('--filter-knownposition',
1885 action='store_true', dest='filter_knownposition', default=False,
1886 help="Eliminate unknown positions from results.")
1888 parser.add_option('--filter-speedcheck',
1889 action='store', type='int', dest='speedcheck', default=200, metavar='KNOTS',
1890 help='Eliminate erroneaous positions from results,'
1891 ' based on impossible speed.')
1893 parser.add_option('--filter-type',
1894 action='append', type='int', dest='type_list', metavar='TYPE',
1895 help="process a specific ship type.")
1896 parser.add_option('--help-types',
1897 action='store_true', dest='help_types', default=False,
1898 help="display list of available types")
1900 parser.add_option('--filter-area',
1901 action='store', type='str', dest='area_file', metavar="FILE.KML",
1902 help="only process a specific area as defined in a kml polygon file.")
1903 parser.add_option('--filter-farfrom',
1904 action='store', dest='far_from', nargs=3, metavar='LAT LONG MILES',
1905 help="only show ships farther than MILES miles from LAT,LONG")
1906 parser.add_option('--filter-closeto',
1907 action='store', dest='close_to', nargs=3, metavar='LAT LONG MILES',
1908 help="only show ships closer than MILES miles from LAT,LONG")
1909 parser.add_option('--filter-sog-le',
1910 action='store', dest='sog_le', metavar='KNOTS',
1911 help='only show ships when speed over ground is less or equal to KNOTS.')
1913 parser.add_option('--filter-destination',
1914 action='store', type='str', dest='filter_destination', metavar="DESTINATION",
1915 help="Only print ships with that destination.")
1917 parser.add_option('--no-headers',
1918 action='store_false', dest='csv_headers', default=True,
1919 help="skip CSV headers")
1921 parser.add_option('--csv-fields',
1922 action='store', type='str', dest='csv_fields',
1923 default='mmsi,flag,name,imo,callsign,type,length,width,datetime,status,sog,latitude,longitude,cog,heading,destination,eta,draught',
1924 help='Which fields should be extracted for csv output. Default=%default')
1927 expert_group = OptionGroup(parser, "Expert Options",
1928 "You normaly don't need any of these")
1930 expert_group.add_option('--db',
1931 action='store', dest='db', default=DBPATH,
1932 help="path to filesystem database. Default=%default")
1934 expert_group.add_option('--debug-sql',
1935 action='store_true', dest='debug_sql', default=False,
1936 help="print all sql queries to stdout before running them")
1938 expert_group.add_option('--action',
1939 choices=('dump', 'removemanual', 'mmsidump', 'nirgaldebug', 'fixdestination'), default='dump',
1940 help='Possible values are:\n'
1941 'dump: dump values in csv format. This is the default.\n'
1942 'removemanual: Delete Manual Input entries from the database.\n'
1943 'mmsidump: Dump mmsi')
1944 parser.add_option_group(expert_group)
1946 (options, args) = parser.parse_args()
1949 if options.help_types:
1950 print "Known ship types:"
1951 keys = SHIP_TYPES.keys()
1954 print k, SHIP_TYPES[k]
1960 loglevel = logging.DEBUG
1962 loglevel = logging.INFO
1963 logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
1965 if options.debug_sql:
1973 print >> sys.stderr, "No ship to process"
1976 target_mmsi_iterator = []
1981 elif arg.startswith('@'):
1982 target_mmsi_iterator += load_fleet_to_uset(fleetname_to_fleetid(arg[1:]))
1983 elif arg.startswith('^'):
1984 target_mmsi_iterator += load_fleet_to_uset(int(arg[1:]))
1986 target_mmsi_iterator.append(arg)
1988 if target_mmsi_iterator:
1989 logging.warning('Selecting all ships, ignoring other arguments')
1990 target_mmsi_iterator = all_mmsi_generator()
1996 if options.sdt_start:
1997 # remove non digit characters
1998 options.sdt_start = "".join([ c for c in options.sdt_start if c.isdigit()])
1999 if len(options.sdt_start)==14:
2000 options.sdt_start = datetime.strptime(options.sdt_start, '%Y%m%d%H%M%S')
2001 elif len(options.sdt_start)==8:
2002 options.sdt_start = datetime.strptime(options.sdt_start, '%Y%m%d')
2004 print >> sys.stderr, "Invalid format for --start option"
2008 # remove non digit characters
2009 options.sdt_end = "".join([ c for c in options.sdt_end if c.isdigit()])
2010 if len(options.sdt_end)==14:
2011 options.sdt_end = datetime.strptime(options.sdt_end, '%Y%m%d%H%M%S')
2012 elif len(options.sdt_end)==8:
2013 options.sdt_end = datetime.strptime(options.sdt_end, '%Y%m%d')
2014 options.sdt_end = datetime.combine(options.sdt_end.date(), time(23, 59, 59))
2016 print >> sys.stderr, "Invalid format for --end option"
2019 if options.sdt_duration:
2021 options.sdt_duration = options.sdt_duration.replace(' ', '')
2023 options.sdt_duration = options.sdt_duration.upper()
2024 if options.sdt_duration[-1] == 'S':
2025 options.sdt_duration = options.sdt_duration[:-1]
2027 elif options.sdt_duration[-1] == 'M':
2028 options.sdt_duration = options.sdt_duration[:-1]
2030 elif options.sdt_duration[-1] == 'H':
2031 options.sdt_duration = options.sdt_duration[:-1]
2032 duration_unit = 60*60
2033 elif options.sdt_duration[-1] == 'D':
2034 options.sdt_duration = options.sdt_duration[:-1]
2035 duration_unit = 24*60*60
2036 elif options.sdt_duration[-1] == 'W':
2037 options.sdt_duration = options.sdt_duration[:-1]
2038 duration_unit = 7*24*60*60
2042 options.sdt_duration = long(options.sdt_duration)
2044 print >> sys.stderr, "Can't parse duration"
2046 options.sdt_duration = timedelta(0, options.sdt_duration * duration_unit)
2048 if options.sdt_start or options.sdt_duration or options.granularity is not None or options.max_count:
2049 # Time period is enabled (note that date_end only defaults to one day archives ending then)
2050 if not options.sdt_start and not options.sdt_end and not options.sdt_duration:
2051 options.sdt_duration = timedelta(1) # One day
2052 # continue without else
2053 if not options.sdt_start and not options.sdt_end and options.sdt_duration:
2054 dt_end = datetime.utcnow()
2055 dt_start = dt_end - options.sdt_duration
2056 #elif not options.sdt_start and options.sdt_end and not options.sdt_duration:
2058 elif not options.sdt_start and options.sdt_end and options.sdt_duration:
2059 dt_end = options.sdt_end
2060 dt_start = dt_end - options.sdt_duration
2061 elif options.sdt_start and not options.sdt_end and not options.sdt_duration:
2062 dt_start = options.sdt_start
2063 dt_end = datetime.utcnow()
2064 elif options.sdt_start and not options.sdt_end and options.sdt_duration:
2065 dt_start = options.sdt_start
2066 dt_end = dt_start + options.sdt_duration
2067 elif options.sdt_start and options.sdt_end and not options.sdt_duration:
2068 dt_start = options.sdt_start
2069 dt_end = options.sdt_end
2071 assert options.sdt_start and options.sdt_end and options.sdt_duration, 'Internal error'
2072 print >> sys.stderr, "You can't have all 3 --start --end and --duration"
2074 if options.granularity is None:
2075 options.granularity = 600
2077 # Only get one position
2080 dt_end = options.sdt_end
2082 dt_end = datetime.utcnow()
2083 options.max_count = 1
2084 if options.granularity is None:
2085 options.granularity = 600
2087 logging.debug('--start is %s', dt_start)
2088 logging.debug('--end is %s', dt_end)
2096 if options.filter_knownposition:
2097 filters.append(filter_knownposition)
2099 if options.speedcheck != 0:
2100 maxmps = options.speedcheck / 3600. # from knots to NM per seconds
2101 filters.append(lambda nmea: filter_speedcheck(nmea, maxmps))
2103 if options.area_file:
2104 area = load_area_from_kml_polygon(options.area_file)
2105 filters.append(lambda nmea: filter_area(nmea, area))
2107 if options.close_to:
2109 lat = clean_latitude(unicode(options.close_to[0], 'utf-8'))
2110 lon = clean_longitude(unicode(options.close_to[1], 'utf-8'))
2111 except LatLonFormatError as err:
2112 print >> sys.stderr, err.args
2114 miles = float(options.close_to[2])
2115 filters.append(lambda nmea: filter_close_to(nmea, lat, lon, miles))
2117 if options.far_from:
2119 lat = clean_latitude(unicode(options.far_from[0], 'utf-8'))
2120 lon = clean_longitude(unicode(options.far_from[1], 'utf-8'))
2121 except LatLonFormatError as err:
2122 print >> sys.stderr, err.args
2124 miles = float(options.far_from[2])
2125 filters.append(lambda nmea: filter_far_from(nmea, lat, lon, miles))
2128 filters.append(lambda nmea: filter_sog_le(nmea, float(options.sog_le)))
2130 if options.type_list:
2131 def filter_type(nmea):
2132 return nmea.type in options.type_list
2133 filters.append(filter_type)
2135 if options.filter_destination:
2136 filters.append(lambda nmea: nmea.destination.startswith(options.filter_destination))
2142 if options.action == 'dump':
2143 fields = options.csv_fields.split(',')
2144 output = csv.writer(sys.stdout)
2145 if options.csv_headers:
2146 output.writerow(fields)
2147 for mmsi in target_mmsi_iterator:
2148 logging.debug('Considering %s', repr(mmsi))
2149 assert dt_end is not None
2150 for nmea in NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count):
2151 output.writerow(nmea.get_dump_row(fields))
2153 elif options.action == 'removemanual':
2155 print >> sys.stderr, "removemanual action doesn't support filters"
2158 # TODO: dates = range dt_start, dt_end
2160 logging.info("Processing date %s", dt)
2161 for mmsi in target_mmsi_iterator:
2162 BankNmea1(mmsi, dt).packday(remove_manual_input=True)
2164 elif options.action == 'mmsidump':
2165 for strmmsi in target_mmsi_iterator :
2168 elif options.action == 'fixdestination':
2169 for mmsi in target_mmsi_iterator:
2170 for nmea in NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count):
2171 destination = nmea.destination.rstrip(' @\0')
2173 sqlexec(u'UPDATE vessel SET destination = %(destination)s WHERE mmsi=%(mmsi)s AND destination IS NULL', {'mmsi':strmmsi_to_mmsi(mmsi), 'destination':destination})
2174 logging.info('%s -> %s', mmsi, repr(destination))
2176 break # go to next mmsi
2179 if __name__ == '__main__':