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