Fixed Ivory Coast name
[ais.git] / bin / common.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 __all__ = [
5     'DB_STARTDATE', 'DBPATH',
6     'COUNTRIES_MID', 'STATUS_CODES', 'SHIP_TYPES',
7     'AIS_STATUS_NOT_AVAILABLE',
8     'AIS_ROT_HARD_LEFT', 'AIS_ROT_HARD_RIGHT', 'AIS_ROT_NOT_AVAILABLE',
9     'AIS_LATLON_SCALE', 'AIS_LON_NOT_AVAILABLE', 'AIS_LAT_NOT_AVAILABLE',
10     'AIS_COG_SCALE', 'AIS_COG_NOT_AVAILABLE',
11     'AIS_NO_HEADING',
12     'AIS_SOG_SCALE', 'AIS_SOG_NOT_AVAILABLE', 'AIS_SOG_FAST_MOVER', 'AIS_SOG_MAX_SPEED',
13     #'_hash3_pathfilename',
14     'db_bydate_addrecord',
15     'db_lastinfo_setrecord_ifnewer',
16     'sql_add_nmea5',
17     #'aivdm_record123_format',
18     #'aivdm_record123_length',
19     #'aivdm_record5_format',
20     #'aivdm_record5_length',
21     'add_nmea1',
22     'add_nmea5_full',
23     'add_nmea5_partial',
24     'strmmsi_to_mmsi',
25     'mmsi_to_strmmsi',
26     'Nmea1',
27     'Nmea5',
28     'Nmea',
29     'BankNmea1',
30     'Nmea1Feeder',
31     'BankNmea5',
32     'Nmea5Feeder',
33     'NmeaFeeder',
34     'all_mmsi_generator',
35     'load_fleet_to_uset',
36     'filter_area',
37     'filter_knownposition',
38     'filter_speedcheck',
39     ]
40             
41
42 import sys
43 import os
44 import struct
45 import logging
46 from datetime import datetime, timedelta, date, time
47 from fcntl import lockf, LOCK_EX, LOCK_UN, LOCK_SH
48 import csv
49
50 from ais.ntools import *
51 from ais.db import *
52 from ais.area import load_area_from_kml_polygon
53 from ais.earth3d import dist3_latlong_ais
54
55 DB_STARTDATE = datetime(2008, 6, 1)
56
57 # This is the location of the filesystem database
58 DBPATH = '/var/lib/ais/db'
59
60 # see make-countries.py
61 COUNTRIES_MID = {
62     201: u'Albania',
63     202: u'Andorra',
64     203: u'Austria',
65     204: u'Azores',
66     205: u'Belgium',
67     206: u'Belarus',
68     207: u'Bulgaria',
69     208: u'Vatican City State',
70     209: u'Cyprus',
71     210: u'Cyprus',
72     211: u'Germany',
73     212: u'Cyprus',
74     213: u'Georgia',
75     214: u'Moldova',
76     215: u'Malta',
77     216: u'Armenia',
78     218: u'Germany',
79     219: u'Denmark',
80     220: u'Denmark',
81     224: u'Spain',
82     225: u'Spain',
83     226: u'France',
84     227: u'France',
85     228: u'France',
86     230: u'Finland',
87     231: u'Faroe Islands',
88     232: u'United Kingdom',
89     233: u'United Kingdom',
90     234: u'United Kingdom',
91     235: u'United Kingdom',
92     236: u'Gibraltar',
93     237: u'Greece',
94     238: u'Croatia',
95     239: u'Greece',
96     240: u'Greece',
97     242: u'Morocco',
98     243: u'Hungary',
99     244: u'Netherlands',
100     245: u'Netherlands',
101     246: u'Netherlands',
102     247: u'Italy',
103     248: u'Malta',
104     249: u'Malta',
105     250: u'Ireland',
106     251: u'Iceland',
107     252: u'Liechtenstein',
108     253: u'Luxembourg',
109     254: u'Monaco',
110     255: u'Madeira',
111     256: u'Malta',
112     257: u'Norway',
113     258: u'Norway',
114     259: u'Norway',
115     261: u'Poland',
116     262: u'Montenegro',
117     263: u'Portugal',
118     264: u'Romania',
119     265: u'Sweden',
120     266: u'Sweden',
121     267: u'Slovak Republic',
122     268: u'San Marino',
123     269: u'Switzerland',
124     270: u'Czech Republic',
125     271: u'Turkey',
126     272: u'Ukraine',
127     273: u'Russian Federation',
128     274: u'The Former Yugoslav Republic of Macedonia',
129     275: u'Latvia',
130     276: u'Estonia',
131     277: u'Lithuania',
132     278: u'Slovenia',
133     279: u'Serbia',
134     301: u'Anguilla',
135     303: u'Alaska',
136     304: u'Antigua and Barbuda',
137     305: u'Antigua and Barbuda',
138     306: u'Netherlands Antilles',
139     307: u'Aruba',
140     308: u'Bahamas',
141     309: u'Bahamas',
142     310: u'Bermuda',
143     311: u'Bahamas',
144     312: u'Belize',
145     314: u'Barbados',
146     316: u'Canada',
147     319: u'Cayman Islands',
148     321: u'Costa Rica',
149     323: u'Cuba',
150     325: u'Dominica',
151     327: u'Dominican Republic',
152     329: u'Guadeloupe',
153     330: u'Grenada',
154     331: u'Greenland',
155     332: u'Guatemala',
156     334: u'Honduras',
157     336: u'Haiti',
158     338: u'United States of America',
159     339: u'Jamaica',
160     341: u'Saint Kitts and Nevis',
161     343: u'Saint Lucia',
162     345: u'Mexico',
163     347: u'Martinique',
164     348: u'Montserrat',
165     350: u'Nicaragua',
166     351: u'Panama',
167     352: u'Panama',
168     353: u'Panama',
169     354: u'Panama',
170     355: u'Panama',
171     356: u'Panama',
172     357: u'Panama',
173     358: u'Puerto Rico',
174     359: u'El Salvador',
175     361: u'Saint Pierre and Miquelon',
176     362: u'Trinidad and Tobago',
177     364: u'Turks and Caicos Islands',
178     366: u'United States of America',
179     367: u'United States of America',
180     368: u'United States of America',
181     369: u'United States of America',
182     370: u'Panama',
183     371: u'Panama',
184     372: u'Panama',
185     375: u'Saint Vincent and the Grenadines',
186     376: u'Saint Vincent and the Grenadines',
187     377: u'Saint Vincent and the Grenadines',
188     378: u'British Virgin Islands',
189     379: u'United States Virgin Islands',
190     401: u'Afghanistan',
191     403: u'Saudi Arabia',
192     405: u'Bangladesh',
193     408: u'Bahrain',
194     410: u'Bhutan',
195     412: u'China',
196     413: u'China',
197     416: u'Taiwan',
198     417: u'Sri Lanka',
199     419: u'India',
200     422: u'Iran',
201     423: u'Azerbaijani Republic',
202     425: u'Iraq',
203     428: u'Israel',
204     431: u'Japan',
205     432: u'Japan',
206     434: u'Turkmenistan',
207     436: u'Kazakhstan',
208     437: u'Uzbekistan',
209     438: u'Jordan',
210     440: u'Korea',
211     441: u'Korea',
212     443: u'Palestine',
213     445: u"Democratic People's Republic of Korea",
214     447: u'Kuwait',
215     450: u'Lebanon',
216     451: u'Kyrgyz Republic',
217     453: u'Macao',
218     455: u'Maldives',
219     457: u'Mongolia',
220     459: u'Nepal',
221     461: u'Oman',
222     463: u'Pakistan',
223     466: u'Qatar',
224     468: u'Syrian Arab Republic',
225     470: u'United Arab Emirates',
226     473: u'Yemen',
227     475: u'Yemen',
228     477: u'Hong Kong',
229     478: u'Bosnia and Herzegovina',
230     501: u'Adelie Land',
231     503: u'Australia',
232     506: u'Myanmar',
233     508: u'Brunei Darussalam',
234     510: u'Micronesia',
235     511: u'Palau',
236     512: u'New Zealand',
237     514: u'Cambodia',
238     515: u'Cambodia',
239     516: u'Christmas Island',
240     518: u'Cook Islands',
241     520: u'Fiji',
242     523: u'Cocos',
243     525: u'Indonesia',
244     529: u'Kiribati',
245     531: u"Lao People's Democratic Republic",
246     533: u'Malaysia',
247     536: u'Northern Mariana Islands',
248     538: u'Marshall Islands',
249     540: u'New Caledonia',
250     542: u'Niue',
251     544: u'Nauru',
252     546: u'French Polynesia',
253     548: u'Philippines',
254     553: u'Papua New Guinea',
255     555: u'Pitcairn Island',
256     557: u'Solomon Islands',
257     559: u'American Samoa',
258     561: u'Samoa',
259     563: u'Singapore',
260     564: u'Singapore',
261     565: u'Singapore',
262     567: u'Thailand',
263     570: u'Tonga',
264     572: u'Tuvalu',
265     574: u'Viet Nam',
266     576: u'Vanuatu',
267     578: u'Wallis and Futuna Islands',
268     601: u'South Africa',
269     603: u'Angola',
270     605: u'Algeria',
271     607: u'Saint Paul and Amsterdam Islands',
272     608: u'Ascension Island',
273     609: u'Burundi',
274     610: u'Benin',
275     611: u'Botswana',
276     612: u'Central African Republic',
277     613: u'Cameroon',
278     615: u'Congo',
279     616: u'Comoros',
280     617: u'Cape Verde',
281     618: u'Crozet Archipelago',
282     619: u"Côte d'Ivoire",
283     621: u'Djibouti',
284     622: u'Egypt',
285     624: u'Ethiopia',
286     625: u'Eritrea',
287     626: u'Gabonese Republic',
288     627: u'Ghana',
289     629: u'Gambia',
290     630: u'Guinea-Bissau',
291     631: u'Equatorial Guinea',
292     632: u'Guinea',
293     633: u'Burkina Faso',
294     634: u'Kenya',
295     635: u'Kerguelen Islands',
296     636: u'Liberia',
297     637: u'Liberia',
298     642: u"Socialist People's Libyan Arab Jamahiriya",
299     644: u'Lesotho',
300     645: u'Mauritius',
301     647: u'Madagascar',
302     649: u'Mali',
303     650: u'Mozambique',
304     654: u'Mauritania',
305     655: u'Malawi',
306     656: u'Niger',
307     657: u'Nigeria',
308     659: u'Namibia',
309     660: u'Reunion',
310     661: u'Rwanda',
311     662: u'Sudan',
312     663: u'Senegal',
313     664: u'Seychelles',
314     665: u'Saint Helena',
315     666: u'Somali Democratic Republic',
316     667: u'Sierra Leone',
317     668: u'Sao Tome and Principe',
318     669: u'Swaziland',
319     670: u'Chad',
320     671: u'Togolese Republic',
321     672: u'Tunisia',
322     674: u'Tanzania',
323     675: u'Uganda',
324     676: u'Democratic Republic of the Congo',
325     677: u'Tanzania',
326     678: u'Zambia',
327     679: u'Zimbabwe',
328     701: u'Argentine Republic',
329     710: u'Brazil',
330     720: u'Bolivia',
331     725: u'Chile',
332     730: u'Colombia',
333     735: u'Ecuador',
334     740: u'Falkland Islands',
335     745: u'Guiana',
336     750: u'Guyana',
337     755: u'Paraguay',
338     760: u'Peru',
339     765: u'Suriname',
340     770: u'Uruguay',
341     775: u'Venezuela',
342 }
343
344 STATUS_CODES = {
345      0:  'Under way using engine',
346      1:  'At anchor',
347      2:  'Not under command',
348      3:  'Restricted manoeuverability',
349      4:  'Constrained by her draught',
350      5:  'Moored',
351      6:  'Aground',
352      7:  'Engaged in Fishing',
353      8:  'Under way sailing',
354      9:  '9 - Reserved for future amendment of Navigational Status for HSC',
355     10:  '10 - Reserved for future amendment of Navigational Status for WIG',
356     11:  '11 - Reserved for future use',
357     12:  '12 - Reserved for future use',
358     13:  '13 - Reserved for future use',
359     14:  '14 - Reserved for future use', # Land stations
360     15:  'Not defined', # default
361 }
362
363 SHIP_TYPES = {
364      0: 'Not available (default)',
365      1: 'Reserved for future use',
366      2: 'Reserved for future use',
367      3: 'Reserved for future use',
368      4: 'Reserved for future use',
369      5: 'Reserved for future use',
370      6: 'Reserved for future use',
371      7: 'Reserved for future use',
372      8: 'Reserved for future use',
373      9: 'Reserved for future use',
374     10: 'Reserved for future use',
375     11: 'Reserved for future use',
376     12: 'Reserved for future use',
377     13: 'Reserved for future use',
378     14: 'Reserved for future use',
379     15: 'Reserved for future use',
380     16: 'Reserved for future use',
381     17: 'Reserved for future use',
382     18: 'Reserved for future use',
383     19: 'Reserved for future use',
384     20: 'Wing in ground (WIG), all ships of this type',
385     21: 'Wing in ground (WIG), Hazardous category A',
386     22: 'Wing in ground (WIG), Hazardous category B',
387     23: 'Wing in ground (WIG), Hazardous category C',
388     24: 'Wing in ground (WIG), Hazardous category D',
389     25: 'Wing in ground (WIG), Reserved for future use',
390     26: 'Wing in ground (WIG), Reserved for future use',
391     27: 'Wing in ground (WIG), Reserved for future use',
392     28: 'Wing in ground (WIG), Reserved for future use',
393     29: 'Wing in ground (WIG), Reserved for future use',
394     30: 'Fishing',
395     31: 'Towing',
396     32: 'Towing: length exceeds 200m or breadth exceeds 25m',
397     33: 'Dredging or underwater ops',
398     34: 'Diving ops',
399     35: 'Military ops',
400     36: 'Sailing',
401     37: 'Pleasure Craft',
402     38: 'Reserved',
403     39: 'Reserved',
404     40: 'High speed craft (HSC), all ships of this type',
405     41: 'High speed craft (HSC), Hazardous category A',
406     42: 'High speed craft (HSC), Hazardous category B',
407     43: 'High speed craft (HSC), Hazardous category C',
408     44: 'High speed craft (HSC), Hazardous category D',
409     45: 'High speed craft (HSC), Reserved for future use',
410     46: 'High speed craft (HSC), Reserved for future use',
411     47: 'High speed craft (HSC), Reserved for future use',
412     48: 'High speed craft (HSC), Reserved for future use',
413     49: 'High speed craft (HSC), No additional information',
414     50: 'Pilot Vessel',
415     51: 'Search and Rescue vessel',
416     52: 'Tug',
417     53: 'Port Tender',
418     54: 'Anti-pollution equipment',
419     55: 'Law Enforcement',
420     56: 'Spare - Local Vessel',
421     57: 'Spare - Local Vessel',
422     58: 'Medical Transport',
423     59: 'Ship according to RR Resolution No. 18',
424     60: 'Passenger, all ships of this type',
425     61: 'Passenger, Hazardous category A',
426     62: 'Passenger, Hazardous category B',
427     63: 'Passenger, Hazardous category C',
428     64: 'Passenger, Hazardous category D',
429     65: 'Passenger, Reserved for future use',
430     66: 'Passenger, Reserved for future use',
431     67: 'Passenger, Reserved for future use',
432     68: 'Passenger, Reserved for future use',
433     69: 'Passenger, No additional information',
434     70: 'Cargo', # 'Cargo, all ships of this type',
435     71: 'Cargo, Hazardous category A',
436     72: 'Cargo, Hazardous category B',
437     73: 'Cargo, Hazardous category C',
438     74: 'Cargo, Hazardous category D',
439     75: 'Cargo', # 'Cargo, Reserved for future use',
440     76: 'Cargo', # 'Cargo, Reserved for future use',
441     77: 'Cargo', # 'Cargo, Reserved for future use',
442     78: 'Cargo', # 'Cargo, Reserved for future use',
443     79: 'Cargo', # 'Cargo, No additional information',
444     80: 'Tanker', # 'Tanker, all ships of this type',
445     81: 'Tanker, Hazardous category A',
446     82: 'Tanker, Hazardous category B',
447     83: 'Tanker, Hazardous category C',
448     84: 'Tanker, Hazardous category D',
449     85: 'Tanker', # 'Tanker, Reserved for future use',
450     86: 'Tanker', # 'Tanker, Reserved for future use',
451     87: 'Tanker', # 'Tanker, Reserved for future use',
452     88: 'Tanker', # 'Tanker, Reserved for future use',
453     89: 'Tanker, No additional information',
454     90: 'Other Type, all ships of this type',
455     91: 'Other Type, Hazardous category A',
456     92: 'Other Type, Hazardous category B',
457     93: 'Other Type, Hazardous category C',
458     94: 'Other Type, Hazardous category D',
459     95: 'Other Type, Reserved for future use',
460     96: 'Other Type, Reserved for future use',
461     97: 'Other Type, Reserved for future use',
462     98: 'Other Type, Reserved for future use',
463     99: 'Other Type, no additional information',
464     100: 'Default Navaid',
465     101: 'Reference point',
466     102: 'RACON',
467     103: 'Offshore Structure',
468     104: 'Spare',
469     105: 'Light, without sectors',
470     106: 'Light, with sectors',
471     107: 'Leading Light Front',
472     108: 'Leading Light Rear',
473     109: 'Beacon, Cardinal N',
474     110: 'Beacon, Cardinal E',
475     111: 'Beacon, Cardinal S',
476     112: 'Beacon, Cardinal W',
477     113: 'Beacon, Port hand',
478     114: 'Beacon, Starboard hand',
479     115: 'Beacon, Preferred Channel port hand',
480     116: 'Beacon, Preferred Channel starboard hand',
481     117: 'Beacon, Isolated danger',
482     118: 'Beacon, Safe water',
483     119: 'Beacon, Special mark',
484     120: 'Cardinal Mark N',
485     121: 'Cardinal Mark E',
486     122: 'Cardinal Mark S',
487     123: 'Cardinal Mark W',
488     124: 'Port hand Mark',
489     125: 'Starboard hand Mark',
490     126: 'Preferred Channel Port hand',
491     127: 'Preferred Channel Starboard hand',
492     128: 'Isolated danger',
493     129: 'Safe Water',
494     130: 'Manned VTS / Special Mark',
495     131: 'Light Vessel / LANBY',
496 }
497
498 AIS_STATUS_NOT_AVAILABLE = 15
499 AIS_ROT_HARD_LEFT = -127
500 AIS_ROT_HARD_RIGHT = 127
501 AIS_ROT_NOT_AVAILABLE = -128 # not like gpsd
502
503 AIS_LATLON_SCALE = 600000.0
504 AIS_LON_NOT_AVAILABLE = 0x6791AC0
505 AIS_LAT_NOT_AVAILABLE = 0x3412140
506 AIS_COG_SCALE = 10.0
507 AIS_COG_NOT_AVAILABLE = 3600
508 AIS_NO_HEADING = 511
509 AIS_SOG_SCALE = 10.0
510 AIS_SOG_NOT_AVAILABLE = 1023
511 AIS_SOG_FAST_MOVER = 1022
512 AIS_SOG_MAX_SPEED = 1021
513
514
515 def _hash3_pathfilename(filename):
516     """
517     Returns a level 3 directory hashed filename on that basis:
518     123456789 -> 1/12/123/123456789
519     """
520     return os.path.join(filename[0], filename[:2], filename[:3], filename)
521
522
523 def db_bydate_addrecord(basefilename, record, timestamp):
524     strdt = datetime.utcfromtimestamp(timestamp).strftime('%Y%m%d')
525     filename = os.path.join(DBPATH, 'bydate', strdt, _hash3_pathfilename(basefilename))
526     f = open_with_mkdirs(filename, 'ab')
527     lockf(f, LOCK_EX)
528     #f.seek(0,2) # go to EOF
529     assert f.tell() % len(record) == 0, 'Invalid length for %s' % filename
530     f.write(record)
531     f.close()
532
533
534 def db_lastinfo_setrecord_ifnewer(basefilename, record, timestamp):
535     '''
536     Overwrite last information if date is newer
537     Input record must be complete
538     '''
539     filename = DBPATH+'/last/'+_hash3_pathfilename(basefilename)
540
541     try:
542         f = open(filename, 'r+b')
543     except IOError, ioerr:
544         if ioerr.errno != 2:
545             raise
546         # File was not found? Ok, create it. FIXME: we should lock something...
547         f = open_with_mkdirs(filename, 'wb')
548         f.write(record)
549         updated = True
550     else:
551         lockf(f, LOCK_EX)
552         assert f.tell() == 0
553         oldrecord = f.read(4)
554         assert len(oldrecord) == 4
555         oldtimestamp = struct.unpack('I', oldrecord)[0]
556         f.seek(0)
557         assert f.tell() == 0
558         if timestamp > oldtimestamp:
559             f.write(record)
560             assert f.tell() == len(record), \
561                 "tell=%s size=%s" % (f.tell(), len(record))
562             updated = True
563         else:
564             updated = False
565     f.close()
566     return updated
567
568
569 def sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
570     dim_bow, dim_stern, dim_port, dim_starboard, \
571     eta_M, eta_D, eta_h, eta_m, draught, destination, source):
572     ''' Don't call directly '''
573     sqlinfo = {}
574     sqlinfo['mmsi'] = strmmsi_to_mmsi(strmmsi)
575     sqlinfo['updated'] = datetime.utcfromtimestamp(timestamp)
576     sqlinfo['imo'] = imo or None
577     sqlinfo['name'] = name or None
578     sqlinfo['callsign'] = callsign or None
579     sqlinfo['type'] = type
580     sqlinfo['destination'] = None
581     if destination:
582         destination = destination.replace('\0', ' ').rstrip(' @\0')
583     sqlinfo['destination'] = destination or None
584     sqlinfo['source'] = source
585     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)
586     if sqlinfo['imo']:
587         sqlexec(u'UPDATE vessel SET imo = %(imo)s WHERE mmsi=%(mmsi)s AND (imo IS NULL OR updated<%(updated)s)', sqlinfo)
588     if sqlinfo['name']:
589         sqlexec(u'UPDATE vessel SET name = %(name)s WHERE mmsi=%(mmsi)s AND (name IS NULL OR updated<%(updated)s)', sqlinfo)
590     if sqlinfo['callsign']:
591         sqlexec(u'UPDATE vessel SET callsign = %(callsign)s WHERE mmsi=%(mmsi)s AND (callsign IS NULL OR updated<%(updated)s)', sqlinfo)
592     if sqlinfo['type']:
593         sqlexec(u'UPDATE vessel SET type = %(type)s WHERE mmsi=%(mmsi)s AND (type IS NULL OR updated<%(updated)s)', sqlinfo)
594     if sqlinfo['destination']:
595         sqlexec(u'UPDATE vessel SET destination = %(destination)s WHERE mmsi=%(mmsi)s AND (destination IS NULL OR updated<%(updated)s)', sqlinfo)
596     sqlexec(u'UPDATE vessel SET (updated, source) = (%(updated)s, %(source)s) WHERE mmsi=%(mmsi)s AND updated<%(updated)s', sqlinfo)
597     dbcommit()
598
599
600
601
602 aivdm_record123_format = 'IBbhiiII4s'
603 aivdm_record123_length = struct.calcsize(aivdm_record123_format)
604 aivdm_record5_format = 'II20s7sBHHBBBBBBH20s4s'
605 aivdm_record5_length = struct.calcsize(aivdm_record5_format)
606
607
608 def add_nmea1(strmmsi, timestamp, status, rot, sog, \
609               latitude, longitude, cog, heading, source):
610     '''
611     Input is raw data, unscaled
612     FIXME: lat & lon are inverted compared to raw aivdm structure
613     '''
614     record = struct.pack(aivdm_record123_format, timestamp, status, rot, sog, latitude, longitude, cog, heading, source)
615     #print repr(record)
616     filename = strmmsi+'.nmea1'
617     db_bydate_addrecord(filename, record, timestamp)
618     # There's no need to be smart: all the information are taken, or none.
619     return db_lastinfo_setrecord_ifnewer(filename, record, timestamp)
620
621
622 def add_nmea5_full(strmmsi, timestamp, imo, name, callsign, type, \
623                    dim_bow, dim_stern, dim_port, dim_starboard, \
624                    eta_M, eta_D, eta_h, eta_m, draught, destination, source):
625     '''
626     Input is raw data, unscaled
627     All fields are set, and can be upgraded if the record is newer
628     FIXME: name & callsign are inverted compared to raw aivdm structure
629     '''
630     record = struct.pack(aivdm_record5_format, timestamp, imo, name, callsign, \
631                          type, dim_bow, dim_stern, dim_port, dim_starboard, \
632                          eta_M, eta_D, eta_h, eta_m, draught, destination, source)
633     #print repr(record)
634     filename = strmmsi+'.nmea5'
635     db_bydate_addrecord(filename, record, timestamp)
636     updated = db_lastinfo_setrecord_ifnewer(filename, record, timestamp)
637     if updated:
638         sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
639                       dim_bow, dim_stern, dim_port, dim_starboard, \
640                       eta_M, eta_D, eta_h, eta_m, draught, destination, source)
641     return updated
642
643 def add_nmea5_partial(strmmsi, timestamp, imo, name, callsign, type, \
644                       dim_bow, dim_stern, dim_port, dim_starboard, \
645                       eta_M, eta_D, eta_h, eta_m, draught, destination, source):
646     '''
647     Input is raw data, unscaled
648     All fields are not set. Only some of them can be upgraded, if they're newer
649     '''
650     record = struct.pack(aivdm_record5_format, \
651                          timestamp, imo, name, callsign, type, \
652                          dim_bow, dim_stern, dim_port, dim_starboard, \
653                          eta_M, eta_D, eta_h, eta_m, draught, destination, \
654                          source)
655     #print repr(record)
656     filename = strmmsi + '.nmea5'
657     db_bydate_addrecord(filename, record, timestamp)
658
659     updated = False
660     filename = os.path.join(DBPATH, 'last', _hash3_pathfilename(filename))
661     try:
662         f = open(filename, 'r+b')
663     except IOError, ioerr:
664         if ioerr.errno != 2:
665             raise
666         # File was not found? Ok, create it. FIXME: we should lock something...
667         f = open_with_mkdirs(filename, 'wb')
668         lockf(f, LOCK_EX)
669         f.write(record)
670         # keep the lock
671         updated = True
672     else:
673         lockf(f, LOCK_EX)
674         oldrecord = f.read(aivdm_record5_length)
675         oldtimestamp, oldimo, oldname, oldcallsign, oldtype, \
676         olddim_bow, olddim_stern, olddim_port, olddim_starboard, \
677         oldeta_M, oldeta_D, oldeta_h, oldeta_m, \
678         olddraught, olddestination, oldsource \
679                   = struct.unpack(aivdm_record5_format, oldrecord)
680         if timestamp > oldtimestamp:
681             # we have incoming recent information
682             if imo == 0:
683                 imo = oldimo
684             if name == '':
685                 name = oldname
686             if callsign == '':
687                 callsign = oldcallsign
688             if type == 0:
689                 type = oldtype
690             if dim_bow == 0:
691                 dim_bow = olddim_bow
692             if dim_stern == 0:
693                 dim_stern = olddim_stern
694             if dim_port == 0:
695                 dim_port = olddim_port
696             if dim_starboard == 0:
697                 dim_starboard = olddim_starboard
698             if eta_M == 0 or eta_D == 0 or eta_h == 24 or eta_m == 60 \
699                           or destination == '':
700                 eta_M = oldeta_M
701                 eta_D = oldeta_D
702                 eta_h = oldeta_h
703                 eta_m = oldeta_m
704                 destination = olddestination
705             if draught == 0:
706                 draught = olddraught
707             record = struct.pack(aivdm_record5_format, \
708                                  timestamp, imo, name, callsign, type, \
709                                  dim_bow, dim_stern, dim_port, dim_starboard, \
710                                  eta_M, eta_D, eta_h, eta_m, draught, \
711                                  destination, source)
712             f.seek(0)
713             f.write(record)
714             updated = True
715         else:
716             # we received an obsolete info, but maybe there are some new things in it
717             if oldimo == 0 and imo != 0:
718                 oldimo = imo
719                 updated = True
720             if oldname == '' and name != '':
721                 oldname = name
722                 updated = True
723             if oldcallsign == '' and callsign != '':
724                 oldcallsign = callsign
725                 updated = True
726             if oldtype == 0 and type != 0:
727                 oldtype = type
728                 updated = True
729             if olddim_bow == 0 and dim_bow != 0:
730                 olddim_bow = dim_bow
731                 updated = True
732             if olddim_stern == 0 and dim_stern != 0:
733                 olddim_stern = dim_stern
734                 updated = True
735             if olddim_port == 0 and dim_port != 0:
736                 olddim_port = dim_port
737                 updated = True
738             if olddim_starboard == 0 and dim_starboard != 0:
739                 olddim_starboard = dim_starboard
740                 updated = True
741             # FIXME
742             if (oldeta_M == 0 or oldeta_D == 0 or olddestination == '') \
743                     and ((eta_M != 0 and eta_D != 0) or destination!=''):
744                 oldeta_M = eta_M
745                 oldeta_D = eta_D
746                 oldeta_h = eta_h
747                 oldeta_m = eta_m
748                 olddestination = destination
749                 updated = True
750             if olddraught == 0 and draught != 0:
751                 olddraught = draught
752                 updated = True
753             if updated:
754                 oldsource = source
755                 record = struct.pack(aivdm_record5_format, \
756                                      oldtimestamp, oldimo, oldname, \
757                                      oldcallsign, oldtype, \
758                                      olddim_bow, olddim_stern, \
759                                      olddim_port, olddim_starboard, \
760                                      oldeta_M, oldeta_D, oldeta_h, oldeta_m, \
761                                      olddraught, olddestination, oldsource)
762             
763                 f.seek(0)
764                 f.write(record)
765     # keep the file locked during SQL updates
766     if updated:
767         sql_add_nmea5(strmmsi, timestamp, imo, name, callsign, type, \
768                       dim_bow, dim_stern, dim_port, dim_starboard, \
769                       eta_M, eta_D, eta_h, eta_m, draught, destination, source)
770     f.close()
771     return updated
772
773
774
775 def strmmsi_to_mmsi(strmmsi):
776     """
777     Convert from str mmsi to sql-int mmsi
778     Special treatment manal input
779     """
780     if strmmsi.isdigit():
781         return int(strmmsi)
782     else:
783         assert strmmsi[3:5] == 'MI'
784         strmmsi = strmmsi[:3]+'00'+strmmsi[5:]
785         return int('-'+strmmsi)
786
787
788 def mmsi_to_strmmsi(mmsi):
789     """
790     Convert from sql-into mmsi to str mmsi
791     Special treatment manal input
792     """
793     if mmsi >= 0:
794         return "%08d" % mmsi
795     strmmsi = "%08d" % -mmsi
796     assert strmmsi[3:5] == '00'
797     strmmsi = strmmsi[:3]+'MI'+strmmsi[5:]
798     return strmmsi
799
800
801 __misources__ = {} # cache of manual source names
802 def _get_mi_sourcename(id):
803     """
804     Get the nice name for sources whose id4 starts with 'MI'
805     """
806     global __misources__
807     if not __misources__:
808         sqlexec(u'SELECT id, name FROM mi_source')
809         while True:
810             row = get_common_cursor().fetchone()
811             if row is None:
812                 break
813             __misources__[row[0]] = row[1]
814     result = __misources__.get(id, None)
815     if result is None:
816         return u"Manual input #%s" % id
817     return result
818
819
820 class Nmea1:
821     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'):
822         self.timestamp_1 = timestamp
823         self.status      = status
824         self.rot         = rot
825         self.sog         = sog
826         self.latitude    = latitude
827         self.longitude   = longitude
828         self.cog         = cog
829         self.heading     = heading
830         self.source_1    = source
831
832     from_values = __init__
833
834     def to_values(self):
835         return self.timestamp_1, self.status, self.rot, self.sog, self.latitude, self.longitude, self.cog, self.heading, self.source_1
836
837     def from_record(self, record):
838         values = struct.unpack(aivdm_record123_format, record)
839         Nmea1.__init__(self, *values)
840
841     @staticmethod
842     def new_from_record(record):
843         values = struct.unpack(aivdm_record123_format, record)
844         return Nmea1(*values)
845
846     def to_record(self):
847         return struct.pack(aivdm_record123_format, *Nmea1.to_values())
848         
849     def from_file(self, file):
850         record = file.read(aivdm_record123_length)
851         Nmea1.from_record(self, record)
852
853     @staticmethod
854     def new_from_file(file):
855         record = file.read(aivdm_record123_length)
856         return Nmea1.new_from_record(record)
857
858     def from_lastinfo(self, strmmsi):
859         filename_nmea1 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea1')
860         try:
861             f = file(filename_nmea1, 'rb')
862         except IOError:
863             logging.debug("file %s doesn't exists" % filename_nmea1)
864             return
865         lockf(f, LOCK_SH)
866         Nmea1.from_file(self, f)
867         f.close()
868
869     @staticmethod
870     def new_from_lastinfo(strmmsi):
871         filename_nmea1 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea1')
872         try:
873             f = file(filename_nmea1, 'rb')
874         except IOError:
875             logging.debug("file %s doesn't exists" % filename_nmea1)
876             return None
877         lockf(f, LOCK_SH)
878         record = f.read(aivdm_record123_length)
879         f.close()
880         return Nmea1.new_from_record(record)
881
882
883     def dump_to_stdout(self):
884         """
885         Prints content to stdout
886         """
887         print datetime.utcfromtimestamp(self.timestamp_1), 
888         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):
889             print repr(i),
890         print
891  
892     @staticmethod
893     def _clean_str(txt):
894         if txt is None:
895             return ''
896         return txt.replace('\0','').replace('@', '').strip()
897
898     def get_status(self, default='Unknown'):
899         return STATUS_CODES.get(self.status, default)
900  
901     def get_sog_str(self, default='Unknown'):
902         if self.sog == AIS_SOG_NOT_AVAILABLE:
903             return default
904         if self.sog == AIS_SOG_FAST_MOVER:
905             return 'over 102.2 kts'
906         return '%.1f kts' % (self.sog/AIS_SOG_SCALE)
907
908     def get_rot_str(self, default='Unknown'):
909         if self.rot == AIS_ROT_NOT_AVAILABLE:
910             return default
911         if self.rot == 0:
912             return 'Not turning'
913         if self.rot < 0:
914             side = 'port'
915         else:
916             side = 'starboard'
917         rot = abs(self.rot)
918         if rot == 127:
919             result = 'To '
920         else:
921             result = '%d %% to ' % rot*100./127
922         return result + side
923
924     @staticmethod
925     def _decimaldegree_to_dms(f, emispheres):
926         if f >= 0:
927             e = emispheres[0]
928         else:
929             f = -f
930             e = emispheres[1]
931         result = '%d°' % int(f)
932         f = (f%1)*60
933         result += '%02.05f\' ' % f
934         result += e
935         return result
936
937     def get_latitude_str(self, default='Unknown'):
938         if self.latitude == AIS_LAT_NOT_AVAILABLE:
939             return default
940         return Nmea1._decimaldegree_to_dms(self.latitude / AIS_LATLON_SCALE, 'NS')
941
942     def get_longitude_str(self, default='Unknown'):
943         if self.longitude == AIS_LON_NOT_AVAILABLE:
944             return default
945         return Nmea1._decimaldegree_to_dms(self.longitude / AIS_LATLON_SCALE, 'EW')
946
947     def get_cog_str(self, default='Unknown'):
948         if self.cog == AIS_COG_NOT_AVAILABLE:
949             return default
950         return '%.1f°' % (self.cog/10.)
951
952     def get_heading_str(self, default='Unknown'):
953         if self.heading == AIS_NO_HEADING:
954             return default
955         return '%s°' % self.heading
956
957     def get_source_1_str(self):
958         return Nmea.format_source(self.source_1)
959
960 class Nmea5:
961     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=''):
962         self.timestamp_5   = timestamp
963         self.imo           = imo
964         self.name          = name         
965         self.callsign      = callsign
966         self.type          = type
967         self.dim_bow       = dim_bow
968         self.dim_stern     = dim_stern
969         self.dim_port      = dim_port
970         self.dim_starboard = dim_starboard
971         self.eta_M         = eta_M
972         self.eta_D         = eta_D
973         self.eta_h         = eta_h
974         self.eta_m         = eta_m
975         self.draught       = draught
976         self.destination   = destination
977         self.source_5      = source
978
979     from_values = __init__
980
981     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=''):
982         updated = False
983         if self.imo == 0 or imo != 0:
984             self.imo = imo
985             updated = True
986         if self.name == '' or name != '':
987             self.name = name
988             updated = True
989         if self.callsign == '' or callsign != '':
990             self.callsign = callsign
991             updated = True
992         if self.type == 0 or type != 0:
993             self.type = type
994             updated = True
995         if self.dim_bow == 0 or dim_bow != 0:
996             self.dim_bow = dim_bow
997             updated = True
998         if self.dim_stern == 0 or dim_stern != 0:
999             self.dim_stern = dim_stern
1000             updated = True
1001         if self.dim_port == 0 or dim_port != 0:
1002             self.dim_port = dim_port
1003             updated = True
1004         if self.dim_starboard == 0 or dim_starboard != 0:
1005             self.dim_starboard = dim_starboard
1006             updated = True
1007         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:
1008             self.eta_M = eta_M
1009             self.eta_D = eta_D
1010             self.eta_h = eta_h
1011             self.eta_m = eta_m
1012             updated = True
1013         if self.draught == 0 or draught != 0:
1014             self.draught = draught
1015             updated = True
1016         if self.destination == '' or destination != '':
1017             self.destination = destination
1018             updated = True
1019         if updated:
1020             self.timestamp_5 = timestamp
1021             self.source_5 = source
1022         return updated
1023
1024     def to_values(self):
1025         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
1026
1027     def from_record(self, record):
1028         values = struct.unpack(aivdm_record5_format, record)
1029         Nmea5.__init__(self, *values)
1030
1031     @staticmethod
1032     def new_from_record(record):
1033         values = struct.unpack(aivdm_record5_format, record)
1034         return Nmea5(*values)
1035
1036     def to_record(self):
1037         return struct.pack(aivdm_record5_format, *Nmea5.to_values(self))
1038         
1039     def from_file(self, file):
1040         record = file.read(aivdm_record5_length)
1041         Nmea5.from_record(self, record)
1042
1043     @staticmethod
1044     def new_from_file(file):
1045         record = file.read(aivdm_record5_length)
1046         return Nmea5.new_from_record(record)
1047
1048     def from_lastinfo(self, strmmsi):
1049         filename_nmea5 = os.path.join(DBPATH,
1050                                       'last',
1051                                       _hash3_pathfilename(strmmsi+'.nmea5'))
1052         try:
1053             f = file(filename_nmea5, 'rb')
1054         except IOError:
1055             logging.debug("file %s doesn't exists" % filename_nmea5)
1056             return
1057         lockf(f, LOCK_SH)
1058         Nmea5.from_file(self, f)
1059         f.close()
1060
1061     @staticmethod
1062     def new_from_lastinfo(strmmsi):
1063         filename_nmea5 = DBPATH+'/last/'+_hash3_pathfilename(strmmsi+'.nmea5')
1064         try:
1065             f = file(filename_nmea5, 'rb')
1066         except IOError:
1067             logging.debug("file %s doesn't exists" % filename_nmea5)
1068             return None
1069         lockf(f, LOCK_SH)
1070         record = f.read(aivdm_record5_length)
1071         f.close()
1072         return Nmea5.new_from_record(record)
1073
1074     @staticmethod
1075     def _clean_str(txt):
1076         if txt is None:
1077             return ''
1078         return txt.replace('\0','').replace('@', '').strip()
1079
1080     def get_name(self, default='Unknown'):
1081         result = self._clean_str(self.name)
1082         if result:
1083             return result
1084         return default
1085
1086     def get_callsign(self, default='Unknown'):
1087         return self._clean_str(self.callsign) or default
1088
1089     def get_shiptype(self, default='Unknown'):
1090         return SHIP_TYPES.get(self.type, default)
1091
1092     def get_length(self):
1093         return self.dim_bow + self.dim_stern
1094
1095     def get_width(self):
1096         return self.dim_port + self.dim_starboard
1097
1098     _monthes = 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(',')
1099     def get_eta_str(self, default='Unknown'):
1100         if not self.eta_M and not self.eta_D:
1101             return default
1102         result = ''
1103         if self.eta_M:
1104             if self.eta_M <= len(Nmea5._monthes):
1105                 result += Nmea5._monthes[self.eta_M - 1]
1106             else:
1107                 result += '%02d' % self.eta_M
1108         else:
1109             result += '***'
1110         result += ' '
1111         if self.eta_D:
1112             result += '%02d' % self.eta_D
1113         else:
1114             result += '**'
1115         if self.eta_h != 24:
1116             result += ' %02d' % self.eta_h
1117             if self.eta_m == 60:
1118                 result += 'h'
1119             else:
1120                 result += ':%02d' % self.eta_m
1121         return result
1122     
1123     def get_draught_str(self, default='Unknown'):
1124         if not self.draught:
1125             return default
1126         return '%.1f meters' % (self.draught/10.)
1127
1128     def get_destination(self, default='Unknown'):
1129         return self._clean_str(self.destination) or default
1130
1131     def get_source_5_str(self):
1132         return Nmea.format_source(self.source_5)
1133
1134 class Nmea(Nmea1, Nmea5):
1135     """
1136     This is nmea info, a merge of nmea1 and nmea5 packets
1137     """
1138     def __init__(self, strmmsi):
1139         self.strmmsi = strmmsi
1140         Nmea1.__init__(self, timestamp=0)
1141         Nmea5.__init__(self, timestamp=0)
1142
1143     ########################
1144     # Because of multiple inheritance some functions are unavailable:
1145     def _nmea_not_implemented(*args, **kargs):
1146         # used to avoid conflicting inherited members
1147         raise NotImplementedError
1148     from_values = _nmea_not_implemented
1149     to_values = _nmea_not_implemented
1150     from_record = _nmea_not_implemented
1151     new_from_record = _nmea_not_implemented
1152     to_record = _nmea_not_implemented
1153     from_file = _nmea_not_implemented
1154     new_from_file = _nmea_not_implemented
1155     ########################
1156
1157     def from_lastinfo(self, strmmsi):
1158         Nmea1.from_lastinfo(self, strmmsi)
1159         Nmea5.from_lastinfo(self, strmmsi)
1160     
1161     @staticmethod
1162     def new_from_lastinfo(strmmsi):
1163         # better than unimplemented, but not optimal
1164         nmea = Nmea(strmmsi)
1165         nmea.from_lastinfo(strmmsi)
1166         return nmea
1167
1168
1169     def get_flag(self, default=u'Unknown'):
1170         if self.strmmsi.startswith('00') and self.strmmsi[3:5]!='MI':
1171             ref_mmsi = self.strmmsi[2:]
1172         else:
1173             ref_mmsi = self.strmmsi
1174         country_mid = int(ref_mmsi[0:3])
1175         country_name = COUNTRIES_MID.get(country_mid, default)
1176         return country_name
1177
1178     def get_mmsi_public(self, default='Unknown'):
1179         if self.strmmsi.isdigit():
1180             return self.strmmsi
1181         return default
1182
1183     def get_title(self):
1184         """
1185         Returns the name of the ship if available
1186         Or its mmsi
1187         """
1188         return self.get_name(None) or self.get_mmsi_public()
1189
1190     def get_last_timestamp(self):
1191         """
1192         Returns the most recent of update from timestamp1, timestamp5
1193         """
1194         if self.timestamp_1 > self.timestamp_5:
1195             return self.timestamp_1
1196         else:
1197             return self.timestamp_5
1198
1199     def get_last_updated_str(self):
1200         """
1201         Returns a pretty formated update data as a string
1202         """
1203         lastupdate = self.get_last_timestamp()
1204         if lastupdate == 0:
1205             return u'Never'
1206         dt_lastupdate = datetime.utcfromtimestamp(lastupdate)
1207         delta = datetime.utcnow() - dt_lastupdate
1208         def nice_timedelta_str(delta):
1209             strdelta = ''
1210             disprank = None # first item type displayed
1211             if delta.days:
1212                 strdelta += str(delta.days)
1213                 if delta.days > 1:
1214                     strdelta += ' days '
1215                 else:
1216                     strdelta += ' day '
1217                 disprank = 0
1218             delta_s = delta.seconds
1219             delta_m = delta_s / 60
1220             delta_s -= delta_m * 60
1221             delta_h = delta_m / 60
1222             delta_m -= delta_h * 60
1223
1224             if delta_h:
1225                 strdelta += str(delta_h)
1226                 if delta_h > 1:
1227                     strdelta += ' hours '
1228                 else:
1229                     strdelta += ' hour '
1230                 if disprank is None:
1231                     disprank = 1
1232             if delta_m and (disprank is None or disprank >= 1):
1233                 strdelta += str(delta_m)
1234                 if delta_m > 1:
1235                     strdelta += ' minutes '
1236                 else:
1237                     strdelta += ' minute '
1238                 if disprank is None:
1239                     disprank = 2
1240             if delta_s and (disprank is None or disprank >= 2):
1241                 strdelta += str(delta_s)
1242                 if delta_s > 1:
1243                     strdelta += ' seconds '
1244                 else:
1245                     strdelta += ' second '
1246                 if disprank is None:
1247                     disprank = 3
1248             if not strdelta:
1249                 strdelta = 'less than a second '
1250             strdelta += ' ago'
1251             return strdelta
1252         return dt_lastupdate.strftime('%Y-%m-%d %H:%M:%S GMT') + ' (' +  nice_timedelta_str(delta) + ')'
1253
1254     @staticmethod
1255     def format_source(infosrc):
1256         if infosrc == '\0\0\0\0':
1257             return u'(empty)'
1258         elif infosrc.startswith('MI'):
1259             if len(infosrc) == 4:
1260                 return _get_mi_sourcename(struct.unpack('<2xH', infosrc)[0])
1261             else:
1262                 return u'Manual input'
1263         elif infosrc.startswith('U'):
1264             return u'User input'
1265         elif infosrc.startswith('NM'):
1266             return u'NMEA packets from '+xml_escape(infosrc[2:])
1267         elif infosrc.startswith('SP'):
1268             return u"ShipPlotter user %s" % infosrc[2:]
1269         elif infosrc == u'MTWW':
1270             return u'MarineTraffic.com web site'
1271         elif infosrc == u'MTTR':
1272             return u'MarineTraffic.com track files'
1273         else:
1274             return infosrc
1275
1276     csv_headers = [
1277         'mmsi',
1278         'flag',
1279         'name',
1280         'imo',
1281         'callsign',
1282         'type',
1283         'length',
1284         'width',
1285         'datetime',
1286         'status',
1287         'sog',
1288         'latitude',
1289         'longitude',
1290         'cog',
1291         'heading',
1292         'destination',
1293         'eta',
1294         'draught',
1295         ]
1296
1297     def get_dump_row(self):
1298         result = []
1299         def _clean(txt):
1300             if txt is None:
1301                 return ''
1302             return txt.replace('\0','').replace('@', '').strip()
1303         result.append(self.strmmsi)
1304         country_mid = int(self.strmmsi[:3])
1305         country_name = COUNTRIES_MID.get(country_mid, u'unknown')
1306         result.append(country_name.encode('utf-8'))
1307         result.append(_clean(self.name))
1308         result.append(str(self.imo))
1309         result.append(_clean(self.callsign))
1310         result.append(str(self.type) + '-' + SHIP_TYPES.get(self.type, 'unknown'))
1311         d = self.dim_bow + self.dim_stern
1312         if d:
1313             result.append(d)
1314         else:
1315             result.append(None)
1316         d = self.dim_port + self.dim_starboard
1317         if d:
1318             result.append(d)
1319         else:
1320             result.append(None)
1321         result.append(datetime.utcfromtimestamp(self.timestamp_1).strftime('%Y-%m-%dT%H:%M:%SZ'))
1322         result.append(STATUS_CODES.get(self.status, 'unknown'))
1323         if self.sog != AIS_SOG_NOT_AVAILABLE:
1324             result.append(str(self.sog/AIS_SOG_SCALE))
1325         else:
1326             result.append(None)
1327         if self.latitude != AIS_LAT_NOT_AVAILABLE:
1328             result.append(str(self.latitude/AIS_LATLON_SCALE))
1329         else:
1330             result.append(None)
1331         if self.longitude != AIS_LON_NOT_AVAILABLE:
1332             result.append(str(self.longitude/AIS_LATLON_SCALE))
1333         else:
1334             result.append(None)
1335         if self.cog != AIS_COG_NOT_AVAILABLE:
1336             result.append(str(self.cog/10.))
1337         else:
1338             result.append(None)
1339         if self.heading != AIS_NO_HEADING:
1340             result.append(str(self.heading))
1341         else:
1342             result.append(None)
1343         result.append(self.get_destination(''))
1344         result.append(self.get_eta_str(''))
1345         result.append(self.draught)
1346         result.append(self.source_5)
1347         return result
1348
1349
1350 class BankNmea1(list):
1351     """
1352     That class handle a .nmea1 archive file
1353     """
1354     def __init__(self, strmmsi, dt):
1355         list.__init__(self)
1356         self.strmmsi = strmmsi
1357         if isinstance(dt, date):
1358             dt = dt.strftime('%Y%m%d')
1359         self.date = dt
1360
1361     def get_filename(self):
1362         return os.path.join(DBPATH, 'bydate', self.date, _hash3_pathfilename(self.strmmsi+'.nmea1'))
1363
1364     def __load_from_file(self, file):
1365         '''
1366         Adds all record from opened file in this bank
1367         File must be locked before call
1368         '''
1369         while True:
1370             record = file.read(aivdm_record123_length)
1371             if not record:
1372                 break
1373             self.append(Nmea1.new_from_record(record))
1374
1375     def _write_in_file(self, file):
1376         '''
1377         Write all records from that bank in opened file
1378         File must be locked before call
1379         File should be truncated after call
1380         '''
1381         for nmea1 in self:
1382             file.write(nmea1.to_record())
1383
1384     def __load(self):
1385         try:
1386             file = open(self.get_filename(), 'rb')
1387             lockf(file, LOCK_SH)
1388         except IOError, ioerr:
1389             if ioerr.errno == 2: # No file
1390                 return
1391             raise
1392         self.__load_from_file(file)
1393         file.close()
1394         
1395     def __iter__(self):
1396         """
1397         Each call reload the file
1398         """
1399         self.__load()
1400         self.sort_by_date_reverse()
1401         return list.__iter__(self)
1402
1403     def packday(remove_manual_input=False):
1404         # FIXME broken
1405         #print "MMSI", strmmsi
1406
1407         self = BankNmea1(self.strmmsi, self.date)
1408         filename = self.get_filename()
1409         try:
1410             file = open(filename, 'r+b') # read/write binary
1411         except IOError, ioerr:
1412             if ioerr.errno != 2: # No file
1413                 raise
1414             return self # no data
1415         lockf(file, LOCK_EX)
1416         self.__load_from_file(file)
1417         self.sort_by_date()
1418
1419         file_has_changed = False
1420         file_must_be_unlinked = False
1421
1422         #print "PACKING..."
1423         file_has_changed = self.remove_duplicate_timestamp() or file_has_changed
1424
1425         if remove_manual_input:
1426             #print "REMOVING MANUAL INPUT..."
1427             file_has_changed = self.remove_manual_input() or file_has_changed
1428
1429         if file_has_changed:
1430             file.seek(0)
1431             self._write_in_file(file)
1432             file.truncate()
1433             if file.tell() == 0:
1434                 file_must_be_unlinked = True
1435
1436         file.close()
1437         
1438         if file_must_be_unlinked:
1439             # FIXME we release the lock before unlinking
1440             # another process might encounter an empty file (not handled)
1441             logging.warning('file was truncated to size 0. unlinking')
1442             os.unlink(filename) # we have the lock (!)
1443
1444     def dump_to_stdout(self):
1445         """
1446         Print contents to stdout
1447         """
1448         for nmea1 in self:
1449             nmea1.dump_to_stdout()
1450
1451     def sort_by_date(self):
1452         self.sort(lambda n1, n2: n1.timestamp_1 - n2.timestamp_1)
1453
1454     def sort_by_date_reverse(self):
1455         self.sort(lambda n1, n2: n2.timestamp_1 - n1.timestamp_1)
1456
1457     def remove_duplicate_timestamp(self):
1458         file_has_changed = False
1459         if len(self) <= 1:
1460             return file_has_changed
1461         last_timestamp = self[0].timestamp_1
1462         i = 1
1463         while i < len(self):
1464             if self[i].timestamp_1 == last_timestamp:
1465                 del self[i]
1466                 file_has_changed = True
1467             else:
1468                 last_timestamp = self[i].timestamp_1
1469                 i += 1
1470         return file_has_changed
1471         
1472     def remove_manual_input(self):
1473         file_has_changed = False
1474         i = 0
1475         while i < len(self):
1476             if self[i].source_1[:2] == 'MI':
1477                 del self[i]
1478                 file_has_changed = True
1479             else:
1480                 i += 1
1481         return file_has_changed
1482
1483 class Nmea1Feeder:
1484     """
1485     Yields all nmea1 packets between two given datetimes
1486     in REVERSE order (recent information first)
1487     """
1488     def __init__(self, strmmsi, datetime_end, datetime_begin=None, max_count=0):
1489         self.strmmsi = strmmsi
1490         assert datetime_end is not None
1491         self.datetime_end = datetime_end
1492         self.datetime_begin = datetime_begin or DB_STARTDATE
1493         self.max_count = max_count
1494
1495     def __iter__(self):
1496         dt_end = self.datetime_end
1497         d_end = dt_end.date()
1498         ts_end = datetime_to_timestamp(dt_end)
1499         if self.datetime_begin:
1500             dt_begin = self.datetime_begin
1501             d_begin = dt_begin.date()
1502             ts_begin = datetime_to_timestamp(dt_begin)
1503         else:
1504             dt_begin = None
1505             d_begin = None
1506             ts_begin = None
1507
1508         d = d_end
1509         count = 0
1510         while True:
1511             if d_begin is not None and d < d_begin:
1512                 return
1513             bank = BankNmea1(self.strmmsi, d)
1514             for nmea1 in bank:
1515                 if ts_begin is not None and nmea1.timestamp_1 < ts_begin:
1516                     return
1517                 if nmea1.timestamp_1 > ts_end:
1518                     continue
1519                 
1520                 yield nmea1
1521                
1522                 count += 1
1523                 if self.max_count and count >= self.max_count:
1524                     return
1525             d += timedelta(-1)
1526
1527
1528 class BankNmea5(list):
1529     """
1530     That class handle a .nmea5 archive file
1531     """
1532     def __init__(self, strmmsi, dt):
1533         list.__init__(self)
1534         self.strmmsi = strmmsi
1535         if isinstance(dt, date):
1536             try:
1537                 dt = dt.strftime('%Y%m%d')
1538             except ValueError:
1539                 logging.critical('dt=%s', dt)
1540                 raise
1541         self.date = dt
1542
1543     def get_filename(self):
1544         return os.path.join(DBPATH, 'bydate', self.date, _hash3_pathfilename(self.strmmsi+'.nmea5'))
1545
1546     def __load_from_file(self, file):
1547         '''
1548         Adds all record from opened file in this bank
1549         File must be locked before call
1550         '''
1551         while True:
1552             record = file.read(aivdm_record5_length)
1553             if not record:
1554                 break
1555             self.append(Nmea5.new_from_record(record))
1556
1557     def _write_in_file(self, file):
1558         '''
1559         Write all records from that bank in opened file
1560         File must be locked before call
1561         File should be truncated after call
1562         '''
1563         for nmea5 in self:
1564             file.write(nmea5.to_record())
1565
1566     def __load(self):
1567         try:
1568             file = open(self.get_filename(), 'rb')
1569             lockf(file, LOCK_SH)
1570         except IOError, ioerr:
1571             if ioerr.errno == 2: # No file
1572                 return
1573             raise
1574         self.__load_from_file(file)
1575         file.close()
1576         
1577     def __iter__(self):
1578         """
1579         Each call reload the file
1580         """
1581         self.__load()
1582         self.sort_by_date_reverse()
1583         return list.__iter__(self)
1584
1585     def sort_by_date(self):
1586         self.sort(lambda n1, n2: n1.timestamp_5 - n2.timestamp_5)
1587
1588     def sort_by_date_reverse(self):
1589         self.sort(lambda n1, n2: n2.timestamp_5 - n1.timestamp_5)
1590
1591 class Nmea5Feeder:
1592     """
1593     Yields all nmea5 packets between two given datetimes
1594     in REVERSE order (recent information first)
1595     """
1596     def __init__(self, strmmsi, datetime_end, datetime_begin=None, max_count=0):
1597         self.strmmsi = strmmsi
1598         assert datetime_end is not None
1599         self.datetime_end = datetime_end
1600         self.datetime_begin = datetime_begin or DB_STARTDATE
1601         self.max_count = max_count
1602
1603     def __iter__(self):
1604         dt_end = self.datetime_end
1605         d_end = dt_end.date()
1606         ts_end = datetime_to_timestamp(dt_end)
1607         if self.datetime_begin:
1608             dt_begin = self.datetime_begin
1609             d_begin = dt_begin.date()
1610             ts_begin = datetime_to_timestamp(dt_begin)
1611         else:
1612             dt_begin = None
1613             d_begin = None
1614             ts_begin = None
1615
1616         d = d_end
1617         count = 0
1618         while True:
1619             if d_begin is not None and d < d_begin:
1620                 return
1621             bank = BankNmea5(self.strmmsi, d)
1622             for nmea1 in bank:
1623                 if ts_begin is not None and nmea1.timestamp_5 < ts_begin:
1624                     return
1625                 if nmea1.timestamp_5 > ts_end:
1626                     continue
1627                 
1628                 yield nmea1
1629                
1630                 count += 1
1631                 if self.max_count and count >= self.max_count:
1632                     return
1633             d += timedelta(-1)
1634
1635
1636 class NmeaFeeder:
1637     """
1638     Yields nmea packets matching criteria.
1639     """
1640     def __init__(self, strmmsi, datetime_end, datetime_begin=None, filters=None, granularity=1, max_count=None):
1641         if granularity <= 0:
1642             logging.warning('Granularity=%d generates duplicate entries', granularity)
1643         self.strmmsi = strmmsi
1644         assert datetime_end is not None
1645         self.datetime_end = datetime_end
1646         self.datetime_begin = datetime_begin or DB_STARTDATE
1647         self.filters = filters or []
1648         self.granularity = granularity
1649         self.max_count = max_count
1650
1651     def __iter__(self):
1652         nmea = Nmea(self.strmmsi)
1653         if self.datetime_begin:
1654             nmea5_datetime_begin = self.datetime_begin - timedelta(30) # go back up to 30 days to get a good nmea5 packet
1655         else:
1656             nmea5_datetime_begin = None
1657         nmea5_iterator = Nmea5Feeder(self.strmmsi, self.datetime_end, nmea5_datetime_begin).__iter__()
1658         nmea5 = Nmea5(self.strmmsi, sys.maxint)
1659
1660         count = 0
1661         lasttimestamp = sys.maxint
1662         for nmea1 in Nmea1Feeder(self.strmmsi, self.datetime_end, self.datetime_begin):
1663             Nmea1.from_values(nmea, *nmea1.to_values())
1664             
1665             # try to get an nmea5 paket older
1666             nmea5_updated = False
1667             while nmea5 is not None and nmea5.timestamp_5 > nmea1.timestamp_1:
1668                 try:
1669                     nmea5 = nmea5_iterator.next()
1670                     nmea5_updated = True
1671                 except StopIteration:
1672                     nmea5 = None
1673             
1674             if nmea5_updated and nmea5 is not None:
1675                 Nmea5.merge_from_values(nmea, *nmea5.to_values())
1676
1677             filtered_out = False
1678             for is_ok in self.filters:
1679                 if not is_ok(nmea):
1680                     filtered_out = True
1681                     break
1682             if filtered_out:
1683                 continue
1684
1685             if nmea.timestamp_1 <= lasttimestamp - self.granularity:
1686                 yield nmea
1687                 count += 1
1688                 if self.max_count and count >= self.max_count:
1689                     return
1690                 lasttimestamp = nmea.timestamp_1
1691
1692
1693 def all_mmsi_generator():
1694     """
1695     Returns an array of all known strmmsi.
1696     """
1697     for dirname, dirs, fnames in os.walk(os.path.join(DBPATH, 'last')):
1698         for fname in fnames:
1699             if fname[-6:] == '.nmea1':
1700                 yield fname[:-6]
1701
1702
1703 def load_fleet_to_uset(fleetname):
1704     """
1705     Loads a fleet by name-id.
1706     Returns an array of strmmsi.
1707     """
1708     result = []
1709     sqlexec(u"SELECT mmsi FROM fleet_vessel WHERE fleet=%(fleetname)s", {'fleetname': fleetname})
1710     cursor = get_common_cursor()
1711     while True:
1712         row = cursor.fetchone()
1713         if not row:
1714             break
1715         mmsi = row[0]
1716         result.append(mmsi_to_strmmsi(mmsi))
1717     logging.debug('fleet=%s', result)
1718     return result
1719
1720
1721 def filter_area(nmea, area):
1722     """
1723     Returns false if position is out of area.
1724     """
1725     if nmea.latitude == AIS_LAT_NOT_AVAILABLE or nmea.longitude == AIS_LON_NOT_AVAILABLE:
1726         return False
1727     if not area.contains((nmea.latitude/AIS_LATLON_SCALE, nmea.longitude/AIS_LATLON_SCALE)):
1728         return False
1729     return True
1730
1731 def filter_knownposition(nmea):
1732     """
1733     Returns false if position is not fully known
1734     """
1735     # we are filtering out latitude=0 and longitude=0, that is not supposed to be necessary...
1736     return nmea.latitude != AIS_LAT_NOT_AVAILABLE and nmea.longitude != AIS_LON_NOT_AVAILABLE and nmea.latitude != 0 and nmea.longitude != 0
1737
1738
1739 _filter_positioncheck_last_mmsi = None
1740 def filter_speedcheck(nmea, max_mps):
1741     """
1742     mps is miles per seconds
1743     """
1744     global _filter_positioncheck_last_mmsi
1745     global _filter_positioncheck_last_time
1746     global _filter_positioncheck_last_time_failed
1747     global _filter_positioncheck_last_lat
1748     global _filter_positioncheck_last_lon
1749     global _filter_positioncheck_error_count
1750     if nmea.strmmsi != _filter_positioncheck_last_mmsi:
1751         _filter_positioncheck_last_time = None
1752         _filter_positioncheck_last_mmsi = nmea.strmmsi
1753         _filter_positioncheck_error_count = 0
1754     if _filter_positioncheck_last_time is not None:
1755         seconds = _filter_positioncheck_last_time - nmea.timestamp_1
1756         distance = dist3_latlong_ais((_filter_positioncheck_last_lat, _filter_positioncheck_last_lon), (nmea.latitude, nmea.longitude))
1757         if seconds:
1758             speed = distance/seconds
1759             if speed > max_mps:
1760                 if _filter_positioncheck_error_count < 10:
1761                     logging.debug("Ignoring point: distance = %s, time = %s, speed = %s kt, source = %s", distance, seconds, distance/seconds*3600, repr(nmea.source_1))
1762                     if _filter_positioncheck_error_count == 0 or _filter_positioncheck_last_time_failed != nmea.timestamp_1:
1763                         _filter_positioncheck_error_count += 1
1764                         _filter_positioncheck_last_time_failed = nmea.timestamp_1
1765                     return False
1766                 else:
1767                     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))
1768             _filter_positioncheck_error_count = 0
1769     _filter_positioncheck_last_time = nmea.timestamp_1
1770     _filter_positioncheck_last_lat = nmea.latitude
1771     _filter_positioncheck_last_lon = nmea.longitude
1772     return True
1773
1774
1775 def main():
1776     """
1777     Perform various operation on the database
1778     For usage, see "ais --help"
1779     """
1780     from optparse import OptionParser, OptionGroup
1781     global DBPATH
1782
1783     parser = OptionParser(usage='%prog [options] { mmsi | @fleet }+ | all')
1784
1785     parser.add_option('-d', '--debug',
1786         action='store_true', dest='debug', default=False,
1787         help="debug mode")
1788
1789     parser.add_option('-e', '--end',
1790         action='store', dest='sdt_end', metavar="'YYYYMMDD HHMMSS'",
1791         help='End data processing on that GMT date time.'
1792              'Default is now.'
1793              'If a date is provided without time, time defaults to 235959.')
1794     parser.add_option('-s', '--start',
1795         action='store', dest='sdt_start', metavar="'YYYYMMDD HHMMSS'",
1796         help='Start data processing on that date.'
1797              'Using that option enables multiple output of the same boat.'
1798              'Disabled by default.'
1799              'If a date is provided without time, time default to 000000.'
1800              'If other options enable multiple output, default to 1 day before'
1801              ' --end date/time.')
1802     parser.add_option('-g', '--granularity',
1803         action='store', type='int', dest='granularity', metavar='SECONDS',
1804         help='Dump only one position every granularity seconds.'
1805              'Using that option enables multiple output of the same boat.'
1806              'If other options enable multiple output, defaults to 600'
1807              ' (10 minutes)')
1808     parser.add_option('--max',
1809         action='store', type='int', dest='max_count', metavar='NUMBER',
1810         help='Dump a maximum of NUMBER positions every granularity seconds.'
1811              'Using that option enables multiple output of the same boat.')
1812
1813     parser.add_option('--filter-knownposition',
1814         action='store_true', dest='filter_knownposition', default=False,
1815         help="Eliminate unknown positions from results.")
1816
1817     parser.add_option('--filter-speedcheck',
1818         action='store', type='int', dest='speedcheck', default=200, metavar='KNOTS',
1819         help='Eliminate erroneaous positions from results,' 
1820              ' based on impossible speed.')
1821
1822     parser.add_option('--filter-type',
1823         action='append', type='int', dest='type_list', metavar='TYPE',
1824         help="process a specific ship type.")
1825     parser.add_option('--help-types',
1826         action='store_true', dest='help_types', default=False,
1827         help="display list of available types")
1828
1829     parser.add_option('--filter-area',
1830         action='store', type='str', dest='area_file', metavar="FILE.KML",
1831         help="only process a specific area as defined in a kml polygon file.")
1832
1833     parser.add_option('--filter-destination',
1834         action='store', type='str', dest='filter_destination', metavar="DESTINATION",
1835         help="Only print ships with that destination.")
1836
1837     parser.add_option('--no-headers',
1838         action='store_false', dest='csv_headers', default=True,
1839         help="skip CSV headers")
1840     #
1841
1842     expert_group = OptionGroup(parser, "Expert Options",
1843         "You normaly don't need any of these")
1844
1845     expert_group.add_option('--db',
1846         action='store', dest='db', default=DBPATH,
1847         help="path to filesystem database. Default=%default")
1848
1849     expert_group.add_option('--debug-sql',
1850         action='store_true', dest='debug_sql', default=False,
1851         help="print all sql queries to stdout before running them")
1852
1853     expert_group.add_option('--action',
1854         choices=('dump', 'removemanual', 'mmsidump', 'nirgaldebug', 'fixdestination'), default='dump',
1855         help='Possible values are:\n'
1856             'dump: dump values in csv format. This is the default.\n'
1857             'removemanual: Delete Manual Input entries from the database.\n'
1858             'mmsidump: Dump mmsi')
1859     parser.add_option_group(expert_group)
1860
1861     (options, args) = parser.parse_args()
1862
1863
1864     if options.help_types:
1865         print "Known ship types:"
1866         keys = SHIP_TYPES.keys()
1867         keys.sort()
1868         for k in keys:
1869             print k, SHIP_TYPES[k]
1870         sys.exit(0)
1871
1872     DBPATH = options.db
1873
1874     if options.debug:
1875         loglevel = logging.DEBUG
1876     else:
1877         loglevel = logging.INFO
1878     logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
1879
1880     if options.debug_sql:
1881         sql_setdebug(True)
1882
1883     #
1884     # Ships selections
1885     #
1886
1887     if len(args)==0:
1888         print >> sys.stderr, "No ship to process"
1889         sys.exit(1)
1890
1891     target_mmsi_iterator = []
1892     all_targets = False
1893     for arg in args:
1894         if arg == 'all':
1895             all_targets = True
1896         elif arg.startswith('@'):
1897             target_mmsi_iterator += load_fleet_to_uset(arg[1:])
1898         else:
1899             target_mmsi_iterator.append(arg)
1900     if all_targets:
1901         if target_mmsi_iterator:
1902             logging.warning('Selecting all ships, ignoring other arguments')
1903         target_mmsi_iterator = all_mmsi_generator()
1904
1905     #
1906     # Dates selections
1907     #
1908
1909     if options.sdt_end:
1910         # remove non digit characters
1911         options.sdt_end = "".join([ c for c in options.sdt_end if c.isdigit()])
1912         if len(options.sdt_end)==14:
1913             dt_end = datetime.strptime(options.sdt_end, '%Y%m%d%H%M%S')
1914         elif len(options.sdt_end)==8:
1915             dt_end = datetime.strptime(options.sdt_end, '%Y%m%d')
1916             dt_end = datetime.combine(dt_end.date(), time(23,59,59))
1917         else:
1918             print >> sys.stderr, "Invalid format for --end option"
1919             sys.exit(1)
1920     else:
1921         dt_end = datetime.utcnow()
1922     logging.debug('--end is %s', dt_end)
1923
1924     if options.sdt_start or options.granularity is not None or options.max_count:
1925         # time period is enabled
1926         if options.sdt_start:
1927             options.sdt_start = "".join([ c for c in options.sdt_start if c.isdigit()])
1928             if len(options.sdt_start)==14:
1929                 dt_start = datetime.strptime(options.sdt_start, '%Y%m%d%H%M%S')
1930             elif len(options.sdt_start)==8:
1931                 dt_start = datetime.strptime(options.sdt_start, '%Y%m%d')
1932             else:
1933                 print >> sys.stderr, "Invalid format for --start option"
1934                 sys.exit(1)
1935         else:
1936             dt_start = dt_end - timedelta(1)
1937         if options.granularity is None:
1938             options.granularity = 600
1939     else:
1940         dt_start = None
1941         options.max_count = 1
1942         if options.granularity is None:
1943             options.granularity = 600
1944     logging.debug('--start is %s', dt_start)
1945
1946     #
1947     # Filters
1948     #
1949
1950     filters = []
1951     
1952     if options.filter_knownposition:
1953         filters.append(filter_knownposition)
1954
1955     if options.speedcheck != 0:
1956         maxmps = options.speedcheck / 3600. # from knots to NM per seconds
1957         filters.append(lambda nmea: filter_speedcheck(nmea, maxmps))
1958
1959     if options.area_file:
1960         area = load_area_from_kml_polygon(options.area_file)
1961         filters.append(lambda nmea: filter_area(nmea, area))
1962     
1963     if options.type_list:
1964         def filter_type(nmea):
1965             return nmea.type in options.type_list
1966         filters.append(filter_type)
1967
1968     if options.filter_destination:
1969         filters.append(lambda nmea: nmea.destination.startswith(options.filter_destination))
1970
1971     #
1972     # Processing
1973     #
1974
1975     if options.action == 'dump':
1976         output = csv.writer(sys.stdout)
1977         if options.csv_headers:
1978             output.writerow(Nmea.csv_headers)
1979         for mmsi in target_mmsi_iterator:
1980             logging.debug('Considering %s', repr(mmsi))
1981             assert dt_end is not None
1982             for nmea in NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count):
1983                 output.writerow(nmea.get_dump_row())
1984
1985     elif options.action == 'removemanual':
1986         if filters:
1987             print >> sys.stderr, "removemanual action doesn't support filters"
1988             sys.exit(1)
1989
1990         for dt in dates:
1991             logging.info("Processing date %s", dt)
1992             for mmsi in target_mmsi_iterator:
1993                 BankNmea1(mmsi, dt).packday(remove_manual_input=True)
1994     
1995     elif options.action == 'mmsidump':
1996         for strmmsi in target_mmsi_iterator :
1997             print strmmsi
1998
1999     elif options.action == 'fixdestination':
2000         for mmsi in target_mmsi_iterator:
2001             for nmea in NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count):
2002                 destination = nmea.destination.rstrip(' @\0')
2003                 if destination:
2004                     sqlexec(u'UPDATE vessel SET destination = %(destination)s WHERE mmsi=%(mmsi)s AND destination IS NULL', {'mmsi':strmmsi_to_mmsi(mmsi), 'destination':destination})
2005                     logging.info('%s -> %s', mmsi, repr(destination))
2006                     dbcommit()
2007                     break # go to next mmsi
2008
2009
2010 if __name__ == '__main__':
2011     main()