2 # -*- coding: utf-8 -*-
4 from __future__ import division
9 from datetime import datetime, timedelta, date, time
10 from fcntl import lockf, LOCK_EX, LOCK_UN, LOCK_SH
13 from ais.ntools import *
15 from ais.area import load_area_from_kml_polygon
16 from ais.earth3d import dist3_latlong_ais, dist3_xyz, latlon_to_xyz_deg, latlon_to_xyz_ais
19 'DB_STARTDATE', 'DBPATH',
20 'COUNTRIES_MID', 'STATUS_CODES', 'SHIP_TYPES',
21 'AIS_STATUS_NOT_AVAILABLE',
22 'AIS_ROT_HARD_LEFT', 'AIS_ROT_HARD_RIGHT', 'AIS_ROT_NOT_AVAILABLE',
23 'AIS_LATLON_SCALE', 'AIS_LON_NOT_AVAILABLE', 'AIS_LAT_NOT_AVAILABLE',
24 'AIS_COG_SCALE', 'AIS_COG_NOT_AVAILABLE',
26 'AIS_SOG_SCALE', 'AIS_SOG_NOT_AVAILABLE', 'AIS_SOG_FAST_MOVER', 'AIS_SOG_MAX_SPEED',
27 #'_hash3_pathfilename',
28 'db_bydate_addrecord',
29 'db_lastinfo_setrecord_ifnewer',
46 'fleetname_to_fleetid',
52 'filter_knownposition',
56 DB_STARTDATE = datetime(2008, 6, 1)
58 # This is the location of the filesystem database
59 DBPATH = '/var/lib/ais/db'
61 # see make-countries.py
70 208: u'Vatican City State',
88 231: u'Faroe Islands',
89 232: u'United Kingdom',
90 233: u'United Kingdom',
91 234: u'United Kingdom',
92 235: u'United Kingdom',
108 252: u'Liechtenstein',
122 267: u'Slovak Republic',
125 270: u'Czech Republic',
128 273: u'Russian Federation',
129 274: u'The Former Yugoslav Republic of Macedonia',
137 304: u'Antigua and Barbuda',
138 305: u'Antigua and Barbuda',
139 306: u'Netherlands Antilles',
148 319: u'Cayman Islands',
152 327: u'Dominican Republic',
159 338: u'United States of America',
161 341: u'Saint Kitts and Nevis',
176 361: u'Saint Pierre and Miquelon',
177 362: u'Trinidad and Tobago',
178 364: u'Turks and Caicos Islands',
179 366: u'United States of America',
180 367: u'United States of America',
181 368: u'United States of America',
182 369: u'United States of America',
186 375: u'Saint Vincent and the Grenadines',
187 376: u'Saint Vincent and the Grenadines',
188 377: u'Saint Vincent and the Grenadines',
189 378: u'British Virgin Islands',
190 379: u'United States Virgin Islands',
192 403: u'Saudi Arabia',
202 423: u'Azerbaijani Republic',
207 434: u'Turkmenistan',
214 445: u"Democratic People's Republic of Korea",
217 451: u'Kyrgyz Republic',
225 468: u'Syrian Arab Republic',
226 470: u'United Arab Emirates',
230 478: u'Bosnia and Herzegovina',
234 508: u'Brunei Darussalam',
240 516: u'Christmas Island',
241 518: u'Cook Islands',
246 531: u"Lao People's Democratic Republic",
248 536: u'Northern Mariana Islands',
249 538: u'Marshall Islands',
250 540: u'New Caledonia',
253 546: u'French Polynesia',
255 553: u'Papua New Guinea',
256 555: u'Pitcairn Island',
257 557: u'Solomon Islands',
258 559: u'American Samoa',
268 578: u'Wallis and Futuna Islands',
269 601: u'South Africa',
272 607: u'Saint Paul and Amsterdam Islands',
273 608: u'Ascension Island',
277 612: u'Central African Republic',
282 618: u'Crozet Archipelago',
283 619: u"Côte d'Ivoire",
288 626: u'Gabonese Republic',
291 630: u'Guinea-Bissau',
292 631: u'Equatorial Guinea',
294 633: u'Burkina Faso',
296 635: u'Kerguelen Islands',
299 642: u"Socialist People's Libyan Arab Jamahiriya",
315 665: u'Saint Helena',
316 666: u'Somali Democratic Republic',
317 667: u'Sierra Leone',
318 668: u'Sao Tome and Principe',
321 671: u'Togolese Republic',
325 676: u'Democratic Republic of the Congo',
329 701: u'Argentine Republic',
335 740: u'Falkland Islands',
346 0: 'Under way using engine',
348 2: 'Not under command',
349 3: 'Restricted manoeuverability',
350 4: 'Constrained by her draught',
353 7: 'Engaged in Fishing',
354 8: 'Under way sailing',
355 9: '9 - Reserved for future amendment of Navigational Status for HSC',
356 10: '10 - Reserved for future amendment of Navigational Status for WIG',
357 11: '11 - Reserved for future use',
358 12: '12 - Reserved for future use',
359 13: '13 - Reserved for future use',
360 14: '14 - Reserved for future use', # Land stations
361 15: 'Not defined', # default
365 0: 'Not available (default)',
366 1: 'Reserved for future use',
367 2: 'Reserved for future use',
368 3: 'Reserved for future use',
369 4: 'Reserved for future use',
370 5: 'Reserved for future use',
371 6: 'Reserved for future use',
372 7: 'Reserved for future use',
373 8: 'Reserved for future use',
374 9: 'Reserved for future use',
375 10: 'Reserved for future use',
376 11: 'Reserved for future use',
377 12: 'Reserved for future use',
378 13: 'Reserved for future use',
379 14: 'Reserved for future use',
380 15: 'Reserved for future use',
381 16: 'Reserved for future use',
382 17: 'Reserved for future use',
383 18: 'Reserved for future use',
384 19: 'Reserved for future use',
385 20: 'Wing in ground (WIG), all ships of this type',
386 21: 'Wing in ground (WIG), Hazardous category A',
387 22: 'Wing in ground (WIG), Hazardous category B',
388 23: 'Wing in ground (WIG), Hazardous category C',
389 24: 'Wing in ground (WIG), Hazardous category D',
390 25: 'Wing in ground (WIG), Reserved for future use',
391 26: 'Wing in ground (WIG), Reserved for future use',
392 27: 'Wing in ground (WIG), Reserved for future use',
393 28: 'Wing in ground (WIG), Reserved for future use',
394 29: 'Wing in ground (WIG), Reserved for future use',
397 32: 'Towing: length exceeds 200m or breadth exceeds 25m',
398 33: 'Dredging or underwater ops',
402 37: 'Pleasure Craft',
405 40: 'High speed craft (HSC), all ships of this type',
406 41: 'High speed craft (HSC), Hazardous category A',
407 42: 'High speed craft (HSC), Hazardous category B',
408 43: 'High speed craft (HSC), Hazardous category C',
409 44: 'High speed craft (HSC), Hazardous category D',
410 45: 'High speed craft (HSC), Reserved for future use',
411 46: 'High speed craft (HSC), Reserved for future use',
412 47: 'High speed craft (HSC), Reserved for future use',
413 48: 'High speed craft (HSC), Reserved for future use',
414 49: 'High speed craft (HSC), No additional information',
416 51: 'Search and Rescue vessel',
419 54: 'Anti-pollution equipment',
420 55: 'Law Enforcement',
421 56: 'Spare - Local Vessel',
422 57: 'Spare - Local Vessel',
423 58: 'Medical Transport',
424 59: 'Ship according to RR Resolution No. 18',
425 60: 'Passenger, all ships of this type',
426 61: 'Passenger, Hazardous category A',
427 62: 'Passenger, Hazardous category B',
428 63: 'Passenger, Hazardous category C',
429 64: 'Passenger, Hazardous category D',
430 65: 'Passenger, Reserved for future use',
431 66: 'Passenger, Reserved for future use',
432 67: 'Passenger, Reserved for future use',
433 68: 'Passenger, Reserved for future use',
434 69: 'Passenger, No additional information',
435 70: 'Cargo', # 'Cargo, all ships of this type',
436 71: 'Cargo, Hazardous category A',
437 72: 'Cargo, Hazardous category B',
438 73: 'Cargo, Hazardous category C',
439 74: 'Cargo, Hazardous category D',
440 75: 'Cargo', # 'Cargo, Reserved for future use',
441 76: 'Cargo', # 'Cargo, Reserved for future use',
442 77: 'Cargo', # 'Cargo, Reserved for future use',
443 78: 'Cargo', # 'Cargo, Reserved for future use',
444 79: 'Cargo', # 'Cargo, No additional information',
445 80: 'Tanker', # 'Tanker, all ships of this type',
446 81: 'Tanker, Hazardous category A',
447 82: 'Tanker, Hazardous category B',
448 83: 'Tanker, Hazardous category C',
449 84: 'Tanker, Hazardous category D',
450 85: 'Tanker', # 'Tanker, Reserved for future use',
451 86: 'Tanker', # 'Tanker, Reserved for future use',
452 87: 'Tanker', # 'Tanker, Reserved for future use',
453 88: 'Tanker', # 'Tanker, Reserved for future use',
454 89: 'Tanker, No additional information',
455 90: 'Other Type, all ships of this type',
456 91: 'Other Type, Hazardous category A',
457 92: 'Other Type, Hazardous category B',
458 93: 'Other Type, Hazardous category C',
459 94: 'Other Type, Hazardous category D',
460 95: 'Other Type, Reserved for future use',
461 96: 'Other Type, Reserved for future use',
462 97: 'Other Type, Reserved for future use',
463 98: 'Other Type, Reserved for future use',
464 99: 'Other Type, no additional information',
465 100: 'Default Navaid',
466 101: 'Reference point',
468 103: 'Offshore Structure',
470 105: 'Light, without sectors',
471 106: 'Light, with sectors',
472 107: 'Leading Light Front',
473 108: 'Leading Light Rear',
474 109: 'Beacon, Cardinal N',
475 110: 'Beacon, Cardinal E',
476 111: 'Beacon, Cardinal S',
477 112: 'Beacon, Cardinal W',
478 113: 'Beacon, Port hand',
479 114: 'Beacon, Starboard hand',
480 115: 'Beacon, Preferred Channel port hand',
481 116: 'Beacon, Preferred Channel starboard hand',
482 117: 'Beacon, Isolated danger',
483 118: 'Beacon, Safe water',
484 119: 'Beacon, Special mark',
485 120: 'Cardinal Mark N',
486 121: 'Cardinal Mark E',
487 122: 'Cardinal Mark S',
488 123: 'Cardinal Mark W',
489 124: 'Port hand Mark',
490 125: 'Starboard hand Mark',
491 126: 'Preferred Channel Port hand',
492 127: 'Preferred Channel Starboard hand',
493 128: 'Isolated danger',
495 130: 'Manned VTS / Special Mark',
496 131: 'Light Vessel / LANBY',
499 AIS_STATUS_NOT_AVAILABLE = 15
500 AIS_ROT_HARD_LEFT = -127
501 AIS_ROT_HARD_RIGHT = 127
502 AIS_ROT_NOT_AVAILABLE = -128 # not like gpsd
504 AIS_LATLON_SCALE = 600000.0
505 AIS_LON_NOT_AVAILABLE = 0x6791AC0
506 AIS_LAT_NOT_AVAILABLE = 0x3412140
508 AIS_COG_NOT_AVAILABLE = 3600
511 AIS_SOG_NOT_AVAILABLE = 1023
512 AIS_SOG_FAST_MOVER = 1022
513 AIS_SOG_MAX_SPEED = 1021
516 def _hash3_pathfilename(filename):
518 Returns a level 3 directory hashed filename on that basis:
519 123456789 -> 1/12/123/123456789
521 return os.path.join(filename[0], filename[:2], filename[:3], filename)
524 def db_bydate_addrecord(basefilename, record, timestamp):
525 strdt = datetime.utcfromtimestamp(timestamp).strftime('%Y%m%d')
526 filename = os.path.join(DBPATH, 'bydate', strdt, _hash3_pathfilename(basefilename))
527 f = open_with_mkdirs(filename, 'ab')
529 #f.seek(0,2) # go to EOF
530 assert f.tell() % len(record) == 0, 'Invalid length for %s' % filename
535 def db_lastinfo_setrecord_ifnewer(basefilename, record, timestamp):
537 Overwrite last information if date is newer
538 Input record must be complete
540 filename = DBPATH+'/last/'+_hash3_pathfilename(basefilename)
543 f = open(filename, 'r+b')
544 except IOError, ioerr:
547 # File was not found? Ok, create it. FIXME: we should lock something...
548 f = open_with_mkdirs(filename, 'wb')
554 oldrecord = f.read(4)
555 assert len(oldrecord) == 4
556 oldtimestamp = struct.unpack('I', oldrecord)[0]
559 if timestamp > oldtimestamp:
561 assert f.tell() == len(record), \
562 "tell=%s size=%s" % (f.tell(), len(record))
570 def _sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
571 dim_bow, dim_stern, dim_port, dim_starboard, \
572 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
573 ''' Don't call directly '''
575 sqlinfo['mmsi'] = strmmsi_to_mmsi(strmmsi)
576 sqlinfo['updated'] = datetime.utcfromtimestamp(timestamp)
577 sqlinfo['imo'] = imo or None
578 sqlinfo['name'] = name or None
579 sqlinfo['callsign'] = callsign or None
580 sqlinfo['type'] = type
581 sqlinfo['dim_bow'] = dim_bow
582 sqlinfo['dim_stern'] = dim_stern
583 sqlinfo['dim_port'] = dim_port
584 sqlinfo['dim_starboard'] = dim_starboard
585 sqlinfo['destination'] = None
586 eta = '%02d%02d%02d%02d' % ( eta_M, eta_D, eta_h, eta_m)
587 if eta == '00000000':
588 # FIXME tempory hack for corrupted db/latest/*.nmea5 file
592 destination = destination.replace('\0', ' ').rstrip(' @\0')
593 sqlinfo['destination'] = destination or None
594 sqlinfo['source'] = source
595 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)
597 sqlexec(u'UPDATE vessel SET imo = %(imo)s WHERE mmsi=%(mmsi)s AND (imo IS NULL OR updated<%(updated)s)', sqlinfo)
599 sqlexec(u'UPDATE vessel SET name = %(name)s WHERE mmsi=%(mmsi)s AND (name IS NULL OR updated<%(updated)s)', sqlinfo)
600 if sqlinfo['callsign']:
601 sqlexec(u'UPDATE vessel SET callsign = %(callsign)s WHERE mmsi=%(mmsi)s AND (callsign IS NULL OR updated<%(updated)s)', sqlinfo)
603 sqlexec(u'UPDATE vessel SET type = %(type)s WHERE mmsi=%(mmsi)s AND (type IS NULL OR updated<%(updated)s)', sqlinfo)
604 if sqlinfo['dim_bow'] or sqlinfo['dim_stern']:
605 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)
606 if sqlinfo['dim_port'] or sqlinfo['dim_starboard']:
607 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)
608 if sqlinfo['destination'] or sqlinfo['eta'] != '00002460':
609 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)
610 sqlexec(u'UPDATE vessel SET (updated, source) = (%(updated)s, %(source)s) WHERE mmsi=%(mmsi)s AND updated<%(updated)s', sqlinfo)
616 AIVDM_RECORD123_FORMAT = 'IBbhiiII4s'
617 AIVDM_RECORD123_LENGTH = struct.calcsize(AIVDM_RECORD123_FORMAT)
618 AIVDM_RECORD5_FORMAT = 'II20s7sBHHBBBBBBH20s4s'
619 AIVDM_RECORD5_LENGTH = struct.calcsize(AIVDM_RECORD5_FORMAT)
622 def add_nmea1(strmmsi, timestamp, status, rot, sog, \
623 latitude, longitude, cog, heading, source):
625 Input is raw data, unscaled
626 FIXME: lat & lon are inverted compared to raw aivdm structure
628 record = struct.pack(AIVDM_RECORD123_FORMAT, timestamp, status, rot, sog, latitude, longitude, cog, heading, source)
630 filename = strmmsi+'.nmea1'
631 db_bydate_addrecord(filename, record, timestamp)
632 # There's no need to be smart: all the information are taken, or none.
633 return db_lastinfo_setrecord_ifnewer(filename, record, timestamp)
636 def add_nmea5_full(strmmsi, timestamp, imo, name, callsign, type, \
637 dim_bow, dim_stern, dim_port, dim_starboard, \
638 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
640 Input is raw data, unscaled
641 All fields are set, and can be upgraded if the record is newer
642 FIXME: name & callsign are inverted compared to raw aivdm structure
644 record = struct.pack(AIVDM_RECORD5_FORMAT, timestamp, imo, name, callsign, \
645 type, dim_bow, dim_stern, dim_port, dim_starboard, \
646 eta_M, eta_D, eta_h, eta_m, draught, destination, source)
648 filename = strmmsi+'.nmea5'
649 db_bydate_addrecord(filename, record, timestamp)
650 updated = db_lastinfo_setrecord_ifnewer(filename, record, timestamp)
652 _sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
653 dim_bow, dim_stern, dim_port, dim_starboard, \
654 eta_M, eta_D, eta_h, eta_m, draught, destination, source)
657 def add_nmea5_partial(strmmsi, timestamp, imo, name, callsign, type, \
658 dim_bow, dim_stern, dim_port, dim_starboard, \
659 eta_M, eta_D, eta_h, eta_m, draught, destination, source):
661 Input is raw data, unscaled
662 All fields are not set. Only some of them can be upgraded, if they're newer
664 record = struct.pack(AIVDM_RECORD5_FORMAT, \
665 timestamp, imo, name, callsign, type, \
666 dim_bow, dim_stern, dim_port, dim_starboard, \
667 eta_M, eta_D, eta_h, eta_m, draught, destination, \
670 filename = strmmsi + '.nmea5'
671 db_bydate_addrecord(filename, record, timestamp)
674 filename = os.path.join(DBPATH, 'last', _hash3_pathfilename(filename))
676 f = open(filename, 'r+b')
677 except IOError, ioerr:
680 # File was not found? Ok, create it. FIXME: we should lock something...
681 f = open_with_mkdirs(filename, 'wb')
688 oldrecord = f.read(AIVDM_RECORD5_LENGTH)
689 oldtimestamp, oldimo, oldname, oldcallsign, oldtype, \
690 olddim_bow, olddim_stern, olddim_port, olddim_starboard, \
691 oldeta_M, oldeta_D, oldeta_h, oldeta_m, \
692 olddraught, olddestination, oldsource \
693 = struct.unpack(AIVDM_RECORD5_FORMAT, oldrecord)
694 if timestamp > oldtimestamp:
695 # we have incoming recent information
701 callsign = oldcallsign
707 dim_stern = olddim_stern
709 dim_port = olddim_port
710 if dim_starboard == 0:
711 dim_starboard = olddim_starboard
712 if eta_M == 0 or eta_D == 0 or eta_h == 24 or eta_m == 60 \
713 or destination == '':
718 destination = olddestination
721 record = struct.pack(AIVDM_RECORD5_FORMAT, \
722 timestamp, imo, name, callsign, type, \
723 dim_bow, dim_stern, dim_port, dim_starboard, \
724 eta_M, eta_D, eta_h, eta_m, draught, \
730 # we received an obsolete info, but maybe there are some new things in it
731 if oldimo == 0 and imo != 0:
734 if oldname == '' and name != '':
737 if oldcallsign == '' and callsign != '':
738 oldcallsign = callsign
740 if oldtype == 0 and type != 0:
743 if olddim_bow == 0 and dim_bow != 0:
746 if olddim_stern == 0 and dim_stern != 0:
747 olddim_stern = dim_stern
749 if olddim_port == 0 and dim_port != 0:
750 olddim_port = dim_port
752 if olddim_starboard == 0 and dim_starboard != 0:
753 olddim_starboard = dim_starboard
756 if (oldeta_M == 0 or oldeta_D == 0 or olddestination == '') \
757 and ((eta_M != 0 and eta_D != 0) or destination!=''):
762 olddestination = destination
764 if olddraught == 0 and draught != 0:
769 record = struct.pack(AIVDM_RECORD5_FORMAT, \
770 oldtimestamp, oldimo, oldname, \
771 oldcallsign, oldtype, \
772 olddim_bow, olddim_stern, \
773 olddim_port, olddim_starboard, \
774 oldeta_M, oldeta_D, oldeta_h, oldeta_m, \
775 olddraught, olddestination, oldsource)
779 # keep the file locked during SQL updates
781 _sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
782 dim_bow, dim_stern, dim_port, dim_starboard, \
783 eta_M, eta_D, eta_h, eta_m, draught, destination, source)
789 def strmmsi_to_mmsi(strmmsi):
791 Convert from str mmsi to sql-int mmsi
792 Special treatment manal input
794 if strmmsi.isdigit():
797 assert strmmsi[3:5] == 'MI'
798 strmmsi = strmmsi[:3]+'00'+strmmsi[5:]
799 return int('-'+strmmsi)
802 def mmsi_to_strmmsi(mmsi):
804 Convert from sql-into mmsi to str mmsi
805 Special treatment manal input
809 strmmsi = "%08d" % -mmsi
810 assert strmmsi[3:5] == '00'
811 strmmsi = strmmsi[:3]+'MI'+strmmsi[5:]
815 __misources__ = {} # cache of manual source names
816 def _get_mi_sourcename(id):
818 Get the nice name for sources whose id4 starts with 'MI'
821 if not __misources__:
822 sqlexec(u'SELECT id, name FROM mi_source')
824 row = get_common_cursor().fetchone()
827 __misources__[row[0]] = row[1]
828 result = __misources__.get(id, None)
830 return u"Manual input #%s" % id
835 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'):
836 self.timestamp_1 = timestamp
840 self.latitude = latitude
841 self.longitude = longitude
843 self.heading = heading
844 self.source_1 = source
846 from_values = __init__
849 return self.timestamp_1, self.status, self.rot, self.sog, self.latitude, self.longitude, self.cog, self.heading, self.source_1
851 def from_record(self, record):
852 values = struct.unpack(AIVDM_RECORD123_FORMAT, record)
853 Nmea1.__init__(self, *values)
856 def new_from_record(record):
857 values = struct.unpack(AIVDM_RECORD123_FORMAT, record)
858 return Nmea1(*values)
861 return struct.pack(AIVDM_RECORD123_FORMAT, *Nmea1.to_values(self))
863 def from_file(self, file):
864 record = file.read(AIVDM_RECORD123_LENGTH)
865 Nmea1.from_record(self, record)
868 def new_from_file(file):
869 record = file.read(AIVDM_RECORD123_LENGTH)
870 return Nmea1.new_from_record(record)
872 def from_lastinfo(self, strmmsi):
873 filename_nmea1 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea1')
875 f = file(filename_nmea1, 'rb')
877 logging.debug("file %s doesn't exists" % filename_nmea1)
880 Nmea1.from_file(self, f)
884 def new_from_lastinfo(strmmsi):
885 filename_nmea1 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea1')
887 f = file(filename_nmea1, 'rb')
889 logging.debug("file %s doesn't exists" % filename_nmea1)
892 record = f.read(AIVDM_RECORD123_LENGTH)
894 return Nmea1.new_from_record(record)
897 def dump_to_stdout(self):
899 Prints content to stdout
901 print datetime.utcfromtimestamp(self.timestamp_1),
902 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):
910 return txt.replace('\0','').replace('@', '').strip()
912 def get_status(self, default='Unknown'):
913 return STATUS_CODES.get(self.status, default)
915 def get_sog_str(self, default='Unknown'):
916 if self.sog == AIS_SOG_NOT_AVAILABLE:
918 if self.sog == AIS_SOG_FAST_MOVER:
919 return 'over 102.2 kts'
920 return '%.1f kts' % (self.sog/AIS_SOG_SCALE)
922 def get_rot_str(self, default='Unknown'):
923 if self.rot == AIS_ROT_NOT_AVAILABLE:
935 result = '%d %% to ' % rot*100./127
939 def _decimaldegree_to_dms(f, emispheres):
945 result = '%d°' % int(f)
947 result += '%02.05f\' ' % f
951 def get_latitude_str(self, default='Unknown'):
952 if self.latitude == AIS_LAT_NOT_AVAILABLE:
954 return Nmea1._decimaldegree_to_dms(self.latitude / AIS_LATLON_SCALE, 'NS')
956 def get_longitude_str(self, default='Unknown'):
957 if self.longitude == AIS_LON_NOT_AVAILABLE:
959 return Nmea1._decimaldegree_to_dms(self.longitude / AIS_LATLON_SCALE, 'EW')
961 def get_cog_str(self, default='Unknown'):
962 if self.cog == AIS_COG_NOT_AVAILABLE:
964 return '%.1f°' % (self.cog/10.)
966 def get_heading_str(self, default='Unknown'):
967 if self.heading == AIS_NO_HEADING:
969 return '%s°' % self.heading
971 def get_source_1_str(self):
972 return Nmea.format_source(self.source_1)
975 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=''):
976 self.timestamp_5 = timestamp
979 self.callsign = callsign
981 self.dim_bow = dim_bow
982 self.dim_stern = dim_stern
983 self.dim_port = dim_port
984 self.dim_starboard = dim_starboard
989 self.draught = draught
990 self.destination = destination
991 self.source_5 = source
993 from_values = __init__
995 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=''):
997 if self.imo == 0 or imo != 0:
1000 if self.name == '' or name != '':
1003 if self.callsign == '' or callsign != '':
1004 self.callsign = callsign
1006 if self.type == 0 or type_ != 0:
1009 if self.dim_bow == 0 or dim_bow != 0:
1010 self.dim_bow = dim_bow
1012 if self.dim_stern == 0 or dim_stern != 0:
1013 self.dim_stern = dim_stern
1015 if self.dim_port == 0 or dim_port != 0:
1016 self.dim_port = dim_port
1018 if self.dim_starboard == 0 or dim_starboard != 0:
1019 self.dim_starboard = dim_starboard
1021 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:
1027 if self.draught == 0 or draught != 0:
1028 self.draught = draught
1030 if self.destination == '' or destination != '':
1031 self.destination = destination
1034 self.timestamp_5 = timestamp
1035 self.source_5 = source
1038 def to_values(self):
1039 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
1041 def from_record(self, record):
1042 values = struct.unpack(AIVDM_RECORD5_FORMAT, record)
1043 Nmea5.__init__(self, *values)
1046 def new_from_record(record):
1047 values = struct.unpack(AIVDM_RECORD5_FORMAT, record)
1048 return Nmea5(*values)
1050 def to_record(self):
1051 return struct.pack(AIVDM_RECORD5_FORMAT, *Nmea5.to_values(self))
1053 def from_file(self, file):
1054 record = file.read(AIVDM_RECORD5_LENGTH)
1055 Nmea5.from_record(self, record)
1058 def new_from_file(file):
1059 record = file.read(AIVDM_RECORD5_LENGTH)
1060 return Nmea5.new_from_record(record)
1062 def from_lastinfo(self, strmmsi):
1063 filename_nmea5 = os.path.join(DBPATH,
1065 _hash3_pathfilename(strmmsi+'.nmea5'))
1067 f = file(filename_nmea5, 'rb')
1069 logging.debug("file %s doesn't exists" % filename_nmea5)
1072 Nmea5.from_file(self, f)
1076 def new_from_lastinfo(strmmsi):
1077 filename_nmea5 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea5')
1079 f = file(filename_nmea5, 'rb')
1081 logging.debug("file %s doesn't exists" % filename_nmea5)
1084 record = f.read(AIVDM_RECORD5_LENGTH)
1086 return Nmea5.new_from_record(record)
1089 def _clean_str(txt):
1092 return txt.replace('\0','').replace('@', '').strip()
1094 def get_name(self, default='Unknown'):
1095 result = self._clean_str(self.name)
1100 def get_callsign(self, default='Unknown'):
1101 return self._clean_str(self.callsign) or default
1103 def get_shiptype(self, default='Unknown'):
1104 return SHIP_TYPES.get(self.type, default)
1106 def get_length(self):
1107 return self.dim_bow + self.dim_stern
1109 def get_width(self):
1110 return self.dim_port + self.dim_starboard
1112 _monthes = 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(',')
1113 def get_eta_str(self, default='Unknown'):
1114 if not self.eta_M and not self.eta_D:
1118 if self.eta_M <= len(Nmea5._monthes):
1119 result += Nmea5._monthes[self.eta_M - 1]
1121 result += '%02d' % self.eta_M
1126 result += '%02d' % self.eta_D
1129 if self.eta_h != 24:
1130 result += ' %02d' % self.eta_h
1131 if self.eta_m == 60:
1134 result += ':%02d' % self.eta_m
1137 def get_draught_str(self, default='Unknown'):
1138 if not self.draught:
1140 return '%.1f meters' % (self.draught/10.)
1142 def get_destination(self, default='Unknown'):
1143 return self._clean_str(self.destination) or default
1145 def get_source_5_str(self):
1146 return Nmea.format_source(self.source_5)
1148 class Nmea(Nmea1, Nmea5):
1150 This is nmea info, a merge of nmea1 and nmea5 packets
1152 def __init__(self, strmmsi):
1153 self.strmmsi = strmmsi
1154 Nmea1.__init__(self, timestamp=0)
1155 Nmea5.__init__(self, timestamp=0)
1157 ########################
1158 # Because of multiple inheritance some functions are unavailable:
1159 def _nmea_not_implemented(*args, **kargs):
1160 # used to avoid conflicting inherited members
1161 raise NotImplementedError
1162 from_values = _nmea_not_implemented
1163 to_values = _nmea_not_implemented
1164 from_record = _nmea_not_implemented
1165 new_from_record = _nmea_not_implemented
1166 to_record = _nmea_not_implemented
1167 from_file = _nmea_not_implemented
1168 new_from_file = _nmea_not_implemented
1169 ########################
1171 def from_lastinfo(self, strmmsi):
1172 Nmea1.from_lastinfo(self, strmmsi)
1173 Nmea5.from_lastinfo(self, strmmsi)
1176 def new_from_lastinfo(strmmsi):
1177 # better than unimplemented, but not optimal
1178 nmea = Nmea(strmmsi)
1179 nmea.from_lastinfo(strmmsi)
1183 def get_flag(self, default=u'Unknown'):
1184 if self.strmmsi.startswith('00') and self.strmmsi[3:5]!='MI':
1185 ref_mmsi = self.strmmsi[2:]
1187 ref_mmsi = self.strmmsi
1188 country_mid = int(ref_mmsi[0:3])
1189 country_name = COUNTRIES_MID.get(country_mid, default)
1192 def get_mmsi_public(self, default='Unknown'):
1193 if self.strmmsi.isdigit():
1197 def get_title(self):
1199 Returns the name of the ship if available
1202 return self.get_name(None) or self.get_mmsi_public()
1204 def get_last_timestamp(self):
1206 Returns the most recent of update from timestamp1, timestamp5
1208 if self.timestamp_1 > self.timestamp_5:
1209 return self.timestamp_1
1211 return self.timestamp_5
1213 def get_last_updated_delta_str(self):
1215 Returns a pretty formated update data as a string
1217 lastupdate = self.get_last_timestamp()
1220 dt_lastupdate = datetime.utcfromtimestamp(lastupdate)
1221 delta = datetime.utcnow() - dt_lastupdate
1222 return nice_timedelta_str(delta) + u' ago'
1224 def get_last_updated_str(self):
1226 Returns a pretty formated update data as a string
1228 lastupdate = self.get_last_timestamp()
1231 dt_lastupdate = datetime.utcfromtimestamp(lastupdate)
1232 delta = datetime.utcnow() - dt_lastupdate
1233 return dt_lastupdate.strftime('%Y-%m-%d %H:%M:%S GMT') + ' (' + nice_timedelta_str(delta) + ' ago)'
1236 def format_source(infosrc):
1237 if infosrc == '\0\0\0\0':
1239 elif infosrc.startswith('MI'):
1240 if len(infosrc) == 4:
1241 return _get_mi_sourcename(struct.unpack('<2xH', infosrc)[0])
1243 return u'Manual input'
1244 elif infosrc.startswith('U'):
1245 return u'User input'
1246 elif infosrc.startswith('NM'):
1247 return u'NMEA packets from '+xml_escape(infosrc[2:])
1248 elif infosrc.startswith('SP'):
1249 return u"ShipPlotter user %s" % infosrc[2:]
1250 elif infosrc.startswith('ST'):
1251 return u"Spot track %s" % infosrc[2:]
1252 elif infosrc == u'MTWW':
1253 return u'MarineTraffic.com web site'
1254 elif infosrc == u'MTTR':
1255 return u'MarineTraffic.com track files'
1260 Maps the csv header name to matching function to call to get the data.
1262 csv_name_to_function = {
1263 'mmsi': lambda nmea: nmea.strmmsi,
1265 'name': Nmea5.get_name,
1266 'imo': lambda nmea: str(nmea.imo),
1267 'callsign': Nmea5.get_callsign,
1268 'type': lambda nmea: str(nmea.type) + '-' + nmea.get_shiptype(),
1269 'length':lambda nmea: str(nmea.get_length()),
1270 'width': lambda nmea: str(nmea.get_width()),
1271 'datetime': lambda nmea: datetime.utcfromtimestamp(nmea.get_last_timestamp()).strftime('%Y-%m-%dT%H:%M:%SZ'),
1272 'status': Nmea1.get_status,
1273 'sog': Nmea1.get_sog_str,
1274 'latitude': Nmea1.get_latitude_str,
1275 'longitude': Nmea1.get_longitude_str,
1276 'cog': Nmea1.get_cog_str,
1277 'heading': Nmea1.get_heading_str,
1278 'destination': Nmea5.get_destination,
1279 'eta': Nmea5.get_eta_str,
1280 'draught': Nmea5.get_draught_str,
1283 def get_dump_row(self, fieldnames):
1285 for fieldname in fieldnames:
1286 f = self.csv_name_to_function[fieldname]
1287 result.append(f(self))
1290 #def get_dump_row(self):
1295 # return txt.replace('\0','').replace('@', '').strip()
1297 # result.append(self.strmmsi)
1298 # result.append(self.get_flag().encode('utf-8'))
1299 # result.append(self.get_name())
1300 # result.append(str(self.imo))
1301 # result.append(_clean(self.callsign))
1302 # result.append(str(self.type) + '-' + SHIP_TYPES.get(self.type, 'unknown'))
1303 # d = self.dim_bow + self.dim_stern
1307 # result.append(None)
1308 # d = self.dim_port + self.dim_starboard
1312 # result.append(None)
1313 # result.append(datetime.utcfromtimestamp(self.timestamp_1).strftime('%Y-%m-%dT%H:%M:%SZ'))
1314 # result.append(STATUS_CODES.get(self.status, 'unknown'))
1315 # if self.sog != AIS_SOG_NOT_AVAILABLE:
1316 # result.append(str(self.sog/AIS_SOG_SCALE))
1318 # result.append(None)
1319 # if self.latitude != AIS_LAT_NOT_AVAILABLE:
1320 # result.append(str(self.latitude/AIS_LATLON_SCALE))
1322 # result.append(None)
1323 # if self.longitude != AIS_LON_NOT_AVAILABLE:
1324 # result.append(str(self.longitude/AIS_LATLON_SCALE))
1326 # result.append(None)
1327 # if self.cog != AIS_COG_NOT_AVAILABLE:
1328 # result.append(str(self.cog/10.))
1330 # result.append(None)
1331 # if self.heading != AIS_NO_HEADING:
1332 # result.append(str(self.heading))
1334 # result.append(None)
1335 # result.append(self.get_destination(''))
1336 # result.append(self.get_eta_str(''))
1337 # result.append(self.draught)
1338 # result.append(self.source_5)
1342 class BankNmea1(list):
1344 That class handle a .nmea1 archive file
1346 def __init__(self, strmmsi, dt):
1348 self.strmmsi = strmmsi
1349 if isinstance(dt, date):
1350 dt = dt.strftime('%Y%m%d')
1353 def get_filename(self):
1354 return os.path.join(DBPATH, 'bydate', self.date, _hash3_pathfilename(self.strmmsi+'.nmea1'))
1356 def __load_from_file(self, file):
1358 Adds all record from opened file in this bank
1359 File must be locked before call
1362 record = file.read(AIVDM_RECORD123_LENGTH)
1365 self.append(Nmea1.new_from_record(record))
1367 def _write_in_file(self, file):
1369 Write all records from that bank in opened file
1370 File must be locked before call
1371 File should be truncated after call
1373 for nmea1 in list.__iter__(self): # self.__iter__ reload the bank, we don't want that
1374 file.write(nmea1.to_record())
1378 file = open(self.get_filename(), 'rb')
1379 lockf(file, LOCK_SH)
1380 except IOError, ioerr:
1381 if ioerr.errno == 2: # No file
1384 self.__load_from_file(file)
1389 Each call reload the file
1392 self.sort_by_date_reverse()
1393 return list.__iter__(self)
1395 def packday(self, remove_manual_input=False, remove_source_name=None):
1396 #print "MMSI", strmmsi
1398 filename = self.get_filename()
1400 file = open(filename, 'r+b') # read/write binary
1401 except IOError, ioerr:
1402 if ioerr.errno != 2: # No file
1404 return self # no data
1405 lockf(file, LOCK_EX)
1406 self.__load_from_file(file)
1409 file_has_changed = False
1410 file_must_be_unlinked = False
1412 logging.debug('PACKING...')
1413 file_has_changed = self.remove_duplicate_timestamp() or file_has_changed
1415 if remove_manual_input:
1416 logging.debug('REMOVING MANUAL INPUT...')
1417 file_has_changed = self.remove_manual_input() or file_has_changed
1419 if remove_source_name:
1420 logging.debug('REMOVING SOURCES STARTING BY %s', remove_source_name)
1421 file_has_changed = self.remove_by_source(source_name_start=remove_source_name) or file_has_changed
1423 if file_has_changed:
1424 logging.debug('SAVING CHANGES')
1426 self._write_in_file(file)
1428 if file.tell() == 0:
1429 file_must_be_unlinked = True
1433 if file_must_be_unlinked:
1434 # FIXME we release the lock before unlinking
1435 # another process might encounter an empty file (not handled)
1436 logging.warning('file was truncated to size 0. unlinking')
1437 os.unlink(filename) # we have the lock (!)
1439 def dump_to_stdout(self):
1441 Print contents to stdout
1444 nmea1.dump_to_stdout()
1446 def sort_by_date(self):
1447 self.sort(lambda n1, n2: n1.timestamp_1 - n2.timestamp_1)
1449 def sort_by_date_reverse(self):
1450 self.sort(lambda n1, n2: n2.timestamp_1 - n1.timestamp_1)
1452 def remove_duplicate_timestamp(self):
1453 file_has_changed = False
1455 return file_has_changed
1456 last_timestamp = self[0].timestamp_1
1458 while i < len(self):
1459 if self[i].timestamp_1 == last_timestamp:
1461 file_has_changed = True
1463 last_timestamp = self[i].timestamp_1
1465 return file_has_changed
1467 def remove_manual_input(self):
1468 file_has_changed = False
1470 while i < len(self):
1471 if self[i].source_1[:2] == 'MI':
1473 file_has_changed = True
1476 return file_has_changed
1478 def remove_by_source(self, source_name_start):
1479 file_has_changed = False
1481 while i < len(self):
1482 #logging.debug('Testing %s ...', self[i].source_1)
1483 if self[i].source_1.startswith(source_name_start):
1484 #logging.debug('Deleting ...')
1486 file_has_changed = True
1488 #logging.debug('Keeping ...')
1490 return file_has_changed
1494 Yields all nmea1 packets between two given datetimes
1495 in REVERSE order (recent information first)
1497 def __init__(self, strmmsi, datetime_end, datetime_begin=None, max_count=0):
1498 self.strmmsi = strmmsi
1499 assert datetime_end is not None
1500 self.datetime_end = datetime_end
1501 self.datetime_begin = datetime_begin or DB_STARTDATE
1502 self.max_count = max_count
1505 dt_end = self.datetime_end
1506 d_end = dt_end.date()
1507 ts_end = datetime_to_timestamp(dt_end)
1508 if self.datetime_begin:
1509 dt_begin = self.datetime_begin
1510 d_begin = dt_begin.date()
1511 ts_begin = datetime_to_timestamp(dt_begin)
1520 if d_begin is not None and d < d_begin:
1522 bank = BankNmea1(self.strmmsi, d)
1524 if ts_begin is not None and nmea1.timestamp_1 < ts_begin:
1526 if nmea1.timestamp_1 > ts_end:
1532 if self.max_count and count >= self.max_count:
1537 class BankNmea5(list):
1539 That class handle a .nmea5 archive file
1541 def __init__(self, strmmsi, dt):
1543 self.strmmsi = strmmsi
1544 if isinstance(dt, date):
1546 dt = dt.strftime('%Y%m%d')
1548 logging.critical('dt=%s', dt)
1552 def get_filename(self):
1553 return os.path.join(DBPATH, 'bydate', self.date, _hash3_pathfilename(self.strmmsi+'.nmea5'))
1555 def __load_from_file(self, file):
1557 Adds all record from opened file in this bank
1558 File must be locked before call
1561 record = file.read(AIVDM_RECORD5_LENGTH)
1564 self.append(Nmea5.new_from_record(record))
1566 def _write_in_file(self, file):
1568 Write all records from that bank in opened file
1569 File must be locked before call
1570 File should be truncated after call
1573 file.write(nmea5.to_record())
1577 file = open(self.get_filename(), 'rb')
1578 lockf(file, LOCK_SH)
1579 except IOError, ioerr:
1580 if ioerr.errno == 2: # No file
1583 self.__load_from_file(file)
1588 Each call reload the file
1591 self.sort_by_date_reverse()
1592 return list.__iter__(self)
1594 def sort_by_date(self):
1595 self.sort(lambda n1, n2: n1.timestamp_5 - n2.timestamp_5)
1597 def sort_by_date_reverse(self):
1598 self.sort(lambda n1, n2: n2.timestamp_5 - n1.timestamp_5)
1602 Yields all nmea5 packets between two given datetimes
1603 in REVERSE order (recent information first)
1605 def __init__(self, strmmsi, datetime_end, datetime_begin=None, max_count=0):
1606 self.strmmsi = strmmsi
1607 assert datetime_end is not None
1608 self.datetime_end = datetime_end
1609 self.datetime_begin = datetime_begin or DB_STARTDATE
1610 self.max_count = max_count
1613 dt_end = self.datetime_end
1614 d_end = dt_end.date()
1615 ts_end = datetime_to_timestamp(dt_end)
1616 if self.datetime_begin:
1617 dt_begin = self.datetime_begin
1618 d_begin = dt_begin.date()
1619 ts_begin = datetime_to_timestamp(dt_begin)
1628 if d_begin is not None and d < d_begin:
1630 bank = BankNmea5(self.strmmsi, d)
1632 if ts_begin is not None and nmea1.timestamp_5 < ts_begin:
1634 if nmea1.timestamp_5 > ts_end:
1640 if self.max_count and count >= self.max_count:
1647 Yields nmea packets matching criteria.
1649 def __init__(self, strmmsi, datetime_end, datetime_begin=None, filters=None, granularity=1, max_count=None):
1650 if granularity <= 0:
1651 logging.warning('Granularity=%d generates duplicate entries', granularity)
1652 self.strmmsi = strmmsi
1653 assert datetime_end is not None
1654 self.datetime_end = datetime_end
1655 self.datetime_begin = datetime_begin or DB_STARTDATE
1656 self.filters = filters or []
1657 self.granularity = granularity
1658 self.max_count = max_count
1661 nmea = Nmea(self.strmmsi)
1662 if self.datetime_begin:
1663 nmea5_datetime_begin = self.datetime_begin - timedelta(30) # go back up to 30 days to get a good nmea5 packet
1665 nmea5_datetime_begin = None
1666 nmea5_iterator = Nmea5Feeder(self.strmmsi, self.datetime_end, nmea5_datetime_begin).__iter__()
1667 nmea5 = Nmea5(self.strmmsi, sys.maxint)
1670 lasttimestamp = sys.maxint
1671 for nmea1 in Nmea1Feeder(self.strmmsi, self.datetime_end, self.datetime_begin):
1672 Nmea1.from_values(nmea, *nmea1.to_values())
1674 # try to get an nmea5 paket older
1675 nmea5_updated = False
1676 while nmea5 is not None and nmea5.timestamp_5 > nmea1.timestamp_1:
1678 nmea5 = nmea5_iterator.next()
1679 nmea5_updated = True
1680 except StopIteration:
1683 if nmea5_updated and nmea5 is not None:
1684 Nmea5.merge_from_values(nmea, *nmea5.to_values())
1686 filtered_out = False
1687 for is_ok in self.filters:
1694 if nmea.timestamp_1 <= lasttimestamp - self.granularity:
1697 if self.max_count and count >= self.max_count:
1699 lasttimestamp = nmea.timestamp_1
1702 def nice_timedelta_str(delta):
1704 disprank = None # first item type displayed
1706 strdelta += str(delta.days)
1708 strdelta += ' days '
1712 delta_s = delta.seconds
1713 delta_m = delta_s // 60
1714 delta_s -= delta_m * 60
1715 delta_h = delta_m // 60
1716 delta_m -= delta_h * 60
1719 strdelta += str(delta_h)
1721 strdelta += ' hours '
1723 strdelta += ' hour '
1724 if disprank is None:
1726 if delta_m and (disprank is None or disprank >= 1):
1727 strdelta += str(delta_m)
1729 strdelta += ' minutes '
1731 strdelta += ' minute '
1732 if disprank is None:
1734 if delta_s and (disprank is None or disprank >= 2):
1735 strdelta += str(delta_s)
1737 strdelta += ' seconds '
1739 strdelta += ' second '
1740 if disprank is None:
1743 strdelta = 'less than a second '
1746 def all_mmsi_generator():
1748 Returns an array of all known strmmsi.
1750 for dirname, dirs, fnames in os.walk(os.path.join(DBPATH, 'last')):
1751 for fname in fnames:
1752 if fname[-6:] == '.nmea1':
1756 def load_fleet_to_uset(fleetid):
1758 Loads a fleet by id.
1759 Returns an array of strmmsi.
1762 sqlexec(u"SELECT mmsi FROM fleet_vessel WHERE fleet_id=" + unicode(fleetid))
1763 cursor = get_common_cursor()
1765 row = cursor.fetchone()
1769 result.append(mmsi_to_strmmsi(mmsi))
1770 logging.debug('fleet=%s', result)
1774 def fleetname_to_fleetid(fleetname):
1775 sqlexec(u"SELECT id FROM fleet WHERE name=%(fleetname)s", {'fleetname': fleetname})
1776 cursor = get_common_cursor()
1777 row = cursor.fetchone()
1781 def filter_area(nmea, area):
1783 Returns false if position is out of area.
1785 if nmea.latitude == AIS_LAT_NOT_AVAILABLE or nmea.longitude == AIS_LON_NOT_AVAILABLE:
1787 if not area.contains((nmea.latitude/AIS_LATLON_SCALE, nmea.longitude/AIS_LATLON_SCALE)):
1791 def filter_close_to(nmea, lat, lon, miles=1.0):
1793 Returns true if position is closer than miles from (lat, lon)
1795 return dist3_xyz(latlon_to_xyz_deg(lat, lon), latlon_to_xyz_ais(nmea.latitude, nmea.longitude)) <= miles
1798 def filter_far_from(nmea, lat, lon, miles=1.0):
1800 Returns true if position is farther than miles from (lat, lon)
1802 return dist3_xyz(latlon_to_xyz_deg(lat, lon), latlon_to_xyz_ais(nmea.latitude, nmea.longitude)) >= miles
1805 def filter_sog_le(nmea, max_knts):
1807 Returns true if speed over ground is less than max_knts
1809 return nmea.sog/AIS_SOG_SCALE <= max_knts
1812 def filter_sog_ge(nmea, min_knts):
1814 Returns true if speed over ground is less than min_knts
1816 return nmea.sog/AIS_SOG_SCALE >= min_knts
1819 def filter_knownposition(nmea):
1821 Returns false if position is not fully known
1823 # we are filtering out latitude=0 and longitude=0, that is not supposed to be necessary...
1824 return nmea.latitude != AIS_LAT_NOT_AVAILABLE and nmea.longitude != AIS_LON_NOT_AVAILABLE and nmea.latitude != 0 and nmea.longitude != 0
1827 _filter_positioncheck_last_mmsi = None
1828 def filter_speedcheck(nmea, max_mps):
1830 mps is miles per seconds
1832 global _filter_positioncheck_last_mmsi
1833 global _filter_positioncheck_last_time
1834 global _filter_positioncheck_last_time_failed
1835 global _filter_positioncheck_last_lat
1836 global _filter_positioncheck_last_lon
1837 global _filter_positioncheck_error_count
1838 if nmea.strmmsi != _filter_positioncheck_last_mmsi:
1839 _filter_positioncheck_last_time = None
1840 _filter_positioncheck_last_mmsi = nmea.strmmsi
1841 _filter_positioncheck_error_count = 0
1842 if _filter_positioncheck_last_time is not None:
1843 seconds = _filter_positioncheck_last_time - nmea.timestamp_1
1844 distance = dist3_latlong_ais((_filter_positioncheck_last_lat, _filter_positioncheck_last_lon), (nmea.latitude, nmea.longitude))
1846 speed = distance/seconds
1848 if _filter_positioncheck_error_count < 10:
1849 logging.debug("Ignoring point: distance = %s, time = %s, speed = %s kt, source = %s", distance, seconds, distance/seconds*3600, repr(nmea.source_1))
1850 if _filter_positioncheck_error_count == 0 or _filter_positioncheck_last_time_failed != nmea.timestamp_1:
1851 _filter_positioncheck_error_count += 1
1852 _filter_positioncheck_last_time_failed = nmea.timestamp_1
1855 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))
1856 _filter_positioncheck_error_count = 0
1857 _filter_positioncheck_last_time = nmea.timestamp_1
1858 _filter_positioncheck_last_lat = nmea.latitude
1859 _filter_positioncheck_last_lon = nmea.longitude
1865 Perform various operation on the database
1866 For usage, see "ais --help"
1868 from optparse import OptionParser, OptionGroup
1871 parser = OptionParser(usage='%prog [options] { mmsi | @fleetname | ^fleetid }+ | all')
1873 parser.add_option('-d', '--debug',
1874 action='store_true', dest='debug', default=False,
1877 parser.add_option('-e', '--end',
1878 action='store', dest='sdt_end', metavar="'YYYYMMDD HHMMSS'",
1879 help='End data processing on that GMT date time.'
1881 ' If a date is provided without time, time defaults to 235959.')
1882 parser.add_option('-s', '--start',
1883 action='store', dest='sdt_start', metavar="'YYYYMMDD HHMMSS'",
1884 help='Start data processing on that date.'
1885 ' Using that option enables multiple output of the same boat.'
1886 ' Disabled by default.'
1887 ' If a date is provided without time, time default to 000000.'
1888 ' If other options enable multiple output, default to 1 day before'
1889 ' --end date/time.')
1890 parser.add_option('--duration',
1891 action='store', dest='sdt_duration', metavar="DURATION",
1892 help='Duration of reference period.'
1893 ' Last character may be S for seconds, M(inutes), D(ays), W(eeks)'
1894 ' Default is seconds.'
1895 ' This is the time length bewteen --start and --end above.'
1896 ' If you want multiple output of the same boat, you may use '
1897 ' --start, --end or --duration, 2 of them, but not 3 of them.')
1898 parser.add_option('-g', '--granularity',
1899 action='store', type='int', dest='granularity', metavar='SECONDS',
1900 help='Dump only one position every granularity seconds.'
1901 ' Using that option enables multiple output of the same boat.'
1902 ' If other options enable multiple output, defaults to 600'
1904 parser.add_option('--max',
1905 action='store', type='int', dest='max_count', metavar='NUMBER',
1906 help='Dump a maximum of NUMBER positions every granularity seconds.'
1907 'Using that option enables multiple output of the same boat.')
1909 parser.add_option('--filter-knownposition',
1910 action='store_true', dest='filter_knownposition', default=False,
1911 help="Eliminate unknown positions from results.")
1913 parser.add_option('--filter-speedcheck',
1914 action='store', type='int', dest='speedcheck', default=200, metavar='KNOTS',
1915 help='Eliminate erroneaous positions from results,'
1916 ' based on impossible speed.')
1918 parser.add_option('--filter-type',
1919 action='append', type='int', dest='type_list', metavar='TYPE',
1920 help="process a specific ship type.")
1921 parser.add_option('--help-types',
1922 action='store_true', dest='help_types', default=False,
1923 help="display list of available types")
1925 parser.add_option('--filter-area',
1926 action='store', type='str', dest='area_file', metavar="FILE.KML",
1927 help="only process a specific area as defined in a kml polygon file.")
1928 parser.add_option('--filter-farfrom',
1929 action='store', dest='far_from', nargs=3, metavar='LAT LONG MILES',
1930 help="only show ships farther than MILES miles from LAT,LONG")
1931 parser.add_option('--filter-closeto',
1932 action='store', dest='close_to', nargs=3, metavar='LAT LONG MILES',
1933 help="only show ships closer than MILES miles from LAT,LONG")
1934 parser.add_option('--filter-sog-le',
1935 action='store', dest='sog_le', metavar='KNOTS',
1936 help='only show ships when speed over ground is less or equal than KNOTS.')
1937 parser.add_option('--filter-sog-ge',
1938 action='store', dest='sog_ge', metavar='KNOTS',
1939 help='only show ships when speed over ground is greater or equal than KNOTS.')
1941 parser.add_option('--filter-destination',
1942 action='store', type='str', dest='filter_destination', metavar="DESTINATION",
1943 help="Only print ships with that destination.")
1945 parser.add_option('--no-headers',
1946 action='store_false', dest='csv_headers', default=True,
1947 help="skip CSV headers")
1949 parser.add_option('--csv-fields',
1950 action='store', type='str', dest='csv_fields',
1951 default='mmsi,flag,name,imo,callsign,type,length,width,datetime,status,sog,latitude,longitude,cog,heading,destination,eta,draught',
1952 help='Which fields should be extracted for csv output. Default=%default')
1955 expert_group = OptionGroup(parser, "Expert Options",
1956 "You normaly don't need any of these")
1958 expert_group.add_option('--db',
1959 action='store', dest='db', default=DBPATH,
1960 help="path to filesystem database. Default=%default")
1962 expert_group.add_option('--debug-sql',
1963 action='store_true', dest='debug_sql', default=False,
1964 help="print all sql queries to stdout before running them")
1966 expert_group.add_option('--action',
1967 choices=('dump', 'removemanual', 'removebysource', 'mmsidump', 'nirgaldebug', 'fixdestination'), default='dump',
1968 help='Possible values are:\n'
1969 'dump: dump values in csv format. This is the default.\n'
1970 'removemanual: Delete Manual Input entries from the database.\n'
1971 'mmsidump: Dump mmsi')
1972 parser.add_option_group(expert_group)
1974 (options, args) = parser.parse_args()
1977 if options.help_types:
1978 print "Known ship types:"
1979 keys = SHIP_TYPES.keys()
1982 print k, SHIP_TYPES[k]
1988 loglevel = logging.DEBUG
1990 loglevel = logging.INFO
1991 logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
1993 if options.debug_sql:
2001 print >> sys.stderr, "No ship to process"
2004 target_mmsi_iterator = []
2009 elif arg.startswith('@'):
2010 target_mmsi_iterator += load_fleet_to_uset(fleetname_to_fleetid(arg[1:]))
2011 elif arg.startswith('^'):
2012 target_mmsi_iterator += load_fleet_to_uset(int(arg[1:]))
2014 target_mmsi_iterator.append(arg)
2016 if target_mmsi_iterator:
2017 logging.warning('Selecting all ships, ignoring other arguments')
2018 target_mmsi_iterator = all_mmsi_generator()
2024 if options.sdt_start:
2025 # remove non digit characters
2026 options.sdt_start = "".join([ c for c in options.sdt_start if c.isdigit()])
2027 if len(options.sdt_start)==14:
2028 options.sdt_start = datetime.strptime(options.sdt_start, '%Y%m%d%H%M%S')
2029 elif len(options.sdt_start)==8:
2030 options.sdt_start = datetime.strptime(options.sdt_start, '%Y%m%d')
2032 print >> sys.stderr, "Invalid format for --start option"
2036 # remove non digit characters
2037 options.sdt_end = "".join([ c for c in options.sdt_end if c.isdigit()])
2038 if len(options.sdt_end)==14:
2039 options.sdt_end = datetime.strptime(options.sdt_end, '%Y%m%d%H%M%S')
2040 elif len(options.sdt_end)==8:
2041 options.sdt_end = datetime.strptime(options.sdt_end, '%Y%m%d')
2042 options.sdt_end = datetime.combine(options.sdt_end.date(), time(23, 59, 59))
2044 print >> sys.stderr, "Invalid format for --end option"
2047 if options.sdt_duration:
2049 options.sdt_duration = options.sdt_duration.replace(' ', '')
2051 options.sdt_duration = options.sdt_duration.upper()
2052 if options.sdt_duration[-1] == 'S':
2053 options.sdt_duration = options.sdt_duration[:-1]
2055 elif options.sdt_duration[-1] == 'M':
2056 options.sdt_duration = options.sdt_duration[:-1]
2058 elif options.sdt_duration[-1] == 'H':
2059 options.sdt_duration = options.sdt_duration[:-1]
2060 duration_unit = 60*60
2061 elif options.sdt_duration[-1] == 'D':
2062 options.sdt_duration = options.sdt_duration[:-1]
2063 duration_unit = 24*60*60
2064 elif options.sdt_duration[-1] == 'W':
2065 options.sdt_duration = options.sdt_duration[:-1]
2066 duration_unit = 7*24*60*60
2070 options.sdt_duration = long(options.sdt_duration)
2072 print >> sys.stderr, "Can't parse duration"
2074 options.sdt_duration = timedelta(0, options.sdt_duration * duration_unit)
2076 if options.sdt_start or options.sdt_duration or options.granularity is not None or options.max_count:
2077 # Time period is enabled (note that date_end only defaults to one day archives ending then)
2078 if not options.sdt_start and not options.sdt_end and not options.sdt_duration:
2079 options.sdt_duration = timedelta(1) # One day
2080 # continue without else
2081 if not options.sdt_start and not options.sdt_end and options.sdt_duration:
2082 dt_end = datetime.utcnow()
2083 dt_start = dt_end - options.sdt_duration
2084 #elif not options.sdt_start and options.sdt_end and not options.sdt_duration:
2086 elif not options.sdt_start and options.sdt_end and options.sdt_duration:
2087 dt_end = options.sdt_end
2088 dt_start = dt_end - options.sdt_duration
2089 elif options.sdt_start and not options.sdt_end and not options.sdt_duration:
2090 dt_start = options.sdt_start
2091 dt_end = datetime.utcnow()
2092 elif options.sdt_start and not options.sdt_end and options.sdt_duration:
2093 dt_start = options.sdt_start
2094 dt_end = dt_start + options.sdt_duration
2095 elif options.sdt_start and options.sdt_end and not options.sdt_duration:
2096 dt_start = options.sdt_start
2097 dt_end = options.sdt_end
2099 assert options.sdt_start and options.sdt_end and options.sdt_duration, 'Internal error'
2100 print >> sys.stderr, "You can't have all 3 --start --end and --duration"
2102 if options.granularity is None:
2103 options.granularity = 600
2105 # Only get one position
2108 dt_end = options.sdt_end
2110 dt_end = datetime.utcnow()
2111 options.max_count = 1
2112 if options.granularity is None:
2113 options.granularity = 600
2115 logging.debug('--start is %s', dt_start)
2116 logging.debug('--end is %s', dt_end)
2124 if options.filter_knownposition:
2125 filters.append(filter_knownposition)
2127 if options.speedcheck != 0:
2128 maxmps = options.speedcheck / 3600. # from knots to NM per seconds
2129 filters.append(lambda nmea: filter_speedcheck(nmea, maxmps))
2131 if options.area_file:
2132 area = load_area_from_kml_polygon(options.area_file)
2133 filters.append(lambda nmea: filter_area(nmea, area))
2135 if options.close_to:
2137 lat = clean_latitude(unicode(options.close_to[0], 'utf-8'))
2138 lon = clean_longitude(unicode(options.close_to[1], 'utf-8'))
2139 except LatLonFormatError as err:
2140 print >> sys.stderr, err.args
2142 miles = float(options.close_to[2])
2143 filters.append(lambda nmea: filter_close_to(nmea, lat, lon, miles))
2145 if options.far_from:
2147 lat = clean_latitude(unicode(options.far_from[0], 'utf-8'))
2148 lon = clean_longitude(unicode(options.far_from[1], 'utf-8'))
2149 except LatLonFormatError as err:
2150 print >> sys.stderr, err.args
2152 miles = float(options.far_from[2])
2153 filters.append(lambda nmea: filter_far_from(nmea, lat, lon, miles))
2156 filters.append(lambda nmea: filter_sog_le(nmea, float(options.sog_le)))
2158 filters.append(lambda nmea: filter_sog_ge(nmea, float(options.sog_ge)))
2160 if options.type_list:
2161 def filter_type(nmea):
2162 return nmea.type in options.type_list
2163 filters.append(filter_type)
2165 if options.filter_destination:
2166 filters.append(lambda nmea: nmea.destination.startswith(options.filter_destination))
2172 if options.action == 'dump':
2173 fields = options.csv_fields.split(',')
2174 output = csv.writer(sys.stdout)
2175 if options.csv_headers:
2176 output.writerow(fields)
2177 for mmsi in target_mmsi_iterator:
2178 logging.debug('Considering %s', repr(mmsi))
2179 assert dt_end is not None
2180 for nmea in NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count):
2181 output.writerow(nmea.get_dump_row(fields))
2183 elif options.action == 'removemanual':
2185 print >> sys.stderr, "removemanual action doesn't support filters"
2188 # TODO: dates = range dt_start, dt_end
2189 dt = dt_start.date()
2190 while dt < dt_end.date():
2191 logging.info("Processing date %s", dt)
2192 for mmsi in target_mmsi_iterator:
2193 BankNmea1(mmsi, dt).packday(remove_manual_input=True)
2194 dt = dt + timedelta(1)
2196 elif options.action == 'removebysource':
2198 print >> sys.stderr, "removebysource action doesn't support filters"
2201 # TODO: dates = range dt_start, dt_end
2202 dt = dt_start.date()
2203 while dt <= dt_end.date():
2204 logging.info("Processing date %s", dt)
2205 for mmsi in target_mmsi_iterator:
2206 if BankNmea1(mmsi, dt).packday(remove_source_name='MT'):
2207 logging.info('File was modified. mmsi=%s dt=%s', mmsi, dt)
2208 dt = dt + timedelta(1)
2210 elif options.action == 'mmsidump':
2211 for strmmsi in target_mmsi_iterator :
2214 elif options.action == 'fixdestination':
2215 for mmsi in target_mmsi_iterator:
2216 for nmea in NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count):
2217 destination = nmea.destination.rstrip(' @\0')
2219 sqlexec(u'UPDATE vessel SET destination = %(destination)s WHERE mmsi=%(mmsi)s AND destination IS NULL', {'mmsi':strmmsi_to_mmsi(mmsi), 'destination':destination})
2220 logging.info('%s -> %s', mmsi, repr(destination))
2222 break # go to next mmsi
2225 if __name__ == '__main__':