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