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
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',
48 'filter_knownposition',
52 DB_STARTDATE = datetime(2008, 6, 1)
54 # This is the location of the filesystem database
55 DBPATH = '/var/lib/ais/db'
57 # see make-countries.py
66 208: u'Vatican City State',
84 231: u'Faroe Islands',
85 232: u'United Kingdom',
86 233: u'United Kingdom',
87 234: u'United Kingdom',
88 235: u'United Kingdom',
104 252: u'Liechtenstein',
118 267: u'Slovak Republic',
121 270: u'Czech Republic',
124 273: u'Russian Federation',
125 274: u'The Former Yugoslav Republic of Macedonia',
133 304: u'Antigua and Barbuda',
134 305: u'Antigua and Barbuda',
135 306: u'Netherlands Antilles',
144 319: u'Cayman Islands',
148 327: u'Dominican Republic',
155 338: u'United States of America',
157 341: u'Saint Kitts and Nevis',
172 361: u'Saint Pierre and Miquelon',
173 362: u'Trinidad and Tobago',
174 364: u'Turks and Caicos Islands',
175 366: u'United States of America',
176 367: u'United States of America',
177 368: u'United States of America',
178 369: u'United States of America',
182 375: u'Saint Vincent and the Grenadines',
183 376: u'Saint Vincent and the Grenadines',
184 377: u'Saint Vincent and the Grenadines',
185 378: u'British Virgin Islands',
186 379: u'United States Virgin Islands',
188 403: u'Saudi Arabia',
198 423: u'Azerbaijani Republic',
203 434: u'Turkmenistan',
210 445: u"Democratic People's Republic of Korea",
213 451: u'Kyrgyz Republic',
221 468: u'Syrian Arab Republic',
222 470: u'United Arab Emirates',
226 478: u'Bosnia and Herzegovina',
230 508: u'Brunei Darussalam',
236 516: u'Christmas Island',
237 518: u'Cook Islands',
242 531: u"Lao People's Democratic Republic",
244 536: u'Northern Mariana Islands',
245 538: u'Marshall Islands',
246 540: u'New Caledonia',
249 546: u'French Polynesia',
251 553: u'Papua New Guinea',
252 555: u'Pitcairn Island',
253 557: u'Solomon Islands',
254 559: u'American Samoa',
264 578: u'Wallis and Futuna Islands',
265 601: u'South Africa',
268 607: u'Saint Paul and Amsterdam Islands',
269 608: u'Ascension Island',
273 612: u'Central African Republic',
278 618: u'Crozet Archipelago',
279 619: u"Côte d'Ivoire",
284 626: u'Gabonese Republic',
287 630: u'Guinea-Bissau',
288 631: u'Equatorial Guinea',
290 633: u'Burkina Faso',
292 635: u'Kerguelen Islands',
295 642: u"Socialist People's Libyan Arab Jamahiriya",
311 665: u'Saint Helena',
312 666: u'Somali Democratic Republic',
313 667: u'Sierra Leone',
314 668: u'Sao Tome and Principe',
317 671: u'Togolese Republic',
321 676: u'Democratic Republic of the Congo',
325 701: u'Argentine Republic',
331 740: u'Falkland Islands',
342 0: 'Under way using engine',
344 2: 'Not under command',
345 3: 'Restricted manoeuverability',
346 4: 'Constrained by her draught',
349 7: 'Engaged in Fishing',
350 8: 'Under way sailing',
351 9: '9 - Reserved for future amendment of Navigational Status for HSC',
352 10: '10 - Reserved for future amendment of Navigational Status for WIG',
353 11: '11 - Reserved for future use',
354 12: '12 - Reserved for future use',
355 13: '13 - Reserved for future use',
356 14: '14 - Reserved for future use', # Land stations
357 15: 'Not defined', # default
361 0: 'Not available (default)',
362 1: 'Reserved for future use',
363 2: 'Reserved for future use',
364 3: 'Reserved for future use',
365 4: 'Reserved for future use',
366 5: 'Reserved for future use',
367 6: 'Reserved for future use',
368 7: 'Reserved for future use',
369 8: 'Reserved for future use',
370 9: 'Reserved for future use',
371 10: 'Reserved for future use',
372 11: 'Reserved for future use',
373 12: 'Reserved for future use',
374 13: 'Reserved for future use',
375 14: 'Reserved for future use',
376 15: 'Reserved for future use',
377 16: 'Reserved for future use',
378 17: 'Reserved for future use',
379 18: 'Reserved for future use',
380 19: 'Reserved for future use',
381 20: 'Wing in ground (WIG), all ships of this type',
382 21: 'Wing in ground (WIG), Hazardous category A',
383 22: 'Wing in ground (WIG), Hazardous category B',
384 23: 'Wing in ground (WIG), Hazardous category C',
385 24: 'Wing in ground (WIG), Hazardous category D',
386 25: 'Wing in ground (WIG), Reserved for future use',
387 26: 'Wing in ground (WIG), Reserved for future use',
388 27: 'Wing in ground (WIG), Reserved for future use',
389 28: 'Wing in ground (WIG), Reserved for future use',
390 29: 'Wing in ground (WIG), Reserved for future use',
393 32: 'Towing: length exceeds 200m or breadth exceeds 25m',
394 33: 'Dredging or underwater ops',
398 37: 'Pleasure Craft',
401 40: 'High speed craft (HSC), all ships of this type',
402 41: 'High speed craft (HSC), Hazardous category A',
403 42: 'High speed craft (HSC), Hazardous category B',
404 43: 'High speed craft (HSC), Hazardous category C',
405 44: 'High speed craft (HSC), Hazardous category D',
406 45: 'High speed craft (HSC), Reserved for future use',
407 46: 'High speed craft (HSC), Reserved for future use',
408 47: 'High speed craft (HSC), Reserved for future use',
409 48: 'High speed craft (HSC), Reserved for future use',
410 49: 'High speed craft (HSC), No additional information',
412 51: 'Search and Rescue vessel',
415 54: 'Anti-pollution equipment',
416 55: 'Law Enforcement',
417 56: 'Spare - Local Vessel',
418 57: 'Spare - Local Vessel',
419 58: 'Medical Transport',
420 59: 'Ship according to RR Resolution No. 18',
421 60: 'Passenger, all ships of this type',
422 61: 'Passenger, Hazardous category A',
423 62: 'Passenger, Hazardous category B',
424 63: 'Passenger, Hazardous category C',
425 64: 'Passenger, Hazardous category D',
426 65: 'Passenger, Reserved for future use',
427 66: 'Passenger, Reserved for future use',
428 67: 'Passenger, Reserved for future use',
429 68: 'Passenger, Reserved for future use',
430 69: 'Passenger, No additional information',
431 70: 'Cargo', # 'Cargo, all ships of this type',
432 71: 'Cargo, Hazardous category A',
433 72: 'Cargo, Hazardous category B',
434 73: 'Cargo, Hazardous category C',
435 74: 'Cargo, Hazardous category D',
436 75: 'Cargo', # 'Cargo, Reserved for future use',
437 76: 'Cargo', # 'Cargo, Reserved for future use',
438 77: 'Cargo', # 'Cargo, Reserved for future use',
439 78: 'Cargo', # 'Cargo, Reserved for future use',
440 79: 'Cargo', # 'Cargo, No additional information',
441 80: 'Tanker', # 'Tanker, all ships of this type',
442 81: 'Tanker, Hazardous category A',
443 82: 'Tanker, Hazardous category B',
444 83: 'Tanker, Hazardous category C',
445 84: 'Tanker, Hazardous category D',
446 85: 'Tanker', # 'Tanker, Reserved for future use',
447 86: 'Tanker', # 'Tanker, Reserved for future use',
448 87: 'Tanker', # 'Tanker, Reserved for future use',
449 88: 'Tanker', # 'Tanker, Reserved for future use',
450 89: 'Tanker, No additional information',
451 90: 'Other Type, all ships of this type',
452 91: 'Other Type, Hazardous category A',
453 92: 'Other Type, Hazardous category B',
454 93: 'Other Type, Hazardous category C',
455 94: 'Other Type, Hazardous category D',
456 95: 'Other Type, Reserved for future use',
457 96: 'Other Type, Reserved for future use',
458 97: 'Other Type, Reserved for future use',
459 98: 'Other Type, Reserved for future use',
460 99: 'Other Type, no additional information',
461 100: 'Default Navaid',
462 101: 'Reference point',
464 103: 'Offshore Structure',
466 105: 'Light, without sectors',
467 106: 'Light, with sectors',
468 107: 'Leading Light Front',
469 108: 'Leading Light Rear',
470 109: 'Beacon, Cardinal N',
471 110: 'Beacon, Cardinal E',
472 111: 'Beacon, Cardinal S',
473 112: 'Beacon, Cardinal W',
474 113: 'Beacon, Port hand',
475 114: 'Beacon, Starboard hand',
476 115: 'Beacon, Preferred Channel port hand',
477 116: 'Beacon, Preferred Channel starboard hand',
478 117: 'Beacon, Isolated danger',
479 118: 'Beacon, Safe water',
480 119: 'Beacon, Special mark',
481 120: 'Cardinal Mark N',
482 121: 'Cardinal Mark E',
483 122: 'Cardinal Mark S',
484 123: 'Cardinal Mark W',
485 124: 'Port hand Mark',
486 125: 'Starboard hand Mark',
487 126: 'Preferred Channel Port hand',
488 127: 'Preferred Channel Starboard hand',
489 128: 'Isolated danger',
491 130: 'Manned VTS / Special Mark',
492 131: 'Light Vessel / LANBY',
495 AIS_STATUS_NOT_AVAILABLE = 15
496 AIS_ROT_HARD_LEFT = -127
497 AIS_ROT_HARD_RIGHT = 127
498 AIS_ROT_NOT_AVAILABLE = -128 # not like gpsd
500 AIS_LATLON_SCALE = 600000.0
501 AIS_LON_NOT_AVAILABLE = 0x6791AC0
502 AIS_LAT_NOT_AVAILABLE = 0x3412140
504 AIS_COG_NOT_AVAILABLE = 3600
507 AIS_SOG_NOT_AVAILABLE = 1023
508 AIS_SOG_FAST_MOVER = 1022
509 AIS_SOG_MAX_SPEED = 1021
512 def _hash3_pathfilename(filename):
514 Returns a level 3 directory hashed filename on that basis:
515 123456789 -> 1/12/123/123456789
517 return os.path.join(filename[0], filename[:2], filename[:3], filename)
520 def db_bydate_addrecord(basefilename, record, timestamp):
521 strdt = datetime.utcfromtimestamp(timestamp).strftime('%Y%m%d')
522 filename = os.path.join(DBPATH, 'bydate', strdt, _hash3_pathfilename(basefilename))
523 f = open_with_mkdirs(filename, 'ab')
525 #f.seek(0,2) # go to EOF
526 assert f.tell() % len(record) == 0, 'Invalid length for %s' % filename
531 def db_lastinfo_setrecord_ifnewer(basefilename, record, timestamp):
533 Overwrite last information if date is newer
534 Input record must be complete
536 filename = DBPATH+'/last/'+_hash3_pathfilename(basefilename)
539 f = open(filename, 'r+b')
540 except IOError, ioerr:
543 # File was not found? Ok, create it. FIXME: we should lock something...
544 f = open_with_mkdirs(filename, 'wb')
550 oldrecord = f.read(4)
551 assert len(oldrecord) == 4
552 oldtimestamp = struct.unpack('I', oldrecord)[0]
555 if timestamp > oldtimestamp:
557 assert f.tell() == len(record), \
558 "tell=%s size=%s" % (f.tell(), len(record))
566 def _sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
567 dim_bow, dim_stern, dim_port, dim_starboard, \
568 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
569 ''' Don't call directly '''
571 sqlinfo['mmsi'] = strmmsi_to_mmsi(strmmsi)
572 sqlinfo['updated'] = datetime.utcfromtimestamp(timestamp)
573 sqlinfo['imo'] = imo or None
574 sqlinfo['name'] = name or None
575 sqlinfo['callsign'] = callsign or None
576 sqlinfo['type'] = type
577 sqlinfo['dim_bow'] = dim_bow
578 sqlinfo['dim_stern'] = dim_stern
579 sqlinfo['dim_port'] = dim_port
580 sqlinfo['dim_starboard'] = dim_starboard
581 sqlinfo['destination'] = None
582 eta = '%02d%02d%02d%02d' % ( eta_M, eta_D, eta_h, eta_m)
583 if eta == '00000000':
584 # FIXME tempory hack for corrupted db/latest/*.nmea5 file
588 destination = destination.replace('\0', ' ').rstrip(' @\0')
589 sqlinfo['destination'] = destination or None
590 sqlinfo['source'] = source
591 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)
593 sqlexec(u'UPDATE vessel SET imo = %(imo)s WHERE mmsi=%(mmsi)s AND (imo IS NULL OR updated<%(updated)s)', sqlinfo)
595 sqlexec(u'UPDATE vessel SET name = %(name)s WHERE mmsi=%(mmsi)s AND (name IS NULL OR updated<%(updated)s)', sqlinfo)
596 if sqlinfo['callsign']:
597 sqlexec(u'UPDATE vessel SET callsign = %(callsign)s WHERE mmsi=%(mmsi)s AND (callsign IS NULL OR updated<%(updated)s)', sqlinfo)
599 sqlexec(u'UPDATE vessel SET type = %(type)s WHERE mmsi=%(mmsi)s AND (type IS NULL OR updated<%(updated)s)', sqlinfo)
600 if sqlinfo['dim_bow'] or sqlinfo['dim_stern']:
601 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)
602 if sqlinfo['dim_port'] or sqlinfo['dim_starboard']:
603 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)
604 if sqlinfo['destination'] or sqlinfo['eta'] != '00002460':
605 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)
606 sqlexec(u'UPDATE vessel SET (updated, source) = (%(updated)s, %(source)s) WHERE mmsi=%(mmsi)s AND updated<%(updated)s', sqlinfo)
612 AIVDM_RECORD123_FORMAT = 'IBbhiiII4s'
613 AIVDM_RECORD123_LENGTH = struct.calcsize(AIVDM_RECORD123_FORMAT)
614 AIVDM_RECORD5_FORMAT = 'II20s7sBHHBBBBBBH20s4s'
615 AIVDM_RECORD5_LENGTH = struct.calcsize(AIVDM_RECORD5_FORMAT)
618 def add_nmea1(strmmsi, timestamp, status, rot, sog, \
619 latitude, longitude, cog, heading, source):
621 Input is raw data, unscaled
622 FIXME: lat & lon are inverted compared to raw aivdm structure
624 record = struct.pack(AIVDM_RECORD123_FORMAT, timestamp, status, rot, sog, latitude, longitude, cog, heading, source)
626 filename = strmmsi+'.nmea1'
627 db_bydate_addrecord(filename, record, timestamp)
628 # There's no need to be smart: all the information are taken, or none.
629 return db_lastinfo_setrecord_ifnewer(filename, record, timestamp)
632 def add_nmea5_full(strmmsi, timestamp, imo, name, callsign, type, \
633 dim_bow, dim_stern, dim_port, dim_starboard, \
634 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
636 Input is raw data, unscaled
637 All fields are set, and can be upgraded if the record is newer
638 FIXME: name & callsign are inverted compared to raw aivdm structure
640 record = struct.pack(AIVDM_RECORD5_FORMAT, timestamp, imo, name, callsign, \
641 type, dim_bow, dim_stern, dim_port, dim_starboard, \
642 eta_M, eta_D, eta_h, eta_m, draught, destination, source)
644 filename = strmmsi+'.nmea5'
645 db_bydate_addrecord(filename, record, timestamp)
646 updated = db_lastinfo_setrecord_ifnewer(filename, record, timestamp)
648 _sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
649 dim_bow, dim_stern, dim_port, dim_starboard, \
650 eta_M, eta_D, eta_h, eta_m, draught, destination, source)
653 def add_nmea5_partial(strmmsi, timestamp, imo, name, callsign, type, \
654 dim_bow, dim_stern, dim_port, dim_starboard, \
655 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
657 Input is raw data, unscaled
658 All fields are not set. Only some of them can be upgraded, if they're newer
660 record = struct.pack(AIVDM_RECORD5_FORMAT, \
661 timestamp, imo, name, callsign, type, \
662 dim_bow, dim_stern, dim_port, dim_starboard, \
663 eta_M, eta_D, eta_h, eta_m, draught, destination, \
666 filename = strmmsi + '.nmea5'
667 db_bydate_addrecord(filename, record, timestamp)
670 filename = os.path.join(DBPATH, 'last', _hash3_pathfilename(filename))
672 f = open(filename, 'r+b')
673 except IOError, ioerr:
676 # File was not found? Ok, create it. FIXME: we should lock something...
677 f = open_with_mkdirs(filename, 'wb')
684 oldrecord = f.read(AIVDM_RECORD5_LENGTH)
685 oldtimestamp, oldimo, oldname, oldcallsign, oldtype, \
686 olddim_bow, olddim_stern, olddim_port, olddim_starboard, \
687 oldeta_M, oldeta_D, oldeta_h, oldeta_m, \
688 olddraught, olddestination, oldsource \
689 = struct.unpack(AIVDM_RECORD5_FORMAT, oldrecord)
690 if timestamp > oldtimestamp:
691 # we have incoming recent information
697 callsign = oldcallsign
703 dim_stern = olddim_stern
705 dim_port = olddim_port
706 if dim_starboard == 0:
707 dim_starboard = olddim_starboard
708 if eta_M == 0 or eta_D == 0 or eta_h == 24 or eta_m == 60 \
709 or destination == '':
714 destination = olddestination
717 record = struct.pack(AIVDM_RECORD5_FORMAT, \
718 timestamp, imo, name, callsign, type, \
719 dim_bow, dim_stern, dim_port, dim_starboard, \
720 eta_M, eta_D, eta_h, eta_m, draught, \
726 # we received an obsolete info, but maybe there are some new things in it
727 if oldimo == 0 and imo != 0:
730 if oldname == '' and name != '':
733 if oldcallsign == '' and callsign != '':
734 oldcallsign = callsign
736 if oldtype == 0 and type != 0:
739 if olddim_bow == 0 and dim_bow != 0:
742 if olddim_stern == 0 and dim_stern != 0:
743 olddim_stern = dim_stern
745 if olddim_port == 0 and dim_port != 0:
746 olddim_port = dim_port
748 if olddim_starboard == 0 and dim_starboard != 0:
749 olddim_starboard = dim_starboard
752 if (oldeta_M == 0 or oldeta_D == 0 or olddestination == '') \
753 and ((eta_M != 0 and eta_D != 0) or destination!=''):
758 olddestination = destination
760 if olddraught == 0 and draught != 0:
765 record = struct.pack(AIVDM_RECORD5_FORMAT, \
766 oldtimestamp, oldimo, oldname, \
767 oldcallsign, oldtype, \
768 olddim_bow, olddim_stern, \
769 olddim_port, olddim_starboard, \
770 oldeta_M, oldeta_D, oldeta_h, oldeta_m, \
771 olddraught, olddestination, oldsource)
775 # keep the file locked during SQL updates
777 _sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
778 dim_bow, dim_stern, dim_port, dim_starboard, \
779 eta_M, eta_D, eta_h, eta_m, draught, destination, source)
785 def strmmsi_to_mmsi(strmmsi):
787 Convert from str mmsi to sql-int mmsi
788 Special treatment manal input
790 if strmmsi.isdigit():
793 assert strmmsi[3:5] == 'MI'
794 strmmsi = strmmsi[:3]+'00'+strmmsi[5:]
795 return int('-'+strmmsi)
798 def mmsi_to_strmmsi(mmsi):
800 Convert from sql-into mmsi to str mmsi
801 Special treatment manal input
805 strmmsi = "%08d" % -mmsi
806 assert strmmsi[3:5] == '00'
807 strmmsi = strmmsi[:3]+'MI'+strmmsi[5:]
811 __misources__ = {} # cache of manual source names
812 def _get_mi_sourcename(id):
814 Get the nice name for sources whose id4 starts with 'MI'
817 if not __misources__:
818 sqlexec(u'SELECT id, name FROM mi_source')
820 row = get_common_cursor().fetchone()
823 __misources__[row[0]] = row[1]
824 result = __misources__.get(id, None)
826 return u"Manual input #%s" % id
831 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'):
832 self.timestamp_1 = timestamp
836 self.latitude = latitude
837 self.longitude = longitude
839 self.heading = heading
840 self.source_1 = source
842 from_values = __init__
845 return self.timestamp_1, self.status, self.rot, self.sog, self.latitude, self.longitude, self.cog, self.heading, self.source_1
847 def from_record(self, record):
848 values = struct.unpack(AIVDM_RECORD123_FORMAT, record)
849 Nmea1.__init__(self, *values)
852 def new_from_record(record):
853 values = struct.unpack(AIVDM_RECORD123_FORMAT, record)
854 return Nmea1(*values)
857 return struct.pack(AIVDM_RECORD123_FORMAT, *Nmea1.to_values())
859 def from_file(self, file):
860 record = file.read(AIVDM_RECORD123_LENGTH)
861 Nmea1.from_record(self, record)
864 def new_from_file(file):
865 record = file.read(AIVDM_RECORD123_LENGTH)
866 return Nmea1.new_from_record(record)
868 def from_lastinfo(self, strmmsi):
869 filename_nmea1 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea1')
871 f = file(filename_nmea1, 'rb')
873 logging.debug("file %s doesn't exists" % filename_nmea1)
876 Nmea1.from_file(self, f)
880 def new_from_lastinfo(strmmsi):
881 filename_nmea1 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea1')
883 f = file(filename_nmea1, 'rb')
885 logging.debug("file %s doesn't exists" % filename_nmea1)
888 record = f.read(AIVDM_RECORD123_LENGTH)
890 return Nmea1.new_from_record(record)
893 def dump_to_stdout(self):
895 Prints content to stdout
897 print datetime.utcfromtimestamp(self.timestamp_1),
898 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):
906 return txt.replace('\0','').replace('@', '').strip()
908 def get_status(self, default='Unknown'):
909 return STATUS_CODES.get(self.status, default)
911 def get_sog_str(self, default='Unknown'):
912 if self.sog == AIS_SOG_NOT_AVAILABLE:
914 if self.sog == AIS_SOG_FAST_MOVER:
915 return 'over 102.2 kts'
916 return '%.1f kts' % (self.sog/AIS_SOG_SCALE)
918 def get_rot_str(self, default='Unknown'):
919 if self.rot == AIS_ROT_NOT_AVAILABLE:
931 result = '%d %% to ' % rot*100./127
935 def _decimaldegree_to_dms(f, emispheres):
941 result = '%d°' % int(f)
943 result += '%02.05f\' ' % f
947 def get_latitude_str(self, default='Unknown'):
948 if self.latitude == AIS_LAT_NOT_AVAILABLE:
950 return Nmea1._decimaldegree_to_dms(self.latitude / AIS_LATLON_SCALE, 'NS')
952 def get_longitude_str(self, default='Unknown'):
953 if self.longitude == AIS_LON_NOT_AVAILABLE:
955 return Nmea1._decimaldegree_to_dms(self.longitude / AIS_LATLON_SCALE, 'EW')
957 def get_cog_str(self, default='Unknown'):
958 if self.cog == AIS_COG_NOT_AVAILABLE:
960 return '%.1f°' % (self.cog/10.)
962 def get_heading_str(self, default='Unknown'):
963 if self.heading == AIS_NO_HEADING:
965 return '%s°' % self.heading
967 def get_source_1_str(self):
968 return Nmea.format_source(self.source_1)
971 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=''):
972 self.timestamp_5 = timestamp
975 self.callsign = callsign
977 self.dim_bow = dim_bow
978 self.dim_stern = dim_stern
979 self.dim_port = dim_port
980 self.dim_starboard = dim_starboard
985 self.draught = draught
986 self.destination = destination
987 self.source_5 = source
989 from_values = __init__
991 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=''):
993 if self.imo == 0 or imo != 0:
996 if self.name == '' or name != '':
999 if self.callsign == '' or callsign != '':
1000 self.callsign = callsign
1002 if self.type == 0 or type_ != 0:
1005 if self.dim_bow == 0 or dim_bow != 0:
1006 self.dim_bow = dim_bow
1008 if self.dim_stern == 0 or dim_stern != 0:
1009 self.dim_stern = dim_stern
1011 if self.dim_port == 0 or dim_port != 0:
1012 self.dim_port = dim_port
1014 if self.dim_starboard == 0 or dim_starboard != 0:
1015 self.dim_starboard = dim_starboard
1017 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:
1023 if self.draught == 0 or draught != 0:
1024 self.draught = draught
1026 if self.destination == '' or destination != '':
1027 self.destination = destination
1030 self.timestamp_5 = timestamp
1031 self.source_5 = source
1034 def to_values(self):
1035 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
1037 def from_record(self, record):
1038 values = struct.unpack(AIVDM_RECORD5_FORMAT, record)
1039 Nmea5.__init__(self, *values)
1042 def new_from_record(record):
1043 values = struct.unpack(AIVDM_RECORD5_FORMAT, record)
1044 return Nmea5(*values)
1046 def to_record(self):
1047 return struct.pack(AIVDM_RECORD5_FORMAT, *Nmea5.to_values(self))
1049 def from_file(self, file):
1050 record = file.read(AIVDM_RECORD5_LENGTH)
1051 Nmea5.from_record(self, record)
1054 def new_from_file(file):
1055 record = file.read(AIVDM_RECORD5_LENGTH)
1056 return Nmea5.new_from_record(record)
1058 def from_lastinfo(self, strmmsi):
1059 filename_nmea5 = os.path.join(DBPATH,
1061 _hash3_pathfilename(strmmsi+'.nmea5'))
1063 f = file(filename_nmea5, 'rb')
1065 logging.debug("file %s doesn't exists" % filename_nmea5)
1068 Nmea5.from_file(self, f)
1072 def new_from_lastinfo(strmmsi):
1073 filename_nmea5 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea5')
1075 f = file(filename_nmea5, 'rb')
1077 logging.debug("file %s doesn't exists" % filename_nmea5)
1080 record = f.read(AIVDM_RECORD5_LENGTH)
1082 return Nmea5.new_from_record(record)
1085 def _clean_str(txt):
1088 return txt.replace('\0','').replace('@', '').strip()
1090 def get_name(self, default='Unknown'):
1091 result = self._clean_str(self.name)
1096 def get_callsign(self, default='Unknown'):
1097 return self._clean_str(self.callsign) or default
1099 def get_shiptype(self, default='Unknown'):
1100 return SHIP_TYPES.get(self.type, default)
1102 def get_length(self):
1103 return self.dim_bow + self.dim_stern
1105 def get_width(self):
1106 return self.dim_port + self.dim_starboard
1108 _monthes = 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(',')
1109 def get_eta_str(self, default='Unknown'):
1110 if not self.eta_M and not self.eta_D:
1114 if self.eta_M <= len(Nmea5._monthes):
1115 result += Nmea5._monthes[self.eta_M - 1]
1117 result += '%02d' % self.eta_M
1122 result += '%02d' % self.eta_D
1125 if self.eta_h != 24:
1126 result += ' %02d' % self.eta_h
1127 if self.eta_m == 60:
1130 result += ':%02d' % self.eta_m
1133 def get_draught_str(self, default='Unknown'):
1134 if not self.draught:
1136 return '%.1f meters' % (self.draught/10.)
1138 def get_destination(self, default='Unknown'):
1139 return self._clean_str(self.destination) or default
1141 def get_source_5_str(self):
1142 return Nmea.format_source(self.source_5)
1144 class Nmea(Nmea1, Nmea5):
1146 This is nmea info, a merge of nmea1 and nmea5 packets
1148 def __init__(self, strmmsi):
1149 self.strmmsi = strmmsi
1150 Nmea1.__init__(self, timestamp=0)
1151 Nmea5.__init__(self, timestamp=0)
1153 ########################
1154 # Because of multiple inheritance some functions are unavailable:
1155 def _nmea_not_implemented(*args, **kargs):
1156 # used to avoid conflicting inherited members
1157 raise NotImplementedError
1158 from_values = _nmea_not_implemented
1159 to_values = _nmea_not_implemented
1160 from_record = _nmea_not_implemented
1161 new_from_record = _nmea_not_implemented
1162 to_record = _nmea_not_implemented
1163 from_file = _nmea_not_implemented
1164 new_from_file = _nmea_not_implemented
1165 ########################
1167 def from_lastinfo(self, strmmsi):
1168 Nmea1.from_lastinfo(self, strmmsi)
1169 Nmea5.from_lastinfo(self, strmmsi)
1172 def new_from_lastinfo(strmmsi):
1173 # better than unimplemented, but not optimal
1174 nmea = Nmea(strmmsi)
1175 nmea.from_lastinfo(strmmsi)
1179 def get_flag(self, default=u'Unknown'):
1180 if self.strmmsi.startswith('00') and self.strmmsi[3:5]!='MI':
1181 ref_mmsi = self.strmmsi[2:]
1183 ref_mmsi = self.strmmsi
1184 country_mid = int(ref_mmsi[0:3])
1185 country_name = COUNTRIES_MID.get(country_mid, default)
1188 def get_mmsi_public(self, default='Unknown'):
1189 if self.strmmsi.isdigit():
1193 def get_title(self):
1195 Returns the name of the ship if available
1198 return self.get_name(None) or self.get_mmsi_public()
1200 def get_last_timestamp(self):
1202 Returns the most recent of update from timestamp1, timestamp5
1204 if self.timestamp_1 > self.timestamp_5:
1205 return self.timestamp_1
1207 return self.timestamp_5
1209 def get_last_updated_delta_str(self):
1211 Returns a pretty formated update data as a string
1213 lastupdate = self.get_last_timestamp()
1216 dt_lastupdate = datetime.utcfromtimestamp(lastupdate)
1217 delta = datetime.utcnow() - dt_lastupdate
1218 return nice_timedelta_str(delta) + u' ago'
1220 def get_last_updated_str(self):
1222 Returns a pretty formated update data as a string
1224 lastupdate = self.get_last_timestamp()
1227 dt_lastupdate = datetime.utcfromtimestamp(lastupdate)
1228 delta = datetime.utcnow() - dt_lastupdate
1229 return dt_lastupdate.strftime('%Y-%m-%d %H:%M:%S GMT') + ' (' + nice_timedelta_str(delta) + ' ago)'
1232 def format_source(infosrc):
1233 if infosrc == '\0\0\0\0':
1235 elif infosrc.startswith('MI'):
1236 if len(infosrc) == 4:
1237 return _get_mi_sourcename(struct.unpack('<2xH', infosrc)[0])
1239 return u'Manual input'
1240 elif infosrc.startswith('U'):
1241 return u'User input'
1242 elif infosrc.startswith('NM'):
1243 return u'NMEA packets from '+xml_escape(infosrc[2:])
1244 elif infosrc.startswith('SP'):
1245 return u"ShipPlotter user %s" % infosrc[2:]
1246 elif infosrc == u'MTWW':
1247 return u'MarineTraffic.com web site'
1248 elif infosrc == u'MTTR':
1249 return u'MarineTraffic.com track files'
1274 def get_dump_row(self):
1279 return txt.replace('\0','').replace('@', '').strip()
1280 result.append(self.strmmsi)
1281 country_mid = int(self.strmmsi[:3])
1282 country_name = COUNTRIES_MID.get(country_mid, u'unknown')
1283 result.append(country_name.encode('utf-8'))
1284 result.append(_clean(self.name))
1285 result.append(str(self.imo))
1286 result.append(_clean(self.callsign))
1287 result.append(str(self.type) + '-' + SHIP_TYPES.get(self.type, 'unknown'))
1288 d = self.dim_bow + self.dim_stern
1293 d = self.dim_port + self.dim_starboard
1298 result.append(datetime.utcfromtimestamp(self.timestamp_1).strftime('%Y-%m-%dT%H:%M:%SZ'))
1299 result.append(STATUS_CODES.get(self.status, 'unknown'))
1300 if self.sog != AIS_SOG_NOT_AVAILABLE:
1301 result.append(str(self.sog/AIS_SOG_SCALE))
1304 if self.latitude != AIS_LAT_NOT_AVAILABLE:
1305 result.append(str(self.latitude/AIS_LATLON_SCALE))
1308 if self.longitude != AIS_LON_NOT_AVAILABLE:
1309 result.append(str(self.longitude/AIS_LATLON_SCALE))
1312 if self.cog != AIS_COG_NOT_AVAILABLE:
1313 result.append(str(self.cog/10.))
1316 if self.heading != AIS_NO_HEADING:
1317 result.append(str(self.heading))
1320 result.append(self.get_destination(''))
1321 result.append(self.get_eta_str(''))
1322 result.append(self.draught)
1323 result.append(self.source_5)
1327 class BankNmea1(list):
1329 That class handle a .nmea1 archive file
1331 def __init__(self, strmmsi, dt):
1333 self.strmmsi = strmmsi
1334 if isinstance(dt, date):
1335 dt = dt.strftime('%Y%m%d')
1338 def get_filename(self):
1339 return os.path.join(DBPATH, 'bydate', self.date, _hash3_pathfilename(self.strmmsi+'.nmea1'))
1341 def __load_from_file(self, file):
1343 Adds all record from opened file in this bank
1344 File must be locked before call
1347 record = file.read(AIVDM_RECORD123_LENGTH)
1350 self.append(Nmea1.new_from_record(record))
1352 def _write_in_file(self, file):
1354 Write all records from that bank in opened file
1355 File must be locked before call
1356 File should be truncated after call
1359 file.write(nmea1.to_record())
1363 file = open(self.get_filename(), 'rb')
1364 lockf(file, LOCK_SH)
1365 except IOError, ioerr:
1366 if ioerr.errno == 2: # No file
1369 self.__load_from_file(file)
1374 Each call reload the file
1377 self.sort_by_date_reverse()
1378 return list.__iter__(self)
1380 def packday(remove_manual_input=False):
1382 #print "MMSI", strmmsi
1384 self = BankNmea1(self.strmmsi, self.date)
1385 filename = self.get_filename()
1387 file = open(filename, 'r+b') # read/write binary
1388 except IOError, ioerr:
1389 if ioerr.errno != 2: # No file
1391 return self # no data
1392 lockf(file, LOCK_EX)
1393 self.__load_from_file(file)
1396 file_has_changed = False
1397 file_must_be_unlinked = False
1400 file_has_changed = self.remove_duplicate_timestamp() or file_has_changed
1402 if remove_manual_input:
1403 #print "REMOVING MANUAL INPUT..."
1404 file_has_changed = self.remove_manual_input() or file_has_changed
1406 if file_has_changed:
1408 self._write_in_file(file)
1410 if file.tell() == 0:
1411 file_must_be_unlinked = True
1415 if file_must_be_unlinked:
1416 # FIXME we release the lock before unlinking
1417 # another process might encounter an empty file (not handled)
1418 logging.warning('file was truncated to size 0. unlinking')
1419 os.unlink(filename) # we have the lock (!)
1421 def dump_to_stdout(self):
1423 Print contents to stdout
1426 nmea1.dump_to_stdout()
1428 def sort_by_date(self):
1429 self.sort(lambda n1, n2: n1.timestamp_1 - n2.timestamp_1)
1431 def sort_by_date_reverse(self):
1432 self.sort(lambda n1, n2: n2.timestamp_1 - n1.timestamp_1)
1434 def remove_duplicate_timestamp(self):
1435 file_has_changed = False
1437 return file_has_changed
1438 last_timestamp = self[0].timestamp_1
1440 while i < len(self):
1441 if self[i].timestamp_1 == last_timestamp:
1443 file_has_changed = True
1445 last_timestamp = self[i].timestamp_1
1447 return file_has_changed
1449 def remove_manual_input(self):
1450 file_has_changed = False
1452 while i < len(self):
1453 if self[i].source_1[:2] == 'MI':
1455 file_has_changed = True
1458 return file_has_changed
1462 Yields all nmea1 packets between two given datetimes
1463 in REVERSE order (recent information first)
1465 def __init__(self, strmmsi, datetime_end, datetime_begin=None, max_count=0):
1466 self.strmmsi = strmmsi
1467 assert datetime_end is not None
1468 self.datetime_end = datetime_end
1469 self.datetime_begin = datetime_begin or DB_STARTDATE
1470 self.max_count = max_count
1473 dt_end = self.datetime_end
1474 d_end = dt_end.date()
1475 ts_end = datetime_to_timestamp(dt_end)
1476 if self.datetime_begin:
1477 dt_begin = self.datetime_begin
1478 d_begin = dt_begin.date()
1479 ts_begin = datetime_to_timestamp(dt_begin)
1488 if d_begin is not None and d < d_begin:
1490 bank = BankNmea1(self.strmmsi, d)
1492 if ts_begin is not None and nmea1.timestamp_1 < ts_begin:
1494 if nmea1.timestamp_1 > ts_end:
1500 if self.max_count and count >= self.max_count:
1505 class BankNmea5(list):
1507 That class handle a .nmea5 archive file
1509 def __init__(self, strmmsi, dt):
1511 self.strmmsi = strmmsi
1512 if isinstance(dt, date):
1514 dt = dt.strftime('%Y%m%d')
1516 logging.critical('dt=%s', dt)
1520 def get_filename(self):
1521 return os.path.join(DBPATH, 'bydate', self.date, _hash3_pathfilename(self.strmmsi+'.nmea5'))
1523 def __load_from_file(self, file):
1525 Adds all record from opened file in this bank
1526 File must be locked before call
1529 record = file.read(AIVDM_RECORD5_LENGTH)
1532 self.append(Nmea5.new_from_record(record))
1534 def _write_in_file(self, file):
1536 Write all records from that bank in opened file
1537 File must be locked before call
1538 File should be truncated after call
1541 file.write(nmea5.to_record())
1545 file = open(self.get_filename(), 'rb')
1546 lockf(file, LOCK_SH)
1547 except IOError, ioerr:
1548 if ioerr.errno == 2: # No file
1551 self.__load_from_file(file)
1556 Each call reload the file
1559 self.sort_by_date_reverse()
1560 return list.__iter__(self)
1562 def sort_by_date(self):
1563 self.sort(lambda n1, n2: n1.timestamp_5 - n2.timestamp_5)
1565 def sort_by_date_reverse(self):
1566 self.sort(lambda n1, n2: n2.timestamp_5 - n1.timestamp_5)
1570 Yields all nmea5 packets between two given datetimes
1571 in REVERSE order (recent information first)
1573 def __init__(self, strmmsi, datetime_end, datetime_begin=None, max_count=0):
1574 self.strmmsi = strmmsi
1575 assert datetime_end is not None
1576 self.datetime_end = datetime_end
1577 self.datetime_begin = datetime_begin or DB_STARTDATE
1578 self.max_count = max_count
1581 dt_end = self.datetime_end
1582 d_end = dt_end.date()
1583 ts_end = datetime_to_timestamp(dt_end)
1584 if self.datetime_begin:
1585 dt_begin = self.datetime_begin
1586 d_begin = dt_begin.date()
1587 ts_begin = datetime_to_timestamp(dt_begin)
1596 if d_begin is not None and d < d_begin:
1598 bank = BankNmea5(self.strmmsi, d)
1600 if ts_begin is not None and nmea1.timestamp_5 < ts_begin:
1602 if nmea1.timestamp_5 > ts_end:
1608 if self.max_count and count >= self.max_count:
1615 Yields nmea packets matching criteria.
1617 def __init__(self, strmmsi, datetime_end, datetime_begin=None, filters=None, granularity=1, max_count=None):
1618 if granularity <= 0:
1619 logging.warning('Granularity=%d generates duplicate entries', granularity)
1620 self.strmmsi = strmmsi
1621 assert datetime_end is not None
1622 self.datetime_end = datetime_end
1623 self.datetime_begin = datetime_begin or DB_STARTDATE
1624 self.filters = filters or []
1625 self.granularity = granularity
1626 self.max_count = max_count
1629 nmea = Nmea(self.strmmsi)
1630 if self.datetime_begin:
1631 nmea5_datetime_begin = self.datetime_begin - timedelta(30) # go back up to 30 days to get a good nmea5 packet
1633 nmea5_datetime_begin = None
1634 nmea5_iterator = Nmea5Feeder(self.strmmsi, self.datetime_end, nmea5_datetime_begin).__iter__()
1635 nmea5 = Nmea5(self.strmmsi, sys.maxint)
1638 lasttimestamp = sys.maxint
1639 for nmea1 in Nmea1Feeder(self.strmmsi, self.datetime_end, self.datetime_begin):
1640 Nmea1.from_values(nmea, *nmea1.to_values())
1642 # try to get an nmea5 paket older
1643 nmea5_updated = False
1644 while nmea5 is not None and nmea5.timestamp_5 > nmea1.timestamp_1:
1646 nmea5 = nmea5_iterator.next()
1647 nmea5_updated = True
1648 except StopIteration:
1651 if nmea5_updated and nmea5 is not None:
1652 Nmea5.merge_from_values(nmea, *nmea5.to_values())
1654 filtered_out = False
1655 for is_ok in self.filters:
1662 if nmea.timestamp_1 <= lasttimestamp - self.granularity:
1665 if self.max_count and count >= self.max_count:
1667 lasttimestamp = nmea.timestamp_1
1670 def nice_timedelta_str(delta):
1672 disprank = None # first item type displayed
1674 strdelta += str(delta.days)
1676 strdelta += ' days '
1680 delta_s = delta.seconds
1681 delta_m = delta_s // 60
1682 delta_s -= delta_m * 60
1683 delta_h = delta_m // 60
1684 delta_m -= delta_h * 60
1687 strdelta += str(delta_h)
1689 strdelta += ' hours '
1691 strdelta += ' hour '
1692 if disprank is None:
1694 if delta_m and (disprank is None or disprank >= 1):
1695 strdelta += str(delta_m)
1697 strdelta += ' minutes '
1699 strdelta += ' minute '
1700 if disprank is None:
1702 if delta_s and (disprank is None or disprank >= 2):
1703 strdelta += str(delta_s)
1705 strdelta += ' seconds '
1707 strdelta += ' second '
1708 if disprank is None:
1711 strdelta = 'less than a second '
1714 def all_mmsi_generator():
1716 Returns an array of all known strmmsi.
1718 for dirname, dirs, fnames in os.walk(os.path.join(DBPATH, 'last')):
1719 for fname in fnames:
1720 if fname[-6:] == '.nmea1':
1724 def load_fleet_to_uset(fleetid):
1726 Loads a fleet by id.
1727 Returns an array of strmmsi.
1730 sqlexec(u"SELECT mmsi FROM fleet_vessel WHERE fleet_id=" + unicode(fleetid))
1731 cursor = get_common_cursor()
1733 row = cursor.fetchone()
1737 result.append(mmsi_to_strmmsi(mmsi))
1738 logging.debug('fleet=%s', result)
1742 def fleetname_to_fleetid(fleetname):
1743 sqlexec(u"SELECT id FROM fleet WHERE name=%(fleetname)s", {'fleetname': fleetname})
1744 cursor = get_common_cursor()
1745 row = cursor.fetchone()
1749 def filter_area(nmea, area):
1751 Returns false if position is out of area.
1753 if nmea.latitude == AIS_LAT_NOT_AVAILABLE or nmea.longitude == AIS_LON_NOT_AVAILABLE:
1755 if not area.contains((nmea.latitude/AIS_LATLON_SCALE, nmea.longitude/AIS_LATLON_SCALE)):
1759 def filter_knownposition(nmea):
1761 Returns false if position is not fully known
1763 # we are filtering out latitude=0 and longitude=0, that is not supposed to be necessary...
1764 return nmea.latitude != AIS_LAT_NOT_AVAILABLE and nmea.longitude != AIS_LON_NOT_AVAILABLE and nmea.latitude != 0 and nmea.longitude != 0
1767 _filter_positioncheck_last_mmsi = None
1768 def filter_speedcheck(nmea, max_mps):
1770 mps is miles per seconds
1772 global _filter_positioncheck_last_mmsi
1773 global _filter_positioncheck_last_time
1774 global _filter_positioncheck_last_time_failed
1775 global _filter_positioncheck_last_lat
1776 global _filter_positioncheck_last_lon
1777 global _filter_positioncheck_error_count
1778 if nmea.strmmsi != _filter_positioncheck_last_mmsi:
1779 _filter_positioncheck_last_time = None
1780 _filter_positioncheck_last_mmsi = nmea.strmmsi
1781 _filter_positioncheck_error_count = 0
1782 if _filter_positioncheck_last_time is not None:
1783 seconds = _filter_positioncheck_last_time - nmea.timestamp_1
1784 distance = dist3_latlong_ais((_filter_positioncheck_last_lat, _filter_positioncheck_last_lon), (nmea.latitude, nmea.longitude))
1786 speed = distance/seconds
1788 if _filter_positioncheck_error_count < 10:
1789 logging.debug("Ignoring point: distance = %s, time = %s, speed = %s kt, source = %s", distance, seconds, distance/seconds*3600, repr(nmea.source_1))
1790 if _filter_positioncheck_error_count == 0 or _filter_positioncheck_last_time_failed != nmea.timestamp_1:
1791 _filter_positioncheck_error_count += 1
1792 _filter_positioncheck_last_time_failed = nmea.timestamp_1
1795 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))
1796 _filter_positioncheck_error_count = 0
1797 _filter_positioncheck_last_time = nmea.timestamp_1
1798 _filter_positioncheck_last_lat = nmea.latitude
1799 _filter_positioncheck_last_lon = nmea.longitude
1805 Perform various operation on the database
1806 For usage, see "ais --help"
1808 from optparse import OptionParser, OptionGroup
1811 parser = OptionParser(usage='%prog [options] { mmsi | @fleetname | ^fleetid }+ | all')
1813 parser.add_option('-d', '--debug',
1814 action='store_true', dest='debug', default=False,
1817 parser.add_option('-e', '--end',
1818 action='store', dest='sdt_end', metavar="'YYYYMMDD HHMMSS'",
1819 help='End data processing on that GMT date time.'
1821 'If a date is provided without time, time defaults to 235959.')
1822 parser.add_option('-s', '--start',
1823 action='store', dest='sdt_start', metavar="'YYYYMMDD HHMMSS'",
1824 help='Start data processing on that date.'
1825 'Using that option enables multiple output of the same boat.'
1826 'Disabled by default.'
1827 'If a date is provided without time, time default to 000000.'
1828 'If other options enable multiple output, default to 1 day before'
1829 ' --end date/time.')
1830 parser.add_option('-g', '--granularity',
1831 action='store', type='int', dest='granularity', metavar='SECONDS',
1832 help='Dump only one position every granularity seconds.'
1833 'Using that option enables multiple output of the same boat.'
1834 'If other options enable multiple output, defaults to 600'
1836 parser.add_option('--max',
1837 action='store', type='int', dest='max_count', metavar='NUMBER',
1838 help='Dump a maximum of NUMBER positions every granularity seconds.'
1839 'Using that option enables multiple output of the same boat.')
1841 parser.add_option('--filter-knownposition',
1842 action='store_true', dest='filter_knownposition', default=False,
1843 help="Eliminate unknown positions from results.")
1845 parser.add_option('--filter-speedcheck',
1846 action='store', type='int', dest='speedcheck', default=200, metavar='KNOTS',
1847 help='Eliminate erroneaous positions from results,'
1848 ' based on impossible speed.')
1850 parser.add_option('--filter-type',
1851 action='append', type='int', dest='type_list', metavar='TYPE',
1852 help="process a specific ship type.")
1853 parser.add_option('--help-types',
1854 action='store_true', dest='help_types', default=False,
1855 help="display list of available types")
1857 parser.add_option('--filter-area',
1858 action='store', type='str', dest='area_file', metavar="FILE.KML",
1859 help="only process a specific area as defined in a kml polygon file.")
1861 parser.add_option('--filter-destination',
1862 action='store', type='str', dest='filter_destination', metavar="DESTINATION",
1863 help="Only print ships with that destination.")
1865 parser.add_option('--no-headers',
1866 action='store_false', dest='csv_headers', default=True,
1867 help="skip CSV headers")
1870 expert_group = OptionGroup(parser, "Expert Options",
1871 "You normaly don't need any of these")
1873 expert_group.add_option('--db',
1874 action='store', dest='db', default=DBPATH,
1875 help="path to filesystem database. Default=%default")
1877 expert_group.add_option('--debug-sql',
1878 action='store_true', dest='debug_sql', default=False,
1879 help="print all sql queries to stdout before running them")
1881 expert_group.add_option('--action',
1882 choices=('dump', 'removemanual', 'mmsidump', 'nirgaldebug', 'fixdestination'), default='dump',
1883 help='Possible values are:\n'
1884 'dump: dump values in csv format. This is the default.\n'
1885 'removemanual: Delete Manual Input entries from the database.\n'
1886 'mmsidump: Dump mmsi')
1887 parser.add_option_group(expert_group)
1889 (options, args) = parser.parse_args()
1892 if options.help_types:
1893 print "Known ship types:"
1894 keys = SHIP_TYPES.keys()
1897 print k, SHIP_TYPES[k]
1903 loglevel = logging.DEBUG
1905 loglevel = logging.INFO
1906 logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
1908 if options.debug_sql:
1916 print >> sys.stderr, "No ship to process"
1919 target_mmsi_iterator = []
1924 elif arg.startswith('@'):
1925 target_mmsi_iterator += load_fleet_to_uset(fleetname_to_fleetid(arg[1:]))
1926 elif arg.startswith('^'):
1927 target_mmsi_iterator += load_fleet_to_uset(int(arg[1:]))
1929 target_mmsi_iterator.append(arg)
1931 if target_mmsi_iterator:
1932 logging.warning('Selecting all ships, ignoring other arguments')
1933 target_mmsi_iterator = all_mmsi_generator()
1940 # remove non digit characters
1941 options.sdt_end = "".join([ c for c in options.sdt_end if c.isdigit()])
1942 if len(options.sdt_end)==14:
1943 dt_end = datetime.strptime(options.sdt_end, '%Y%m%d%H%M%S')
1944 elif len(options.sdt_end)==8:
1945 dt_end = datetime.strptime(options.sdt_end, '%Y%m%d')
1946 dt_end = datetime.combine(dt_end.date(), time(23, 59, 59))
1948 print >> sys.stderr, "Invalid format for --end option"
1951 dt_end = datetime.utcnow()
1952 logging.debug('--end is %s', dt_end)
1954 if options.sdt_start or options.granularity is not None or options.max_count:
1955 # time period is enabled
1956 if options.sdt_start:
1957 options.sdt_start = "".join([ c for c in options.sdt_start if c.isdigit()])
1958 if len(options.sdt_start)==14:
1959 dt_start = datetime.strptime(options.sdt_start, '%Y%m%d%H%M%S')
1960 elif len(options.sdt_start)==8:
1961 dt_start = datetime.strptime(options.sdt_start, '%Y%m%d')
1963 print >> sys.stderr, "Invalid format for --start option"
1966 dt_start = dt_end - timedelta(1)
1967 if options.granularity is None:
1968 options.granularity = 600
1971 options.max_count = 1
1972 if options.granularity is None:
1973 options.granularity = 600
1974 logging.debug('--start is %s', dt_start)
1982 if options.filter_knownposition:
1983 filters.append(filter_knownposition)
1985 if options.speedcheck != 0:
1986 maxmps = options.speedcheck / 3600. # from knots to NM per seconds
1987 filters.append(lambda nmea: filter_speedcheck(nmea, maxmps))
1989 if options.area_file:
1990 area = load_area_from_kml_polygon(options.area_file)
1991 filters.append(lambda nmea: filter_area(nmea, area))
1993 if options.type_list:
1994 def filter_type(nmea):
1995 return nmea.type in options.type_list
1996 filters.append(filter_type)
1998 if options.filter_destination:
1999 filters.append(lambda nmea: nmea.destination.startswith(options.filter_destination))
2005 if options.action == 'dump':
2006 output = csv.writer(sys.stdout)
2007 if options.csv_headers:
2008 output.writerow(Nmea.csv_headers)
2009 for mmsi in target_mmsi_iterator:
2010 logging.debug('Considering %s', repr(mmsi))
2011 assert dt_end is not None
2012 for nmea in NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count):
2013 output.writerow(nmea.get_dump_row())
2015 elif options.action == 'removemanual':
2017 print >> sys.stderr, "removemanual action doesn't support filters"
2020 # TODO: dates = range dt_start, dt_end
2022 logging.info("Processing date %s", dt)
2023 for mmsi in target_mmsi_iterator:
2024 BankNmea1(mmsi, dt).packday(remove_manual_input=True)
2026 elif options.action == 'mmsidump':
2027 for strmmsi in target_mmsi_iterator :
2030 elif options.action == 'fixdestination':
2031 for mmsi in target_mmsi_iterator:
2032 for nmea in NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count):
2033 destination = nmea.destination.rstrip(' @\0')
2035 sqlexec(u'UPDATE vessel SET destination = %(destination)s WHERE mmsi=%(mmsi)s AND destination IS NULL', {'mmsi':strmmsi_to_mmsi(mmsi), 'destination':destination})
2036 logging.info('%s -> %s', mmsi, repr(destination))
2038 break # go to next mmsi
2041 if __name__ == '__main__':