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
18 import ais.inputs.config
21 'DB_STARTDATE', 'DBPATH',
22 'COUNTRIES_MID', 'STATUS_CODES', 'SHIP_TYPES',
23 'AIS_STATUS_NOT_AVAILABLE',
24 'AIS_ROT_HARD_LEFT', 'AIS_ROT_HARD_RIGHT', 'AIS_ROT_NOT_AVAILABLE',
25 'AIS_LATLON_SCALE', 'AIS_LON_NOT_AVAILABLE', 'AIS_LAT_NOT_AVAILABLE',
26 'AIS_COG_SCALE', 'AIS_COG_NOT_AVAILABLE',
28 'AIS_SOG_SCALE', 'AIS_SOG_NOT_AVAILABLE', 'AIS_SOG_FAST_MOVER', 'AIS_SOG_MAX_SPEED',
29 #'_hash3_pathfilename',
30 'db_bydate_addrecord',
31 'db_lastinfo_setrecord_ifnewer',
46 'fleetname_to_fleetid',
47 'mmsiiterator_nohiddenship',
53 'filter_knownposition',
57 DB_STARTDATE = datetime(2008, 6, 1)
59 # This is the location of the filesystem database
60 DBPATH = '/var/lib/ais/db'
62 # see make-countries.py
71 208: u'Vatican City State',
89 231: u'Faroe Islands',
90 232: u'United Kingdom',
91 233: u'United Kingdom',
92 234: u'United Kingdom',
93 235: u'United Kingdom',
109 252: u'Liechtenstein',
123 267: u'Slovak Republic',
126 270: u'Czech Republic',
129 273: u'Russian Federation',
130 274: u'The Former Yugoslav Republic of Macedonia',
138 304: u'Antigua and Barbuda',
139 305: u'Antigua and Barbuda',
140 306: u'Netherlands Antilles',
149 319: u'Cayman Islands',
153 327: u'Dominican Republic',
160 338: u'United States of America',
162 341: u'Saint Kitts and Nevis',
177 361: u'Saint Pierre and Miquelon',
178 362: u'Trinidad and Tobago',
179 364: u'Turks and Caicos Islands',
180 366: u'United States of America',
181 367: u'United States of America',
182 368: u'United States of America',
183 369: u'United States of America',
187 375: u'Saint Vincent and the Grenadines',
188 376: u'Saint Vincent and the Grenadines',
189 377: u'Saint Vincent and the Grenadines',
190 378: u'British Virgin Islands',
191 379: u'United States Virgin Islands',
193 403: u'Saudi Arabia',
203 423: u'Azerbaijani Republic',
208 434: u'Turkmenistan',
215 445: u"Democratic People's Republic of Korea",
218 451: u'Kyrgyz Republic',
226 468: u'Syrian Arab Republic',
227 470: u'United Arab Emirates',
231 478: u'Bosnia and Herzegovina',
235 508: u'Brunei Darussalam',
241 516: u'Christmas Island',
242 518: u'Cook Islands',
247 531: u"Lao People's Democratic Republic",
249 536: u'Northern Mariana Islands',
250 538: u'Marshall Islands',
251 540: u'New Caledonia',
254 546: u'French Polynesia',
256 553: u'Papua New Guinea',
257 555: u'Pitcairn Island',
258 557: u'Solomon Islands',
259 559: u'American Samoa',
269 578: u'Wallis and Futuna Islands',
270 601: u'South Africa',
273 607: u'Saint Paul and Amsterdam Islands',
274 608: u'Ascension Island',
278 612: u'Central African Republic',
283 618: u'Crozet Archipelago',
284 619: u"Côte d'Ivoire",
289 626: u'Gabonese Republic',
292 630: u'Guinea-Bissau',
293 631: u'Equatorial Guinea',
295 633: u'Burkina Faso',
297 635: u'Kerguelen Islands',
300 642: u"Socialist People's Libyan Arab Jamahiriya",
316 665: u'Saint Helena',
317 666: u'Somali Democratic Republic',
318 667: u'Sierra Leone',
319 668: u'Sao Tome and Principe',
322 671: u'Togolese Republic',
326 676: u'Democratic Republic of the Congo',
330 701: u'Argentine Republic',
336 740: u'Falkland Islands',
347 0: 'Under way using engine',
349 2: 'Not under command',
350 3: 'Restricted manoeuverability',
351 4: 'Constrained by her draught',
354 7: 'Engaged in Fishing',
355 8: 'Under way sailing',
356 9: '9 - Reserved for future amendment of Navigational Status for HSC',
357 10: '10 - Reserved for future amendment of Navigational Status for WIG',
358 11: '11 - Reserved for future use',
359 12: '12 - Reserved for future use',
360 13: '13 - Reserved for future use',
361 14: '14 - Reserved for future use', # Land stations
362 15: 'Not defined', # default
366 0: 'Not available (default)',
367 1: 'Reserved for future use',
368 2: 'Reserved for future use',
369 3: 'Reserved for future use',
370 4: 'Reserved for future use',
371 5: 'Reserved for future use',
372 6: 'Reserved for future use',
373 7: 'Reserved for future use',
374 8: 'Reserved for future use',
375 9: 'Reserved for future use',
376 10: 'Reserved for future use',
377 11: 'Reserved for future use',
378 12: 'Reserved for future use',
379 13: 'Reserved for future use',
380 14: 'Reserved for future use',
381 15: 'Reserved for future use',
382 16: 'Reserved for future use',
383 17: 'Reserved for future use',
384 18: 'Reserved for future use',
385 19: 'Reserved for future use',
386 20: 'Wing in ground (WIG), all ships of this type',
387 21: 'Wing in ground (WIG), Hazardous category A',
388 22: 'Wing in ground (WIG), Hazardous category B',
389 23: 'Wing in ground (WIG), Hazardous category C',
390 24: 'Wing in ground (WIG), Hazardous category D',
391 25: 'Wing in ground (WIG), Reserved for future use',
392 26: 'Wing in ground (WIG), Reserved for future use',
393 27: 'Wing in ground (WIG), Reserved for future use',
394 28: 'Wing in ground (WIG), Reserved for future use',
395 29: 'Wing in ground (WIG), Reserved for future use',
398 32: 'Towing: length exceeds 200m or breadth exceeds 25m',
399 33: 'Dredging or underwater ops',
403 37: 'Pleasure Craft',
406 40: 'High speed craft (HSC), all ships of this type',
407 41: 'High speed craft (HSC), Hazardous category A',
408 42: 'High speed craft (HSC), Hazardous category B',
409 43: 'High speed craft (HSC), Hazardous category C',
410 44: 'High speed craft (HSC), Hazardous category D',
411 45: 'High speed craft (HSC), Reserved for future use',
412 46: 'High speed craft (HSC), Reserved for future use',
413 47: 'High speed craft (HSC), Reserved for future use',
414 48: 'High speed craft (HSC), Reserved for future use',
415 49: 'High speed craft (HSC), No additional information',
417 51: 'Search and Rescue vessel',
420 54: 'Anti-pollution equipment',
421 55: 'Law Enforcement',
422 56: 'Spare - Local Vessel',
423 57: 'Spare - Local Vessel',
424 58: 'Medical Transport',
425 59: 'Ship according to RR Resolution No. 18',
426 60: 'Passenger, all ships of this type',
427 61: 'Passenger, Hazardous category A',
428 62: 'Passenger, Hazardous category B',
429 63: 'Passenger, Hazardous category C',
430 64: 'Passenger, Hazardous category D',
431 65: 'Passenger, Reserved for future use',
432 66: 'Passenger, Reserved for future use',
433 67: 'Passenger, Reserved for future use',
434 68: 'Passenger, Reserved for future use',
435 69: 'Passenger, No additional information',
436 70: 'Cargo', # 'Cargo, all ships of this type',
437 71: 'Cargo, Hazardous category A',
438 72: 'Cargo, Hazardous category B',
439 73: 'Cargo, Hazardous category C',
440 74: 'Cargo, Hazardous category D',
441 75: 'Cargo', # 'Cargo, Reserved for future use',
442 76: 'Cargo', # 'Cargo, Reserved for future use',
443 77: 'Cargo', # 'Cargo, Reserved for future use',
444 78: 'Cargo', # 'Cargo, Reserved for future use',
445 79: 'Cargo', # 'Cargo, No additional information',
446 80: 'Tanker', # 'Tanker, all ships of this type',
447 81: 'Tanker, Hazardous category A',
448 82: 'Tanker, Hazardous category B',
449 83: 'Tanker, Hazardous category C',
450 84: 'Tanker, Hazardous category D',
451 85: 'Tanker', # 'Tanker, Reserved for future use',
452 86: 'Tanker', # 'Tanker, Reserved for future use',
453 87: 'Tanker', # 'Tanker, Reserved for future use',
454 88: 'Tanker', # 'Tanker, Reserved for future use',
455 89: 'Tanker, No additional information',
456 90: 'Other Type, all ships of this type',
457 91: 'Other Type, Hazardous category A',
458 92: 'Other Type, Hazardous category B',
459 93: 'Other Type, Hazardous category C',
460 94: 'Other Type, Hazardous category D',
461 95: 'Other Type, Reserved for future use',
462 96: 'Other Type, Reserved for future use',
463 97: 'Other Type, Reserved for future use',
464 98: 'Other Type, Reserved for future use',
465 99: 'Other Type, no additional information',
466 100: 'Default Navaid',
467 101: 'Reference point',
469 103: 'Offshore Structure',
471 105: 'Light, without sectors',
472 106: 'Light, with sectors',
473 107: 'Leading Light Front',
474 108: 'Leading Light Rear',
475 109: 'Beacon, Cardinal N',
476 110: 'Beacon, Cardinal E',
477 111: 'Beacon, Cardinal S',
478 112: 'Beacon, Cardinal W',
479 113: 'Beacon, Port hand',
480 114: 'Beacon, Starboard hand',
481 115: 'Beacon, Preferred Channel port hand',
482 116: 'Beacon, Preferred Channel starboard hand',
483 117: 'Beacon, Isolated danger',
484 118: 'Beacon, Safe water',
485 119: 'Beacon, Special mark',
486 120: 'Cardinal Mark N',
487 121: 'Cardinal Mark E',
488 122: 'Cardinal Mark S',
489 123: 'Cardinal Mark W',
490 124: 'Port hand Mark',
491 125: 'Starboard hand Mark',
492 126: 'Preferred Channel Port hand',
493 127: 'Preferred Channel Starboard hand',
494 128: 'Isolated danger',
496 130: 'Manned VTS / Special Mark',
497 131: 'Light Vessel / LANBY',
500 AIS_STATUS_NOT_AVAILABLE = 15
501 AIS_ROT_HARD_LEFT = -127
502 AIS_ROT_HARD_RIGHT = 127
503 AIS_ROT_NOT_AVAILABLE = -128 # not like gpsd
505 AIS_LATLON_SCALE = 600000.0
506 AIS_LON_NOT_AVAILABLE = 0x6791AC0
507 AIS_LAT_NOT_AVAILABLE = 0x3412140
509 AIS_COG_NOT_AVAILABLE = 3600
512 AIS_SOG_NOT_AVAILABLE = 1023
513 AIS_SOG_FAST_MOVER = 1022
514 AIS_SOG_MAX_SPEED = 1021
517 def _hash3_pathfilename(filename):
519 Returns a level 3 directory hashed filename on that basis:
520 123456789 -> 1/12/123/123456789
522 return os.path.join(filename[0], filename[:2], filename[:3], filename)
525 def db_bydate_addrecord(basefilename, record, timestamp):
526 strdt = datetime.utcfromtimestamp(timestamp).strftime('%Y%m%d')
527 filename = os.path.join(DBPATH, 'bydate', strdt, _hash3_pathfilename(basefilename))
528 f = open_with_mkdirs(filename, 'ab')
530 #f.seek(0,2) # go to EOF
531 assert f.tell() % len(record) == 0, 'Invalid length for %s' % filename
536 def db_lastinfo_setrecord_ifnewer(basefilename, record, timestamp):
538 Overwrite last information if date is newer
539 Input record must be complete
541 filename = DBPATH+'/last/'+_hash3_pathfilename(basefilename)
544 f = open(filename, 'r+b')
545 except IOError as ioerr:
548 # File was not found? Ok, create it. FIXME: we should lock something...
549 f = open_with_mkdirs(filename, 'wb')
555 oldrecord = f.read(4)
556 assert len(oldrecord) == 4
557 oldtimestamp = struct.unpack('I', oldrecord)[0]
560 if timestamp > oldtimestamp:
562 assert f.tell() == len(record), \
563 "tell=%s size=%s" % (f.tell(), len(record))
571 def _sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
572 dim_bow, dim_stern, dim_port, dim_starboard, \
573 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
574 ''' Don't call directly '''
576 sqlinfo['mmsi'] = strmmsi_to_mmsi(strmmsi)
577 sqlinfo['updated'] = datetime.utcfromtimestamp(timestamp)
578 sqlinfo['imo'] = imo or None
579 sqlinfo['name'] = name or None
580 sqlinfo['callsign'] = callsign or None
581 sqlinfo['type'] = type
582 sqlinfo['dim_bow'] = dim_bow
583 sqlinfo['dim_stern'] = dim_stern
584 sqlinfo['dim_port'] = dim_port
585 sqlinfo['dim_starboard'] = dim_starboard
586 sqlinfo['destination'] = None
587 eta = '%02d%02d%02d%02d' % ( eta_M, eta_D, eta_h, eta_m)
588 if eta == '00000000':
589 # FIXME tempory hack for corrupted db/latest/*.nmea5 file
593 destination = destination.replace('\0', ' ').rstrip(' @\0')
594 sqlinfo['destination'] = destination or None
595 sqlinfo['source'] = source
596 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)
598 sqlexec(u'UPDATE vessel SET imo = %(imo)s WHERE mmsi=%(mmsi)s AND (imo IS NULL OR updated<%(updated)s)', sqlinfo)
600 sqlexec(u'UPDATE vessel SET name = %(name)s WHERE mmsi=%(mmsi)s AND (name IS NULL OR updated<%(updated)s)', sqlinfo)
601 if sqlinfo['callsign']:
602 sqlexec(u'UPDATE vessel SET callsign = %(callsign)s WHERE mmsi=%(mmsi)s AND (callsign IS NULL OR updated<%(updated)s)', sqlinfo)
604 sqlexec(u'UPDATE vessel SET type = %(type)s WHERE mmsi=%(mmsi)s AND (type IS NULL OR updated<%(updated)s)', sqlinfo)
605 if sqlinfo['dim_bow'] or sqlinfo['dim_stern']:
606 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)
607 if sqlinfo['dim_port'] or sqlinfo['dim_starboard']:
608 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)
609 if sqlinfo['destination'] or sqlinfo['eta'] != '00002460':
610 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)
611 sqlexec(u'UPDATE vessel SET (updated, source) = (%(updated)s, %(source)s) WHERE mmsi=%(mmsi)s AND updated<%(updated)s', sqlinfo)
617 AIVDM_RECORD123_FORMAT = 'IBbhiiII4s'
618 AIVDM_RECORD123_LENGTH = struct.calcsize(AIVDM_RECORD123_FORMAT)
619 AIVDM_RECORD5_FORMAT = 'II20s7sBHHBBBBBBH20s4s'
620 AIVDM_RECORD5_LENGTH = struct.calcsize(AIVDM_RECORD5_FORMAT)
623 def add_nmea1(strmmsi, timestamp, status, rot, sog, \
624 latitude, longitude, cog, heading, source):
626 Input is raw data, unscaled
627 FIXME: lat & lon are inverted compared to raw aivdm structure
629 record = struct.pack(AIVDM_RECORD123_FORMAT, timestamp, status, rot, sog, latitude, longitude, cog, heading, source)
631 filename = strmmsi+'.nmea1'
632 db_bydate_addrecord(filename, record, timestamp)
633 # There's no need to be smart: all the information are taken, or none.
634 return db_lastinfo_setrecord_ifnewer(filename, record, timestamp)
637 def add_nmea5_full(strmmsi, timestamp, imo, name, callsign, type, \
638 dim_bow, dim_stern, dim_port, dim_starboard, \
639 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
641 Input is raw data, unscaled
642 All fields are set, and can be upgraded if the record is newer
643 FIXME: name & callsign are inverted compared to raw aivdm structure
645 record = struct.pack(AIVDM_RECORD5_FORMAT, timestamp, imo, name, callsign, \
646 type, dim_bow, dim_stern, dim_port, dim_starboard, \
647 eta_M, eta_D, eta_h, eta_m, draught, destination, source)
649 filename = strmmsi+'.nmea5'
650 db_bydate_addrecord(filename, record, timestamp)
651 updated = db_lastinfo_setrecord_ifnewer(filename, record, timestamp)
653 _sql_add_nmea5(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)
658 def add_nmea5_partial(strmmsi, timestamp, imo, name, callsign, type, \
659 dim_bow, dim_stern, dim_port, dim_starboard, \
660 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
662 Input is raw data, unscaled
663 All fields are not set. Only some of them can be upgraded, if they're newer
665 record = struct.pack(AIVDM_RECORD5_FORMAT, \
666 timestamp, imo, name, callsign, type, \
667 dim_bow, dim_stern, dim_port, dim_starboard, \
668 eta_M, eta_D, eta_h, eta_m, draught, destination, \
671 filename = strmmsi + '.nmea5'
672 db_bydate_addrecord(filename, record, timestamp)
675 filename = os.path.join(DBPATH, 'last', _hash3_pathfilename(filename))
677 f = open(filename, 'r+b')
678 except IOError as ioerr:
681 # File was not found? Ok, create it. FIXME: we should lock something...
682 f = open_with_mkdirs(filename, 'wb')
689 oldrecord = f.read(AIVDM_RECORD5_LENGTH)
690 oldtimestamp, oldimo, oldname, oldcallsign, oldtype, \
691 olddim_bow, olddim_stern, olddim_port, olddim_starboard, \
692 oldeta_M, oldeta_D, oldeta_h, oldeta_m, \
693 olddraught, olddestination, oldsource \
694 = struct.unpack(AIVDM_RECORD5_FORMAT, oldrecord)
695 if timestamp > oldtimestamp:
696 # we have incoming recent information
702 callsign = oldcallsign
708 dim_stern = olddim_stern
710 dim_port = olddim_port
711 if dim_starboard == 0:
712 dim_starboard = olddim_starboard
713 if eta_M == 0 or eta_D == 0 or eta_h == 24 or eta_m == 60 \
714 or destination == '':
719 destination = olddestination
722 record = struct.pack(AIVDM_RECORD5_FORMAT, \
723 timestamp, imo, name, callsign, type, \
724 dim_bow, dim_stern, dim_port, dim_starboard, \
725 eta_M, eta_D, eta_h, eta_m, draught, \
731 # we received an obsolete info, but maybe there are some new things in it
732 if oldimo == 0 and imo != 0:
735 if oldname == '' and name != '':
738 if oldcallsign == '' and callsign != '':
739 oldcallsign = callsign
741 if oldtype == 0 and type != 0:
744 if olddim_bow == 0 and dim_bow != 0:
747 if olddim_stern == 0 and dim_stern != 0:
748 olddim_stern = dim_stern
750 if olddim_port == 0 and dim_port != 0:
751 olddim_port = dim_port
753 if olddim_starboard == 0 and dim_starboard != 0:
754 olddim_starboard = dim_starboard
757 if (oldeta_M == 0 or oldeta_D == 0 or olddestination == '') \
758 and ((eta_M != 0 and eta_D != 0) or destination!=''):
763 olddestination = destination
765 if olddraught == 0 and draught != 0:
770 record = struct.pack(AIVDM_RECORD5_FORMAT, \
771 oldtimestamp, oldimo, oldname, \
772 oldcallsign, oldtype, \
773 olddim_bow, olddim_stern, \
774 olddim_port, olddim_starboard, \
775 oldeta_M, oldeta_D, oldeta_h, oldeta_m, \
776 olddraught, olddestination, oldsource)
780 # keep the file locked during SQL updates
782 _sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
783 dim_bow, dim_stern, dim_port, dim_starboard, \
784 eta_M, eta_D, eta_h, eta_m, draught, destination, source)
790 __misources__ = {} # cache of manual source names
791 def _get_mi_sourcename(id):
793 Get the nice name for sources whose id4 starts with 'MI'
796 if not __misources__:
797 sqlexec(u'SELECT id, name FROM mi_source')
799 row = get_common_cursor().fetchone()
802 __misources__[row[0]] = row[1]
803 result = __misources__.get(id, None)
805 return u"Manual input #%s" % id
810 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'):
811 self.timestamp_1 = timestamp
815 self.latitude = latitude
816 self.longitude = longitude
818 self.heading = heading
819 self.source_1 = source
821 from_values = __init__
824 return self.timestamp_1, self.status, self.rot, self.sog, self.latitude, self.longitude, self.cog, self.heading, self.source_1
826 def from_record(self, record):
827 values = struct.unpack(AIVDM_RECORD123_FORMAT, record)
828 Nmea1.__init__(self, *values)
831 def new_from_record(record):
832 values = struct.unpack(AIVDM_RECORD123_FORMAT, record)
833 return Nmea1(*values)
836 return struct.pack(AIVDM_RECORD123_FORMAT, *Nmea1.to_values(self))
838 def from_file(self, file):
839 record = file.read(AIVDM_RECORD123_LENGTH)
840 Nmea1.from_record(self, record)
843 def new_from_file(file):
844 record = file.read(AIVDM_RECORD123_LENGTH)
845 return Nmea1.new_from_record(record)
847 def from_lastinfo(self, strmmsi):
848 filename_nmea1 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea1')
850 f = file(filename_nmea1, 'rb')
852 logging.debug("file %s doesn't exists" % filename_nmea1)
855 Nmea1.from_file(self, f)
859 def new_from_lastinfo(strmmsi):
860 filename_nmea1 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea1')
862 f = file(filename_nmea1, 'rb')
864 logging.debug("file %s doesn't exists" % filename_nmea1)
867 record = f.read(AIVDM_RECORD123_LENGTH)
869 return Nmea1.new_from_record(record)
872 def dump_to_stdout(self):
874 Prints content to stdout
876 print datetime.utcfromtimestamp(self.timestamp_1),
877 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):
885 return txt.replace('\0','').replace('@', '').strip()
887 def get_status(self, default='Unknown'):
888 return STATUS_CODES.get(self.status, default)
890 def get_sog_str(self, default='Unknown'):
891 if self.sog == AIS_SOG_NOT_AVAILABLE:
893 if self.sog == AIS_SOG_FAST_MOVER:
894 return 'over 102.2 kts'
895 return '%.1f kts' % (self.sog/AIS_SOG_SCALE)
897 def get_rot_str(self, default='Unknown'):
898 if self.rot == AIS_ROT_NOT_AVAILABLE:
910 result = '%d %% to ' % rot*100./127
914 def _decimaldegree_to_dms(f, emispheres):
920 result = '%d°' % int(f)
922 result += '%02.05f\' ' % f
926 def get_latitude_str(self, default='Unknown'):
927 if self.latitude == AIS_LAT_NOT_AVAILABLE:
929 return Nmea1._decimaldegree_to_dms(self.latitude / AIS_LATLON_SCALE, 'NS')
931 def get_longitude_str(self, default='Unknown'):
932 if self.longitude == AIS_LON_NOT_AVAILABLE:
934 return Nmea1._decimaldegree_to_dms(self.longitude / AIS_LATLON_SCALE, 'EW')
936 def get_cog_str(self, default='Unknown'):
937 if self.cog == AIS_COG_NOT_AVAILABLE:
939 return '%.1f°' % (self.cog/10.)
941 def get_heading_str(self, default='Unknown'):
942 if self.heading == AIS_NO_HEADING:
944 return '%s°' % self.heading
946 def get_source_1_str(self):
947 return Nmea.format_source(self.source_1)
950 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=''):
951 self.timestamp_5 = timestamp
954 self.callsign = callsign
956 self.dim_bow = dim_bow
957 self.dim_stern = dim_stern
958 self.dim_port = dim_port
959 self.dim_starboard = dim_starboard
964 self.draught = draught
965 self.destination = destination
966 self.source_5 = source
968 from_values = __init__
970 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=''):
972 if self.imo == 0 or imo != 0:
975 if self.name == '' or name != '':
978 if self.callsign == '' or callsign != '':
979 self.callsign = callsign
981 if self.type == 0 or type_ != 0:
984 if self.dim_bow == 0 or dim_bow != 0:
985 self.dim_bow = dim_bow
987 if self.dim_stern == 0 or dim_stern != 0:
988 self.dim_stern = dim_stern
990 if self.dim_port == 0 or dim_port != 0:
991 self.dim_port = dim_port
993 if self.dim_starboard == 0 or dim_starboard != 0:
994 self.dim_starboard = dim_starboard
996 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:
1002 if self.draught == 0 or draught != 0:
1003 self.draught = draught
1005 if self.destination == '' or destination != '':
1006 self.destination = destination
1009 self.timestamp_5 = timestamp
1010 self.source_5 = source
1013 def to_values(self):
1014 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
1016 def from_record(self, record):
1017 values = struct.unpack(AIVDM_RECORD5_FORMAT, record)
1018 Nmea5.__init__(self, *values)
1021 def new_from_record(record):
1022 values = struct.unpack(AIVDM_RECORD5_FORMAT, record)
1023 return Nmea5(*values)
1025 def to_record(self):
1026 return struct.pack(AIVDM_RECORD5_FORMAT, *Nmea5.to_values(self))
1028 def from_file(self, file):
1029 record = file.read(AIVDM_RECORD5_LENGTH)
1030 Nmea5.from_record(self, record)
1033 def new_from_file(file):
1034 record = file.read(AIVDM_RECORD5_LENGTH)
1035 return Nmea5.new_from_record(record)
1037 def from_lastinfo(self, strmmsi):
1038 filename_nmea5 = os.path.join(DBPATH,
1040 _hash3_pathfilename(strmmsi+'.nmea5'))
1042 f = file(filename_nmea5, 'rb')
1044 logging.debug("file %s doesn't exists" % filename_nmea5)
1047 Nmea5.from_file(self, f)
1051 def new_from_lastinfo(strmmsi):
1052 filename_nmea5 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea5')
1054 f = file(filename_nmea5, 'rb')
1056 logging.debug("file %s doesn't exists" % filename_nmea5)
1059 record = f.read(AIVDM_RECORD5_LENGTH)
1061 return Nmea5.new_from_record(record)
1064 def _clean_str(txt):
1067 return txt.replace('\0','').replace('@', '').strip()
1069 def get_name(self, default='Unknown'):
1070 result = self._clean_str(self.name)
1075 def get_callsign(self, default='Unknown'):
1076 return self._clean_str(self.callsign) or default
1078 def get_shiptype(self, default='Unknown'):
1079 return SHIP_TYPES.get(self.type, default)
1081 def get_length(self):
1082 return self.dim_bow + self.dim_stern
1084 def get_width(self):
1085 return self.dim_port + self.dim_starboard
1087 _monthes = 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(',')
1088 def get_eta_str(self, default='Unknown'):
1089 if not self.eta_M and not self.eta_D:
1093 if self.eta_M <= len(Nmea5._monthes):
1094 result += Nmea5._monthes[self.eta_M - 1]
1096 result += '%02d' % self.eta_M
1101 result += '%02d' % self.eta_D
1104 if self.eta_h != 24:
1105 result += ' %02d' % self.eta_h
1106 if self.eta_m == 60:
1109 result += ':%02d' % self.eta_m
1112 def get_draught_str(self, default='Unknown'):
1113 if not self.draught:
1115 return '%.1f meters' % (self.draught/10.)
1117 def get_destination(self, default='Unknown'):
1118 return self._clean_str(self.destination) or default
1120 def get_source_5_str(self):
1121 return Nmea.format_source(self.source_5)
1123 class Nmea(Nmea1, Nmea5):
1125 This is nmea info, a merge of nmea1 and nmea5 packets
1127 def __init__(self, strmmsi):
1128 self.strmmsi = strmmsi
1129 Nmea1.__init__(self, timestamp=0)
1130 Nmea5.__init__(self, timestamp=0)
1132 ########################
1133 # Because of multiple inheritance some functions are unavailable:
1134 def _nmea_not_implemented(*args, **kargs):
1135 # used to avoid conflicting inherited members
1136 raise NotImplementedError
1137 from_values = _nmea_not_implemented
1138 to_values = _nmea_not_implemented
1139 from_record = _nmea_not_implemented
1140 new_from_record = _nmea_not_implemented
1141 to_record = _nmea_not_implemented
1142 from_file = _nmea_not_implemented
1143 new_from_file = _nmea_not_implemented
1144 ########################
1146 def from_lastinfo(self, strmmsi):
1147 Nmea1.from_lastinfo(self, strmmsi)
1148 Nmea5.from_lastinfo(self, strmmsi)
1151 def new_from_lastinfo(strmmsi):
1152 # better than unimplemented, but not optimal
1153 nmea = Nmea(strmmsi)
1154 nmea.from_lastinfo(strmmsi)
1158 def get_flag(self, default=u'Unknown'):
1159 if self.strmmsi.startswith('00') and self.strmmsi[3:5]!='MI':
1160 ref_mmsi = self.strmmsi[2:]
1162 ref_mmsi = self.strmmsi
1163 country_mid = int(ref_mmsi[0:3])
1164 country_name = COUNTRIES_MID.get(country_mid, default)
1167 def get_mmsi_public(self, default='Unknown'):
1168 if self.strmmsi.isdigit():
1172 def get_title(self):
1174 Returns the name of the ship if available
1177 return self.get_name(None) or self.get_mmsi_public()
1179 def get_last_timestamp(self):
1181 Returns the most recent of update from timestamp1, timestamp5
1183 if self.timestamp_1 > self.timestamp_5:
1184 return self.timestamp_1
1186 return self.timestamp_5
1188 def get_last_updated_delta_str(self):
1190 Returns a pretty formated update data as a string
1192 lastupdate = self.get_last_timestamp()
1195 dt_lastupdate = datetime.utcfromtimestamp(lastupdate)
1196 delta = datetime.utcnow() - dt_lastupdate
1197 return nice_timedelta_str(delta) + u' ago'
1199 def get_last_updated_str(self):
1201 Returns a pretty formated update data as a string
1203 lastupdate = self.get_last_timestamp()
1206 dt_lastupdate = datetime.utcfromtimestamp(lastupdate)
1207 delta = datetime.utcnow() - dt_lastupdate
1208 return dt_lastupdate.strftime('%Y-%m-%d %H:%M:%S GMT') + ' (' + nice_timedelta_str(delta) + ' ago)'
1211 def format_source(infosrc):
1212 if infosrc == '\0\0\0\0':
1214 elif infosrc.startswith('MI'):
1215 if len(infosrc) == 4:
1216 return _get_mi_sourcename(struct.unpack('<2xH', infosrc)[0])
1218 return u'Manual input'
1219 elif infosrc.startswith('U'):
1220 return u'User input'
1221 elif infosrc.startswith('NM'):
1222 return u'NMEA packets from '+xml_escape(infosrc[2:])
1223 elif infosrc.startswith('SP'):
1224 return u"ShipPlotter user %s" % infosrc[2:]
1225 elif infosrc.startswith('ST'):
1226 return u"Spot track %s" % infosrc[2:]
1227 elif infosrc == u'MTWW':
1228 return u'MarineTraffic.com web site'
1229 elif infosrc == u'MTTR':
1230 return u'MarineTraffic.com track files'
1235 Maps the csv header name to matching function to call to get the data.
1237 csv_name_to_function = {
1238 'mmsi': lambda nmea: nmea.strmmsi,
1240 'name': Nmea5.get_name,
1241 'imo': lambda nmea: str(nmea.imo),
1242 'callsign': Nmea5.get_callsign,
1243 'type': lambda nmea: str(nmea.type) + '-' + nmea.get_shiptype(),
1244 'length':lambda nmea: str(nmea.get_length()),
1245 'width': lambda nmea: str(nmea.get_width()),
1246 'datetime': lambda nmea: datetime.utcfromtimestamp(nmea.get_last_timestamp()).strftime('%Y-%m-%dT%H:%M:%SZ'),
1247 'status': Nmea1.get_status,
1248 'sog': Nmea1.get_sog_str,
1249 'latitude': Nmea1.get_latitude_str,
1250 'longitude': Nmea1.get_longitude_str,
1251 'cog': Nmea1.get_cog_str,
1252 'heading': Nmea1.get_heading_str,
1253 'destination': Nmea5.get_destination,
1254 'eta': Nmea5.get_eta_str,
1255 'draught': Nmea5.get_draught_str,
1258 def get_dump_row(self, fieldnames):
1260 for fieldname in fieldnames:
1261 f = self.csv_name_to_function[fieldname]
1262 result.append(f(self))
1265 #def get_dump_row(self):
1270 # return txt.replace('\0','').replace('@', '').strip()
1272 # result.append(self.strmmsi)
1273 # result.append(self.get_flag().encode('utf-8'))
1274 # result.append(self.get_name())
1275 # result.append(str(self.imo))
1276 # result.append(_clean(self.callsign))
1277 # result.append(str(self.type) + '-' + SHIP_TYPES.get(self.type, 'unknown'))
1278 # d = self.dim_bow + self.dim_stern
1282 # result.append(None)
1283 # d = self.dim_port + self.dim_starboard
1287 # result.append(None)
1288 # result.append(datetime.utcfromtimestamp(self.timestamp_1).strftime('%Y-%m-%dT%H:%M:%SZ'))
1289 # result.append(STATUS_CODES.get(self.status, 'unknown'))
1290 # if self.sog != AIS_SOG_NOT_AVAILABLE:
1291 # result.append(str(self.sog/AIS_SOG_SCALE))
1293 # result.append(None)
1294 # if self.latitude != AIS_LAT_NOT_AVAILABLE:
1295 # result.append(str(self.latitude/AIS_LATLON_SCALE))
1297 # result.append(None)
1298 # if self.longitude != AIS_LON_NOT_AVAILABLE:
1299 # result.append(str(self.longitude/AIS_LATLON_SCALE))
1301 # result.append(None)
1302 # if self.cog != AIS_COG_NOT_AVAILABLE:
1303 # result.append(str(self.cog/10.))
1305 # result.append(None)
1306 # if self.heading != AIS_NO_HEADING:
1307 # result.append(str(self.heading))
1309 # result.append(None)
1310 # result.append(self.get_destination(''))
1311 # result.append(self.get_eta_str(''))
1312 # result.append(self.draught)
1313 # result.append(self.source_5)
1317 class BankNmea1(list):
1319 That class handle a .nmea1 archive file
1321 def __init__(self, strmmsi, dt):
1323 self.strmmsi = strmmsi
1324 if isinstance(dt, date):
1325 dt = dt.strftime('%Y%m%d')
1328 def get_filename(self):
1329 return os.path.join(DBPATH, 'bydate', self.date, _hash3_pathfilename(self.strmmsi+'.nmea1'))
1331 def __load_from_file(self, file):
1333 Adds all record from opened file in this bank
1334 File must be locked before call
1337 record = file.read(AIVDM_RECORD123_LENGTH)
1340 self.append(Nmea1.new_from_record(record))
1342 def _write_in_file(self, file):
1344 Write all records from that bank in opened file
1345 File must be locked before call
1346 File should be truncated after call
1348 for nmea1 in list.__iter__(self): # self.__iter__ reload the bank, we don't want that
1349 file.write(nmea1.to_record())
1353 file = open(self.get_filename(), 'rb')
1354 lockf(file, LOCK_SH)
1355 except IOError as ioerr:
1356 if ioerr.errno == 2: # No file
1359 self.__load_from_file(file)
1364 Each call reload the file
1367 self.sort_by_date_reverse()
1368 return list.__iter__(self)
1370 def packday(self, remove_manual_input=False, remove_source_name=None):
1371 #print "MMSI", strmmsi
1373 filename = self.get_filename()
1375 file = open(filename, 'r+b') # read/write binary
1376 except IOError as ioerr:
1377 if ioerr.errno != 2: # No file
1379 return self # no data
1380 lockf(file, LOCK_EX)
1381 self.__load_from_file(file)
1384 file_has_changed = False
1385 file_must_be_unlinked = False
1387 logging.debug('PACKING...')
1388 file_has_changed = self.remove_duplicate_timestamp() or file_has_changed
1390 if remove_manual_input:
1391 logging.debug('REMOVING MANUAL INPUT...')
1392 file_has_changed = self.remove_manual_input() or file_has_changed
1394 if remove_source_name:
1395 logging.debug('REMOVING SOURCES STARTING BY %s', remove_source_name)
1396 file_has_changed = self.remove_by_source(source_name_start=remove_source_name) or file_has_changed
1398 if file_has_changed:
1399 logging.debug('SAVING CHANGES')
1401 self._write_in_file(file)
1403 if file.tell() == 0:
1404 file_must_be_unlinked = True
1408 if file_must_be_unlinked:
1409 # FIXME we release the lock before unlinking
1410 # another process might encounter an empty file (not handled)
1411 logging.warning('file was truncated to size 0. unlinking')
1412 os.unlink(filename) # we have the lock (!)
1414 def dump_to_stdout(self):
1416 Print contents to stdout
1419 nmea1.dump_to_stdout()
1421 def sort_by_date(self):
1422 self.sort(lambda n1, n2: n1.timestamp_1 - n2.timestamp_1)
1424 def sort_by_date_reverse(self):
1425 self.sort(lambda n1, n2: n2.timestamp_1 - n1.timestamp_1)
1427 def remove_duplicate_timestamp(self):
1428 file_has_changed = False
1430 return file_has_changed
1431 last_timestamp = self[0].timestamp_1
1433 while i < len(self):
1434 if self[i].timestamp_1 == last_timestamp:
1436 file_has_changed = True
1438 last_timestamp = self[i].timestamp_1
1440 return file_has_changed
1442 def remove_manual_input(self):
1443 file_has_changed = False
1445 while i < len(self):
1446 if self[i].source_1[:2] == 'MI':
1448 file_has_changed = True
1451 return file_has_changed
1453 def remove_by_source(self, source_name_start):
1454 file_has_changed = False
1456 while i < len(self):
1457 #logging.debug('Testing %s ...', self[i].source_1)
1458 if self[i].source_1.startswith(source_name_start):
1459 #logging.debug('Deleting ...')
1461 file_has_changed = True
1463 #logging.debug('Keeping ...')
1465 return file_has_changed
1469 Yields all nmea1 packets between two given datetimes
1470 in REVERSE order (recent information first)
1472 def __init__(self, strmmsi, datetime_end, datetime_begin=None, max_count=0):
1473 self.strmmsi = strmmsi
1474 assert datetime_end is not None
1475 self.datetime_end = datetime_end
1476 self.datetime_begin = datetime_begin or DB_STARTDATE
1477 self.max_count = max_count
1480 dt_end = self.datetime_end
1481 d_end = dt_end.date()
1482 ts_end = datetime_to_timestamp(dt_end)
1483 if self.datetime_begin:
1484 dt_begin = self.datetime_begin
1485 d_begin = dt_begin.date()
1486 ts_begin = datetime_to_timestamp(dt_begin)
1495 if d_begin is not None and d < d_begin:
1497 bank = BankNmea1(self.strmmsi, d)
1499 if ts_begin is not None and nmea1.timestamp_1 < ts_begin:
1501 if nmea1.timestamp_1 > ts_end:
1507 if self.max_count and count >= self.max_count:
1512 class BankNmea5(list):
1514 That class handle a .nmea5 archive file
1516 def __init__(self, strmmsi, dt):
1518 self.strmmsi = strmmsi
1519 if isinstance(dt, date):
1521 dt = dt.strftime('%Y%m%d')
1523 logging.critical('dt=%s', dt)
1527 def get_filename(self):
1528 return os.path.join(DBPATH, 'bydate', self.date, _hash3_pathfilename(self.strmmsi+'.nmea5'))
1530 def __load_from_file(self, file):
1532 Adds all record from opened file in this bank
1533 File must be locked before call
1536 record = file.read(AIVDM_RECORD5_LENGTH)
1539 self.append(Nmea5.new_from_record(record))
1541 def _write_in_file(self, file):
1543 Write all records from that bank in opened file
1544 File must be locked before call
1545 File should be truncated after call
1548 file.write(nmea5.to_record())
1552 file = open(self.get_filename(), 'rb')
1553 lockf(file, LOCK_SH)
1554 except IOError as ioerr:
1555 if ioerr.errno == 2: # No file
1558 self.__load_from_file(file)
1563 Each call reload the file
1566 self.sort_by_date_reverse()
1567 return list.__iter__(self)
1569 def sort_by_date(self):
1570 self.sort(lambda n1, n2: n1.timestamp_5 - n2.timestamp_5)
1572 def sort_by_date_reverse(self):
1573 self.sort(lambda n1, n2: n2.timestamp_5 - n1.timestamp_5)
1577 Yields all nmea5 packets between two given datetimes
1578 in REVERSE order (recent information first)
1580 def __init__(self, strmmsi, datetime_end, datetime_begin=None, max_count=0):
1581 self.strmmsi = strmmsi
1582 assert datetime_end is not None
1583 self.datetime_end = datetime_end
1584 self.datetime_begin = datetime_begin or DB_STARTDATE
1585 self.max_count = max_count
1588 dt_end = self.datetime_end
1589 d_end = dt_end.date()
1590 ts_end = datetime_to_timestamp(dt_end)
1591 if self.datetime_begin:
1592 dt_begin = self.datetime_begin
1593 d_begin = dt_begin.date()
1594 ts_begin = datetime_to_timestamp(dt_begin)
1603 if d_begin is not None and d < d_begin:
1605 bank = BankNmea5(self.strmmsi, d)
1607 if ts_begin is not None and nmea1.timestamp_5 < ts_begin:
1609 if nmea1.timestamp_5 > ts_end:
1615 if self.max_count and count >= self.max_count:
1622 Yields nmea packets matching criteria.
1624 def __init__(self, strmmsi, datetime_end, datetime_begin=None, filters=None, granularity=1, max_count=None):
1625 if granularity <= 0:
1626 logging.warning('Granularity=%d generates duplicate entries', granularity)
1627 self.strmmsi = strmmsi
1628 assert datetime_end is not None
1629 self.datetime_end = datetime_end
1630 self.datetime_begin = datetime_begin or DB_STARTDATE
1631 self.filters = filters or []
1632 self.granularity = granularity
1633 self.max_count = max_count
1636 nmea = Nmea(self.strmmsi)
1637 if self.datetime_begin:
1638 nmea5_datetime_begin = self.datetime_begin - timedelta(30) # go back up to 30 days to get a good nmea5 packet
1640 nmea5_datetime_begin = None
1641 nmea5_iterator = Nmea5Feeder(self.strmmsi, self.datetime_end, nmea5_datetime_begin).__iter__()
1642 nmea5 = Nmea5(self.strmmsi, sys.maxint)
1645 lasttimestamp = sys.maxint
1646 for nmea1 in Nmea1Feeder(self.strmmsi, self.datetime_end, self.datetime_begin):
1647 Nmea1.from_values(nmea, *nmea1.to_values())
1649 # try to get an nmea5 paket older
1650 nmea5_updated = False
1651 while nmea5 is not None and nmea5.timestamp_5 > nmea1.timestamp_1:
1653 nmea5 = nmea5_iterator.next()
1654 nmea5_updated = True
1655 except StopIteration:
1658 if nmea5_updated and nmea5 is not None:
1659 Nmea5.merge_from_values(nmea, *nmea5.to_values())
1661 filtered_out = False
1662 for is_ok in self.filters:
1669 if nmea.timestamp_1 <= lasttimestamp - self.granularity:
1672 if self.max_count and count >= self.max_count:
1674 lasttimestamp = nmea.timestamp_1
1677 def nice_timedelta_str(delta):
1679 disprank = None # first item type displayed
1681 strdelta += str(delta.days)
1683 strdelta += ' days '
1687 delta_s = delta.seconds
1688 delta_m = delta_s // 60
1689 delta_s -= delta_m * 60
1690 delta_h = delta_m // 60
1691 delta_m -= delta_h * 60
1694 strdelta += str(delta_h)
1696 strdelta += ' hours '
1698 strdelta += ' hour '
1699 if disprank is None:
1701 if delta_m and (disprank is None or disprank >= 1):
1702 strdelta += str(delta_m)
1704 strdelta += ' minutes '
1706 strdelta += ' minute '
1707 if disprank is None:
1709 if delta_s and (disprank is None or disprank >= 2):
1710 strdelta += str(delta_s)
1712 strdelta += ' seconds '
1714 strdelta += ' second '
1715 if disprank is None:
1718 strdelta = 'less than a second '
1721 def all_mmsi_generator():
1723 Returns an array of all known strmmsi.
1725 for dirname, dirs, fnames in os.walk(os.path.join(DBPATH, 'last')):
1726 for fname in fnames:
1727 if fname[-6:] == '.nmea1':
1731 def load_fleet_to_uset(fleetid):
1733 Loads a fleet by id.
1734 Returns an array of strmmsi.
1737 sqlexec(u"SELECT mmsi FROM fleet_vessel WHERE fleet_id=" + unicode(fleetid))
1738 cursor = get_common_cursor()
1740 row = cursor.fetchone()
1744 result.append(mmsi_to_strmmsi(mmsi))
1745 logging.debug('fleet=%s', result)
1749 def fleetname_to_fleetid(fleetname):
1750 sqlexec(u"SELECT id FROM fleet WHERE name=%(fleetname)s", {'fleetname': fleetname})
1751 cursor = get_common_cursor()
1752 row = cursor.fetchone()
1757 def mmsiiterator_nohiddenship(mmsiiterator):
1759 filters strmmsi from an strmmsi iterator
1761 for strmmsi in mmsiiterator:
1762 mmsi = strmmsi_to_mmsi(strmmsi)
1763 if mmsi not in ais.inputs.config.get_hidden_mmsi():
1766 def filter_area(nmea, area):
1768 Returns false if position is out of area.
1770 if nmea.latitude == AIS_LAT_NOT_AVAILABLE or nmea.longitude == AIS_LON_NOT_AVAILABLE:
1772 if not area.contains((nmea.latitude/AIS_LATLON_SCALE, nmea.longitude/AIS_LATLON_SCALE)):
1776 def filter_close_to(nmea, lat, lon, miles=1.0):
1778 Returns true if position is closer than miles from (lat, lon)
1780 return dist3_xyz(latlon_to_xyz_deg(lat, lon), latlon_to_xyz_ais(nmea.latitude, nmea.longitude)) <= miles
1783 def filter_far_from(nmea, lat, lon, miles=1.0):
1785 Returns true if position is farther than miles from (lat, lon)
1787 return dist3_xyz(latlon_to_xyz_deg(lat, lon), latlon_to_xyz_ais(nmea.latitude, nmea.longitude)) >= miles
1790 def filter_sog_le(nmea, max_knts):
1792 Returns true if speed over ground is less than max_knts
1794 return nmea.sog/AIS_SOG_SCALE <= max_knts
1797 def filter_sog_ge(nmea, min_knts):
1799 Returns true if speed over ground is less than min_knts
1801 return nmea.sog/AIS_SOG_SCALE >= min_knts
1804 def filter_knownposition(nmea):
1806 Returns false if position is not fully known
1808 # we are filtering out latitude=0 and longitude=0, that is not supposed to be necessary...
1809 return nmea.latitude != AIS_LAT_NOT_AVAILABLE and nmea.longitude != AIS_LON_NOT_AVAILABLE and nmea.latitude != 0 and nmea.longitude != 0
1812 _filter_positioncheck_last_mmsi = None
1813 def filter_speedcheck(nmea, max_mps):
1815 mps is miles per seconds
1817 global _filter_positioncheck_last_mmsi
1818 global _filter_positioncheck_last_time
1819 global _filter_positioncheck_last_time_failed
1820 global _filter_positioncheck_last_lat
1821 global _filter_positioncheck_last_lon
1822 global _filter_positioncheck_error_count
1823 if nmea.strmmsi != _filter_positioncheck_last_mmsi:
1824 _filter_positioncheck_last_time = None
1825 _filter_positioncheck_last_mmsi = nmea.strmmsi
1826 _filter_positioncheck_error_count = 0
1827 if _filter_positioncheck_last_time is not None:
1828 seconds = _filter_positioncheck_last_time - nmea.timestamp_1
1829 distance = dist3_latlong_ais((_filter_positioncheck_last_lat, _filter_positioncheck_last_lon), (nmea.latitude, nmea.longitude))
1831 speed = distance/seconds
1833 if _filter_positioncheck_error_count < 10:
1834 logging.debug("Ignoring point: distance = %s, time = %s, speed = %s kt, source = %s", distance, seconds, distance/seconds*3600, repr(nmea.source_1))
1835 if _filter_positioncheck_error_count == 0 or _filter_positioncheck_last_time_failed != nmea.timestamp_1:
1836 _filter_positioncheck_error_count += 1
1837 _filter_positioncheck_last_time_failed = nmea.timestamp_1
1840 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))
1841 _filter_positioncheck_error_count = 0
1842 _filter_positioncheck_last_time = nmea.timestamp_1
1843 _filter_positioncheck_last_lat = nmea.latitude
1844 _filter_positioncheck_last_lon = nmea.longitude
1850 Perform various operation on the database
1851 For usage, see "ais --help"
1853 from optparse import OptionParser, OptionGroup
1856 parser = OptionParser(usage='%prog [options] { mmsi | @fleetname | ^fleetid }+ | all')
1858 parser.add_option('-d', '--debug',
1859 action='store_true', dest='debug', default=False,
1862 parser.add_option('-e', '--end',
1863 action='store', dest='sdt_end', metavar="'YYYYMMDD HHMMSS'",
1864 help='End data processing on that GMT date time.'
1866 ' If a date is provided without time, time defaults to 235959.')
1867 parser.add_option('-s', '--start',
1868 action='store', dest='sdt_start', metavar="'YYYYMMDD HHMMSS'",
1869 help='Start data processing on that date.'
1870 ' Using that option enables multiple output of the same boat.'
1871 ' Disabled by default.'
1872 ' If a date is provided without time, time default to 000000.'
1873 ' If other options enable multiple output, default to 1 day before'
1874 ' --end date/time.')
1875 parser.add_option('--duration',
1876 action='store', dest='sdt_duration', metavar="DURATION",
1877 help='Duration of reference period.'
1878 ' Last character may be S for seconds, M(inutes), D(ays), W(eeks)'
1879 ' Default is seconds.'
1880 ' This is the time length bewteen --start and --end above.'
1881 ' If you want multiple output of the same boat, you may use '
1882 ' --start, --end or --duration, 2 of them, but not 3 of them.')
1883 parser.add_option('-g', '--granularity',
1884 action='store', type='int', dest='granularity', metavar='SECONDS',
1885 help='Dump only one position every granularity seconds.'
1886 ' Using that option enables multiple output of the same boat.'
1887 ' If other options enable multiple output, defaults to 600'
1889 parser.add_option('--max',
1890 action='store', type='int', dest='max_count', metavar='NUMBER',
1891 help='Dump a maximum of NUMBER positions every granularity seconds.'
1892 'Using that option enables multiple output of the same boat.')
1894 parser.add_option('--show-hidden-ships',
1895 action='store_true', dest='show_hidden_ships', default=False,
1896 help='Include hidden ships in results')
1898 parser.add_option('--filter-knownposition',
1899 action='store_true', dest='filter_knownposition', default=False,
1900 help="Eliminate unknown positions from results.")
1902 parser.add_option('--filter-speedcheck',
1903 action='store', type='int', dest='speedcheck', default=200, metavar='KNOTS',
1904 help='Eliminate erroneaous positions from results,'
1905 ' based on impossible speed.')
1907 parser.add_option('--filter-type',
1908 action='append', type='int', dest='type_list', metavar='TYPE',
1909 help="process a specific ship type.")
1910 parser.add_option('--help-types',
1911 action='store_true', dest='help_types', default=False,
1912 help="display list of available types")
1914 parser.add_option('--filter-area',
1915 action='store', type='str', dest='area_file', metavar="FILE.KML",
1916 help="only process a specific area as defined in a kml polygon file.")
1917 parser.add_option('--filter-farfrom',
1918 action='store', dest='far_from', nargs=3, metavar='LAT LONG MILES',
1919 help="only show ships farther than MILES miles from LAT,LONG")
1920 parser.add_option('--filter-closeto',
1921 action='store', dest='close_to', nargs=3, metavar='LAT LONG MILES',
1922 help="only show ships closer than MILES miles from LAT,LONG")
1923 parser.add_option('--filter-sog-le',
1924 action='store', dest='sog_le', metavar='KNOTS',
1925 help='only show ships when speed over ground is less or equal than KNOTS.')
1926 parser.add_option('--filter-sog-ge',
1927 action='store', dest='sog_ge', metavar='KNOTS',
1928 help='only show ships when speed over ground is greater or equal than KNOTS.')
1930 parser.add_option('--filter-destination',
1931 action='store', type='str', dest='filter_destination', metavar="DESTINATION",
1932 help="Only print ships with that destination.")
1934 parser.add_option('--no-headers',
1935 action='store_false', dest='csv_headers', default=True,
1936 help="skip CSV headers")
1938 parser.add_option('--csv-fields',
1939 action='store', type='str', dest='csv_fields',
1940 default='mmsi,flag,name,imo,callsign,type,length,width,datetime,status,sog,latitude,longitude,cog,heading,destination,eta,draught',
1941 help='Which fields should be extracted for csv output. Default=%default')
1944 expert_group = OptionGroup(parser, "Expert Options",
1945 "You normaly don't need any of these")
1947 expert_group.add_option('--db',
1948 action='store', dest='db', default=DBPATH,
1949 help="path to filesystem database. Default=%default")
1951 expert_group.add_option('--debug-sql',
1952 action='store_true', dest='debug_sql', default=False,
1953 help="print all sql queries to stdout before running them")
1955 expert_group.add_option('--action',
1956 choices=('dump', 'removemanual', 'removebysource', 'mmsidump', 'nirgaldebug', 'fixdestination'), default='dump',
1957 help='Possible values are:\n'
1958 'dump: dump values in csv format. This is the default.\n'
1959 'removemanual: Delete Manual Input entries from the database.\n'
1960 'mmsidump: Dump mmsi')
1961 parser.add_option_group(expert_group)
1963 (options, args) = parser.parse_args()
1966 if options.help_types:
1967 print "Known ship types:"
1968 keys = SHIP_TYPES.keys()
1971 print k, SHIP_TYPES[k]
1977 loglevel = logging.DEBUG
1979 loglevel = logging.INFO
1980 logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
1982 if options.debug_sql:
1990 print >> sys.stderr, "No ship to process"
1993 target_mmsi_iterator = [] # strmmsi
1998 elif arg.startswith('@'):
1999 target_mmsi_iterator += load_fleet_to_uset(fleetname_to_fleetid(arg[1:]))
2000 elif arg.startswith('^'):
2001 target_mmsi_iterator += load_fleet_to_uset(int(arg[1:]))
2003 target_mmsi_iterator.append(arg)
2005 if target_mmsi_iterator:
2006 logging.warning('Selecting all ships, ignoring other arguments')
2007 target_mmsi_iterator = all_mmsi_generator()
2009 if not options.show_hidden_ships:
2010 target_mmsi_iterator = mmsiiterator_nohiddenship(target_mmsi_iterator)
2016 if options.sdt_start:
2017 # remove non digit characters
2018 options.sdt_start = "".join([ c for c in options.sdt_start if c.isdigit()])
2019 if len(options.sdt_start)==14:
2020 options.sdt_start = datetime.strptime(options.sdt_start, '%Y%m%d%H%M%S')
2021 elif len(options.sdt_start)==8:
2022 options.sdt_start = datetime.strptime(options.sdt_start, '%Y%m%d')
2024 print >> sys.stderr, "Invalid format for --start option"
2028 # remove non digit characters
2029 options.sdt_end = "".join([ c for c in options.sdt_end if c.isdigit()])
2030 if len(options.sdt_end)==14:
2031 options.sdt_end = datetime.strptime(options.sdt_end, '%Y%m%d%H%M%S')
2032 elif len(options.sdt_end)==8:
2033 options.sdt_end = datetime.strptime(options.sdt_end, '%Y%m%d')
2034 options.sdt_end = datetime.combine(options.sdt_end.date(), time(23, 59, 59))
2036 print >> sys.stderr, "Invalid format for --end option"
2039 if options.sdt_duration:
2041 options.sdt_duration = options.sdt_duration.replace(' ', '')
2043 options.sdt_duration = options.sdt_duration.upper()
2044 if options.sdt_duration[-1] == 'S':
2045 options.sdt_duration = options.sdt_duration[:-1]
2047 elif options.sdt_duration[-1] == 'M':
2048 options.sdt_duration = options.sdt_duration[:-1]
2050 elif options.sdt_duration[-1] == 'H':
2051 options.sdt_duration = options.sdt_duration[:-1]
2052 duration_unit = 60*60
2053 elif options.sdt_duration[-1] == 'D':
2054 options.sdt_duration = options.sdt_duration[:-1]
2055 duration_unit = 24*60*60
2056 elif options.sdt_duration[-1] == 'W':
2057 options.sdt_duration = options.sdt_duration[:-1]
2058 duration_unit = 7*24*60*60
2062 options.sdt_duration = long(options.sdt_duration)
2064 print >> sys.stderr, "Can't parse duration"
2066 options.sdt_duration = timedelta(0, options.sdt_duration * duration_unit)
2068 if options.sdt_start or options.sdt_duration or options.granularity is not None or options.max_count:
2069 # Time period is enabled (note that date_end only defaults to one day archives ending then)
2070 if not options.sdt_start and not options.sdt_end and not options.sdt_duration:
2071 options.sdt_duration = timedelta(1) # One day
2072 # continue without else
2073 if not options.sdt_start and not options.sdt_end and options.sdt_duration:
2074 dt_end = datetime.utcnow()
2075 dt_start = dt_end - options.sdt_duration
2076 #elif not options.sdt_start and options.sdt_end and not options.sdt_duration:
2078 elif not options.sdt_start and options.sdt_end and options.sdt_duration:
2079 dt_end = options.sdt_end
2080 dt_start = dt_end - options.sdt_duration
2081 elif options.sdt_start and not options.sdt_end and not options.sdt_duration:
2082 dt_start = options.sdt_start
2083 dt_end = datetime.utcnow()
2084 elif options.sdt_start and not options.sdt_end and options.sdt_duration:
2085 dt_start = options.sdt_start
2086 dt_end = dt_start + options.sdt_duration
2087 elif options.sdt_start and options.sdt_end and not options.sdt_duration:
2088 dt_start = options.sdt_start
2089 dt_end = options.sdt_end
2091 assert options.sdt_start and options.sdt_end and options.sdt_duration, 'Internal error'
2092 print >> sys.stderr, "You can't have all 3 --start --end and --duration"
2094 if options.granularity is None:
2095 options.granularity = 600
2097 # Only get one position
2100 dt_end = options.sdt_end
2102 dt_end = datetime.utcnow()
2103 options.max_count = 1
2104 if options.granularity is None:
2105 options.granularity = 600
2107 logging.debug('--start is %s', dt_start)
2108 logging.debug('--end is %s', dt_end)
2116 if options.filter_knownposition:
2117 filters.append(filter_knownposition)
2119 if options.speedcheck != 0:
2120 maxmps = options.speedcheck / 3600. # from knots to NM per seconds
2121 filters.append(lambda nmea: filter_speedcheck(nmea, maxmps))
2123 if options.area_file:
2124 area = load_area_from_kml_polygon(options.area_file)
2125 filters.append(lambda nmea: filter_area(nmea, area))
2127 if options.close_to:
2129 lat = clean_latitude(unicode(options.close_to[0], 'utf-8'))
2130 lon = clean_longitude(unicode(options.close_to[1], 'utf-8'))
2131 except LatLonFormatError as err:
2132 print >> sys.stderr, err.args
2134 miles = float(options.close_to[2])
2135 filters.append(lambda nmea: filter_close_to(nmea, lat, lon, miles))
2137 if options.far_from:
2139 lat = clean_latitude(unicode(options.far_from[0], 'utf-8'))
2140 lon = clean_longitude(unicode(options.far_from[1], 'utf-8'))
2141 except LatLonFormatError as err:
2142 print >> sys.stderr, err.args
2144 miles = float(options.far_from[2])
2145 filters.append(lambda nmea: filter_far_from(nmea, lat, lon, miles))
2148 filters.append(lambda nmea: filter_sog_le(nmea, float(options.sog_le)))
2150 filters.append(lambda nmea: filter_sog_ge(nmea, float(options.sog_ge)))
2152 if options.type_list:
2153 def filter_type(nmea):
2154 return nmea.type in options.type_list
2155 filters.append(filter_type)
2157 if options.filter_destination:
2158 filters.append(lambda nmea: nmea.destination.startswith(options.filter_destination))
2164 if options.action == 'dump':
2165 fields = options.csv_fields.split(',')
2166 output = csv.writer(sys.stdout)
2167 if options.csv_headers:
2168 output.writerow(fields)
2169 for mmsi in target_mmsi_iterator:
2170 logging.debug('Considering %s', repr(mmsi))
2171 assert dt_end is not None
2172 for nmea in NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count):
2173 output.writerow(nmea.get_dump_row(fields))
2175 elif options.action == 'removemanual':
2177 print >> sys.stderr, "removemanual action doesn't support filters"
2180 # TODO: dates = range dt_start, dt_end
2181 dt = dt_start.date()
2182 while dt < dt_end.date():
2183 logging.info("Processing date %s", dt)
2184 for mmsi in target_mmsi_iterator:
2185 BankNmea1(mmsi, dt).packday(remove_manual_input=True)
2186 dt = dt + timedelta(1)
2188 elif options.action == 'removebysource':
2190 print >> sys.stderr, "removebysource action doesn't support filters"
2193 # TODO: dates = range dt_start, dt_end
2194 dt = dt_start.date()
2195 while dt <= dt_end.date():
2196 logging.info("Processing date %s", dt)
2197 for mmsi in target_mmsi_iterator:
2198 if BankNmea1(mmsi, dt).packday(remove_source_name='MT'):
2199 logging.info('File was modified. mmsi=%s dt=%s', mmsi, dt)
2200 dt = dt + timedelta(1)
2202 elif options.action == 'mmsidump':
2203 for strmmsi in target_mmsi_iterator :
2206 elif options.action == 'fixdestination':
2207 for mmsi in target_mmsi_iterator:
2208 for nmea in NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count):
2209 destination = nmea.destination.rstrip(' @\0')
2211 sqlexec(u'UPDATE vessel SET destination = %(destination)s WHERE mmsi=%(mmsi)s AND destination IS NULL', {'mmsi':strmmsi_to_mmsi(mmsi), 'destination':destination})
2212 logging.info('%s -> %s', mmsi, repr(destination))
2214 break # go to next mmsi
2217 if __name__ == '__main__':