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