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