185772ca65567e96cca0a6018602a5229be20e46
[ais.git] / bin / show_targets_ships.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from __future__ import division
5 import sys
6 import logging
7 import zipfile
8 from StringIO import StringIO # TODO use python 2.6 io.BufferedWrite(sys.stdout, )
9 from datetime import datetime, timedelta, time
10 import copy
11
12 from ais.ntools import strmmsi_to_mmsi, mmsi_to_strmmsi
13 from ais.common import *
14 from ais.area import load_area_from_kml_polygon
15 from ais.ntools import datetime_to_timestamp, xml_escape, LatLonFormatError, clean_latitude, clean_longitude
16
17 from ais.inputs.config import get_hidden_mmsi
18 from ais.djais.settings import AIS_BASE_URL
19
20 __all__ = [ 'format_fleet_lastpos', 'format_boat_intime', 'STYLE', 'KML_DISPLAYOPT_NONAMES', 'KML_DISPLAYOPT_HISTORICAL', 'KML_DISPLAYOPT_SOURCES', 'KML_DISPLAYOPT_SHOWHIDDEN', 'kml_to_kmz' ]
21
22
23 KML_DISPLAYOPT_NONAMES = 1 # don't print ship name
24 KML_DISPLAYOPT_HISTORICAL = 2 # never show ship track as lost
25 KML_DISPLAYOPT_SOURCES = 4 # display sources
26 KML_DISPLAYOPT_SHOWHIDDEN = 8 # show hidden ships
27
28
29 LOST_PERIOD = timedelta(1)
30
31 KML_HEADER = u'''\
32 <?xml version="1.0" encoding="UTF-8"?>
33 <kml xmlns="http://www.opengis.net/kml/2.2"
34     xmlns:gx="http://www.google.com/kml/ext/2.2">
35 <Document>
36 '''
37
38 KML_FOOTER = u'''\
39 </Document>
40 </kml>
41 '''
42
43    
44
45 class Style:
46     """
47     KML style for ship presentation.
48     It contains a list of png icons that were used.
49     This is a virtual class.
50     """
51     def __init__(self):
52         self.label_size = 0.7
53         self.icon_size = 0.5 # 0.2
54         self.used_icons = set()
55
56     def _format_style(self, stylename, icon, heading=None, color=None):
57         """
58         color format is google styled: aabbggrr
59         example ffea00ff for purple
60         """
61         result = u''
62         if heading is not None:
63             stylename += '-' + str(heading)
64         result += '<Style id="%s">\n' % stylename
65         result += '  <LabelStyle>\n'
66         result += '    <scale>%f</scale>\n' % self.label_size
67         result += '  </LabelStyle>\n'
68         result += '  <IconStyle>\n'
69         result += '    <Icon>\n'
70         result += '      <href>%s</href>\n' % icon
71         result += '    </Icon>\n'
72         if heading is not None:
73             result += '    <heading>%d</heading>\n' % heading
74         if color is not None:
75             result += '    <color>%s</color>\n' % color
76         result += '    <scale>%f</scale>\n' % self.icon_size
77         result += '    <hotSpot x="0.5" y="0.5" xunits="fraction" yunits="fraction"/>\n'
78         result += '  </IconStyle>\n'
79         result += '</Style>\n'
80         self.used_icons.add(icon)
81         return result
82     
83     def make_header(self):
84         raise NotImplementedError # abstract class
85     
86     def get_style_name(self, nmea, is_lost):
87         '''
88         Returns the name of the style based on nmea data
89         and whether the ship was seen recently or not.
90         '''
91         raise NotImplementedError # abstract class
92
93
94 class FishersStyle(Style):
95     def make_header(self):
96         white = None
97         green = 'ff86fd5f' # '5f-fd-86'
98         yellow = 'ff86eeff' #'ff-ee-86'
99         red = 'ff5865fc' #'fc-65-58'
100         result = u''
101         result += self._format_style('landstation', 'capital_small.png')
102
103         result += self._format_style('base-stop', 'boat-stop.png', color=white)
104         result += self._format_style('fisher-stop', 'boat-stop.png', color=red)
105         result += self._format_style('tug-stop', 'boat-stop.png', color=green)
106         result += self._format_style('auth-stop', 'boat-stop.png', color=yellow)
107         
108         for heading in [ None ] + range(0, 360, 10):
109             result += self._format_style('base', 'boat.png', color=white, heading=heading)
110             result += self._format_style('fisher', 'boat.png', color=red, heading=heading)
111             result += self._format_style('tug', 'boat.png', color=green, heading=heading)
112             result += self._format_style('auth', 'boat.png', color=yellow, heading=heading)
113             result += self._format_style('base-lost', 'boat-invis.png', color=white, heading=heading)
114             result += self._format_style('fisher-lost', 'boat-invis.png', color=red, heading=heading)
115             result += self._format_style('tug-lost', 'boat-invis.png', color=green, heading=heading)
116             result += self._format_style('auth-lost', 'boat-invis.png', color=yellow, heading=heading)
117         
118         return result
119
120     def get_style_name(self, nmea, is_lost):
121         '''
122         Returns the name of the style based on nmea data
123         and whether the ship was seen recently or not.
124         '''
125         if nmea.strmmsi.startswith('00') and not nmea.strmmsi.startswith('000'):
126             return 'landstation'
127         
128         if nmea.type == 30: # Fisher ship
129             stylename = 'fisher'
130         elif nmea.type in (31, 32, 52): # Towing or Tug
131             stylename = 'tug'
132         elif nmea.type in (35, 53, 55): # Authority
133             stylename = 'auth'
134         else:
135             stylename = 'base'
136
137         if (nmea.status in (1, 5, 6) and (nmea.sog == AIS_SOG_NOT_AVAILABLE or nmea.sog<0.5*AIS_SOG_SCALE)) \
138            or nmea.sog<1*AIS_SOG_SCALE:
139             stylename += '-stop'
140         else:
141             if is_lost:
142                 stylename += '-lost'
143             
144             if nmea.cog != AIS_COG_NOT_AVAILABLE:
145                 course = int(nmea.cog/10.) # ais format correction
146                 course = (course+5)//10*10 % 360 # go to neareast 10°
147                 stylename += '-%d' % course
148             elif nmea.heading != AIS_NO_HEADING:
149                 course = (nmea.heading+5)//10*10 % 360 # go to neareast 10°
150                 stylename += '-%d' % course
151         return stylename
152
153
154 class PelagosStyle(Style):
155     def make_header(self):
156         white = None
157         green = 'ff86fd5f' # '5f-fd-86'
158         yellow = 'ff86eeff' #'ff-ee-86'
159         pink = 'ffff00ea' #'ea-00-ff'
160         red = 'ff5865fc' #'fc-65-58'
161
162         result = u''
163         result += self._format_style('landstation', 'capital_small.png')
164
165         result += self._format_style('base-stop', 'boat-stop.png', color=white)
166         result += self._format_style('cargo-stop', 'boat-stop.png', color=green)
167         result += self._format_style('tanker-stop', 'boat-stop.png', color=yellow)
168         result += self._format_style('hsc-stop', 'boat-stop.png', color=pink)
169         result += self._format_style('hazarda-stop', 'boat-stop.png', color=red)
170
171         for heading in [ None ] + range(0, 360, 10):
172             result += self._format_style('base', 'boat.png', color=white, heading=heading)
173             result += self._format_style('cargo', 'boat.png', color=green, heading=heading)
174             result += self._format_style('tanker', 'boat.png', color=yellow, heading=heading)
175             result += self._format_style('hsc', 'boat.png', color=pink, heading=heading)
176             result += self._format_style('hazarda', 'boat.png', color=red, heading=heading)
177
178             result += self._format_style('base-lost', 'boat-invis.png', color=white, heading=heading)
179             result += self._format_style('cargo-lost', 'boat-invis.png', color=green, heading=heading)
180             result += self._format_style('tanker-lost', 'boat-invis.png', color=yellow, heading=heading)
181             result += self._format_style('hsc-lost', 'boat-invis.png', color=pink, heading=heading)
182             result += self._format_style('hazarda-lost', 'boat-invis.png', color=red, heading=heading)
183         
184         return result
185
186     def get_style_name(self, nmea, is_lost):
187         '''
188         Returns the name of the style based on nmea data
189         and whether the ship was seen recently or not.
190         '''
191         if (nmea.strmmsi.startswith('00') and not nmea.strmmsi.startswith('000')):
192             return 'landstation'
193         
194         if nmea.type in (41, 61, 71, 81): # major hazard materials
195             stylename = 'hazarda'
196         elif nmea.type >= 70 and nmea.type <= 79:
197             stylename = 'cargo'
198         elif nmea.type >= 80 and nmea.type <= 89:
199             stylename = 'tanker'
200         elif nmea.type >= 40 and nmea.type <= 49:
201             stylename = 'hsc'
202         else:
203             stylename = 'base'
204
205         if (nmea.status in (1, 5, 6) and (nmea.sog == AIS_SOG_NOT_AVAILABLE or nmea.sog<0.5*AIS_SOG_SCALE)) \
206            or nmea.sog<1*AIS_SOG_SCALE:
207             stylename += '-stop'
208         else:
209             if is_lost:
210                 stylename += '-lost'
211             
212             if nmea.cog != AIS_COG_NOT_AVAILABLE:
213                 course = int(nmea.cog/10.) # ais format correction
214                 course = (course+5)//10*10 % 360 # go to neareast 10°
215                 stylename += '-%d' % course
216             elif nmea.heading != AIS_NO_HEADING:
217                 course = (nmea.heading+5)//10*10 % 360 # go to neareast 10°
218                 stylename += '-%d' % course
219         return stylename
220
221 STYLE = FishersStyle()
222
223
224 def format_boat_data(nmea, timeinfo=None, display_options=0):
225     '''
226     timeinfo: None to generate a GoogleEarth 4 file, with no timeing information
227               True to generate a GoogleEarth 5 file, with start time from nmea
228               datetime or timestamp instance to generate a GoogleEarth 5 file, with start time from nmea and this enddate
229     '''
230     mmsi = nmea.strmmsi
231     timestamp_1, status, rot, sog, latitude, longitude, cog, heading, source_1 = Nmea1.to_values(nmea)
232     timestamp_5, imo, name, callsign, type_, dim_bow, dim_stern, dim_port, dim_starboard, eta_M, eta_D, eta_h, eta_m, draught, destination, source_5 = Nmea5.to_values(nmea)
233
234     if latitude == AIS_LAT_NOT_AVAILABLE or longitude == AIS_LON_NOT_AVAILABLE:
235         return u''
236
237     result = u''
238
239     if timeinfo is not None and timeinfo != True:
240         if not isinstance(timeinfo, datetime):
241             timeinfo = datetime.utcfromtimestamp(timeinfo)
242             
243     result += u'<Placemark>\n'
244
245     if not (display_options & KML_DISPLAYOPT_NONAMES):
246         result += u'<name>' + xml_escape(nmea.get_title()) + u'</name>\n'
247
248     result += u'<description><![CDATA[\n'
249     if display_options & KML_DISPLAYOPT_NONAMES:
250         result += u'Vessel name: ' + xml_escape(nmea.get_name()) + u'<br>\n'
251         
252     dt_1 = datetime.utcfromtimestamp(timestamp_1)
253     if display_options & KML_DISPLAYOPT_HISTORICAL:
254         result += u'%s GMT<br>\n' % dt_1.strftime('%Y-%m-%d %H:%M:%S')
255         is_lost = None
256     else:
257         if timeinfo is None:
258             is_lost = dt_1 < datetime.utcnow()-LOST_PERIOD
259             if is_lost:
260                 result += u'Tack <b>lost</b> since %s GMT<br>\n' % dt_1.strftime('%Y-%m-%d %H:%M:%S')
261             else:
262                 result += u'Last seen %s GMT<br>\n' % dt_1.strftime('%Y-%m-%d %H:%M:%S')
263         else: # timeinfo is not None
264             if timeinfo == True:
265                 is_lost = None
266             else:
267                 is_lost = timeinfo > dt_1 + LOST_PERIOD
268
269     if not mmsi.isdigit():
270         result += u'NO MMSI<br>\n'
271         is_land_station = False
272     else:
273         result += u'MMSI: %s ' % mmsi
274         ref_mmsi = str(mmsi) # FIXME not needed
275         is_land_station = ref_mmsi.startswith('00') and not ref_mmsi.startswith('000')
276         if is_land_station:
277             ref_mmsi = ref_mmsi[2:]
278         result += u'('+COUNTRIES_MID.get(int(ref_mmsi[:3]), u'fake')+u')<br>\n'
279     if not is_land_station :
280         if imo:
281             #result += u'IMO<a href="http://www.xvas.it/SPECIAL/VTship.php?imo=%(imo)s&amp;mode=CK">%(imo)s</a><br>\n' % { 'imo': imo }
282             result += u'IMO: %s<br>\n' % imo
283         else:
284             result += u'no known IMO<br>\n'
285     callsign = nmea.get_callsign(default=None)
286     if callsign is not None:
287         result += u'Callsign: %s<br>\n' % xml_escape(callsign)
288     if type_:
289         result += u'Type: %s<br>\n' % SHIP_TYPES.get(type_, 'unknown')
290     if status != AIS_STATUS_NOT_AVAILABLE:
291         result += u'Status: %s<br>\n' % STATUS_CODES.get(status, 'unknown')
292     if cog != AIS_COG_NOT_AVAILABLE:
293         result += u'Course: %.01f°<br>\n' % (cog/10.)
294     if heading != AIS_NO_HEADING:
295         result += u'Heading: %d°<br>\n' % heading
296     if sog != AIS_SOG_NOT_AVAILABLE:
297         if sog != AIS_SOG_FAST_MOVER:
298             result += u'Speed: %.01f kts<br>\n' % (sog/AIS_SOG_SCALE)
299         else:
300             result += u'Speed: more that than 102.2 kts<br>\n'
301     length = nmea.get_length()
302     width = nmea.get_width()
303     if length or width or draught:
304         result += u'Size: %dx%d' % (length, width)
305         if draught:
306             result += u'/%.01f' % (draught/10.)
307         result += u'm<br>\n'
308     destination = nmea.get_destination(default=None)
309     if destination:
310         result += u'Destination: %s<br>\n' % xml_escape(destination)
311     eta = nmea.get_eta_str(default=None)
312     if eta is not None:
313         result += u'ETA: %s<br>\n' % eta
314
315     if (display_options & KML_DISPLAYOPT_SOURCES) and (source_1 or source_5):
316         result += u'Source: '
317         if source_1:
318             result += Nmea.format_source(source_1)
319         if source_5 and source_1 != source_5:
320             result += u', '+ Nmea.format_source(source_5)
321         result += u'<br>\n'
322     result += u'<a href="' + AIS_BASE_URL + u'/vessel/%(mmsi)s/">More...</a>' \
323               % {'mmsi': mmsi}
324     result += u']]>\n'
325     result += u'</description>\n'
326
327     result += u'<styleUrl>#%s</styleUrl>\n' \
328               % STYLE.get_style_name(nmea, is_lost)
329
330     result += u'<Point>\n'
331     result += u'<coordinates>%s,%s</coordinates>' \
332         % (longitude/AIS_LATLON_SCALE, latitude/AIS_LATLON_SCALE)
333     result += u'</Point>\n'
334
335     if timeinfo is not None:
336         #result += u'<TimeStamp><when>%s</when></TimeStamp>\n' \
337         #          % (dt_1.strftime('%Y-%m-%dT%H:%M:%SZ'))
338         result += u'<gx:TimeSpan><begin>%s</begin>' \
339                   % dt_1.strftime('%Y-%m-%dT%H:%M:%SZ')
340         if timeinfo != True:
341             result += u'<end>%s</end>' \
342                       % timeinfo.strftime('%Y-%m-%dT%H:%M:%SZ')
343         result += u'</gx:TimeSpan>\n'
344     result += u'</Placemark>\n'
345     return result
346
347
348
349
350 def format_fleet_lastpos(mmsi_iterator, document_name=None, display_options=0):
351     result = u''
352     result += KML_HEADER
353
354     if document_name is None:
355         document_name = 'AIS database'
356     result += u'<name>%s</name>\n' % document_name
357     
358     result += STYLE.make_header()
359
360     long_ago = datetime_to_timestamp(datetime.utcnow() - timedelta(90))
361
362     for mmsi in mmsi_iterator:
363         nmea = Nmea.new_from_lastinfo(mmsi)
364         if not (display_options & KML_DISPLAYOPT_SHOWHIDDEN) and strmmsi_to_mmsi(mmsi) in get_hidden_mmsi():
365             result += u'<Placemark>\n'
366             result += u'<name>' + xml_escape(nmea.get_title()) + u'</name>\n'
367             result += u'<description>Sorry, access to the position of that ship is restricted. It is not available for you.</description>\n'
368             result += u'</Placemark>\n'
369             continue
370         if nmea.get_last_timestamp() < long_ago:
371             continue
372         result += format_boat_data(nmea, display_options = display_options | KML_DISPLAYOPT_SOURCES)
373     result += KML_FOOTER
374     return result
375
376
377 def format_boat_intime_section(nmea_iterator, kml_displayopt=0):
378     result = u''
379     last_nmea = None
380     for nmea in nmea_iterator:
381         if last_nmea is None:
382             timeinfo = True
383         else:
384             timeinfo = datetime.utcfromtimestamp(last_nmea.timestamp_1)
385
386         result += format_boat_data(nmea, timeinfo,
387                                    kml_displayopt|KML_DISPLAYOPT_HISTORICAL)
388         # make a copy because nmea will be patched with new data:
389         last_nmea = copy.copy(nmea)
390     if not result:
391         result += u'<description>Vessel not found</description>'
392     return result
393
394
395 def format_boat_intime(nmea_iterator):
396     result = u''
397     result += KML_HEADER
398     result += STYLE.make_header()
399     result += format_boat_intime_section(nmea_iterator)
400     result += KML_FOOTER
401     return result
402
403
404 def format_boat_track_section(nmea_iterator, name=u''):
405     strcoordinates = '<Placemark>\n<LineString>\n<coordinates>\n'
406     segment_length = 0
407     for nmea in nmea_iterator:
408         if name == u'':
409             name = nmea.get_title()
410         if nmea.longitude != AIS_LON_NOT_AVAILABLE and nmea.latitude != AIS_LAT_NOT_AVAILABLE:
411             if segment_length > 65000:
412                 logging.debug('Line is too long. Spliting.')
413                 strcoordinates += ' %.8f,%.8f' \
414                                   % (nmea.longitude/AIS_LATLON_SCALE,
415                                      nmea.latitude/AIS_LATLON_SCALE)
416                 strcoordinates += '</coordinates>\n</LineString>\n</Placemark>\n<Placemark>\n<LineString>\n<coordinates>\n'
417                 segment_length = 0
418             else:
419                 segment_length += 1
420             strcoordinates += ' %.8f,%.8f' \
421                               % (nmea.longitude/AIS_LATLON_SCALE,
422                                  nmea.latitude/AIS_LATLON_SCALE)
423     strcoordinates += '</coordinates>\n</LineString></Placemark>\n'
424
425     result = u''
426     result += u'<name>%s track</name>\n' % name
427     if len(strcoordinates)>39+2*(1+12+1+11)+42+1:
428         result += unicode(strcoordinates)
429     else:
430         result += u'<description>No data available</description>\n'
431     return result
432
433
434 def kml_to_kmz(inputstr):
435     if isinstance(inputstr, unicode):
436         inputstr = inputstr.encode('utf-8')
437     output = StringIO()
438     kmz = zipfile.ZipFile(output, 'w')
439     kmz.writestr('doc.kml', inputstr)
440     for iconname in STYLE.used_icons:
441         kmz.write('/usr/lib/ais/kmz_icons/'+iconname, iconname)
442     kmz.close()
443     return output.getvalue()
444     
445
446 def main():
447     global DBPATH, STYLE
448     from optparse import OptionParser, OptionGroup
449
450     parser = OptionParser(usage='%prog [options] { mmsi | @fleetname | #fleetid }+ | all')
451
452     parser.add_option('-d', '--debug',
453         action='store_true', dest='debug', default=False,
454         help="debug mode")
455
456     parser.add_option('-e', '--end',
457         action='store', dest='sdt_end', metavar="'YYYYMMDD HHMMSS'",
458         help='End data processing on that GMT date time.'
459              ' Default is now.'
460              ' If a date is provided without time, time defaults to 235959.')
461     parser.add_option('-s', '--start',
462         action='store', dest='sdt_start', metavar="'YYYYMMDD HHMMSS'",
463         help='Start data processing on that date.'
464              ' Using that option enables multiple output of the same boat.'
465              ' Disabled by default.'
466              ' If a date is provided without time, time default to 000000.'
467              ' If other options enable multiple output, default to 1 day before'
468              ' --end date/time.')
469     parser.add_option('--duration',
470         action='store', dest='sdt_duration', metavar="DURATION",
471         help='Duration of reference period.'
472              ' Last character may be S for seconds, M(inutes), D(ays), W(eeks)'
473              ' Default is seconds.'
474              ' This is the time length bewteen --start and --end above.'
475              ' If you want multiple output of the same boat, you may use '
476              ' --start, --end or --duration, 2 of them, but not 3 of them.')
477     parser.add_option('-g', '--granularity',
478         action='store', type='int', dest='granularity', metavar='SECONDS',
479         help='Dump only one position every granularity seconds.'
480              'Using that option enables multiple output of the same boat.'
481              'If other options enable multiple output, defaults to 600'
482              ' (10 minutes)')
483     parser.add_option('--max',
484         action='store', type='int', dest='max_count', metavar='NUMBER',
485         help='Dump a maximum of NUMBER positions every granularity seconds.'
486              'Using that option enables multiple output of the same boat.')
487
488     parser.add_option('--show-hidden-ships',
489         action='store_true', dest='show_hidden_ships', default=False,
490         help='Include hidden ships in results')
491
492     parser.add_option('--filter-knownposition',
493         action='store_true', dest='filter_knownposition', default=False,
494         help="eliminate unknown positions from results.")
495
496     parser.add_option('--filter-speedcheck',
497         action='store', type='int', dest='speedcheck', default=200, metavar='KNOTS',
498         help='Eliminate erroneaous positions from results,' 
499              ' based on impossible speed.')
500
501     parser.add_option('--filter-type',
502         action='append', type='int', dest='type_list', metavar="TYPE",
503         help="process a specific ship type.")
504     parser.add_option('--help-types',
505         action='store_true', dest='help_types', default=False,
506         help="display list of available types")
507
508     parser.add_option('--filter-area',
509         action='store', type='str', dest='area_file', metavar="FILE.KML",
510         help="only process a specific area as defined in a kml polygon file.")
511     parser.add_option('--filter-farfrom',
512         action='store', dest='far_from', nargs=3, metavar='LAT LONG MILES',
513         help="only show ships farther than MILES miles from LAT,LONG")
514     parser.add_option('--filter-closeto',
515         action='store', dest='close_to', nargs=3, metavar='LAT LONG MILES',
516         help="only show ships closer than MILES miles from LAT,LONG")
517     parser.add_option('--filter-sog-le',
518         action='store', dest='sog_le', metavar='KNOTS',
519         help='only show ships when speed over ground is less or equal to KNOTS.')
520
521     #
522     parser.add_option('--format',
523         choices=('positions', 'track', 'animation'), dest='format', default='positions',
524         help="select output format: positions(*) or track or animation")
525
526     parser.add_option('--kml',
527         action='store_true', dest='output_kml', default=False,
528         help="Output a kml file. Default is output of a kmz file with icons")
529     parser.add_option('--inner-kml',
530         action='store_true', dest='output_innerkml', default=False,
531         help='Output a kml fragment file without the <Document> wrappers.\n'
532              'File should be reproccessed to be usefull. That option implies --kml')
533
534     parser.add_option('--style',
535         choices=('fishers', 'pelagos'), dest='style', default='fishers',
536         help='select one of the predefined style for display: '
537              'fishers(*) or pelagos')
538
539     parser.add_option('--icon-size',
540         action='store', type='float', dest='icon_size', metavar='SCALE', default=0.5,
541         help='Set icons size. Default = %default')
542     parser.add_option('--label-size',
543         action='store', type='float', dest='label_size', metavar='SCALE', default=0.7,
544         help='Set label scale. Default = %default')
545     
546     parser.add_option('--no-names',
547         action='store_const', const=KML_DISPLAYOPT_NONAMES,
548         dest='kml_displayopt_noname', default=0,
549         help="don't show ship names")
550
551     parser.add_option('--show-sources',
552         action='store_const', const=KML_DISPLAYOPT_SOURCES,
553         dest='kml_displayopt_sources', default=0,
554         help="show information source")
555
556     #
557
558     expert_group = OptionGroup(parser, "Expert Options",
559         "You normaly don't need any of these")
560
561     expert_group.add_option('--db',
562         action='store', dest='db', default=DBPATH,
563         help="path to filesystem database. Default=%default")
564     parser.add_option_group(expert_group)
565
566     options, args = parser.parse_args()
567
568     
569     if options.help_types:
570         keys = SHIP_TYPES.keys()
571         keys.sort()
572         for k in keys:
573             print k, SHIP_TYPES[k]
574         sys.exit(0)
575
576     DBPATH = options.db
577
578     if options.debug:
579         loglevel = logging.DEBUG
580     else:
581         loglevel = logging.INFO
582     logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
583
584     #
585     # Ships selections
586     #
587
588     if len(args)==0:
589         print >> sys.stderr, "No ship to process"
590         sys.exit(1)
591
592     target_mmsi_iterator = []
593     all_targets = False
594     for arg in args:
595         if arg == 'all':
596             all_targets = True
597         elif arg.startswith('@'):
598             target_mmsi_iterator += load_fleet_to_uset(fleetname_to_fleetid(arg[1:]))
599         elif arg.startswith('^'):
600             target_mmsi_iterator += load_fleet_to_uset(int(arg[1:]))
601         else:
602             target_mmsi_iterator.append(arg)
603     if all_targets:
604         if target_mmsi_iterator:
605             logging.warning('Selecting all ships, ignoring other arguments')
606         target_mmsi_iterator = all_mmsi_generator()
607
608     if not options.show_hidden_ships:
609         target_mmsi_iterator = mmsiiterator_nohiddenship(target_mmsi_iterator)
610
611     #
612     # Dates selections
613     #
614
615     if options.sdt_start:
616         # remove non digit characters
617         options.sdt_start = "".join([ c for c in options.sdt_start if c.isdigit()])
618         if len(options.sdt_start)==14:
619             options.sdt_start = datetime.strptime(options.sdt_start, '%Y%m%d%H%M%S')
620         elif len(options.sdt_start)==8:
621             options.sdt_start = datetime.strptime(options.sdt_start, '%Y%m%d')
622         else:
623             print >> sys.stderr, "Invalid format for --start option"
624             sys.exit(1)
625
626     if options.sdt_end:
627         # remove non digit characters
628         options.sdt_end = "".join([ c for c in options.sdt_end if c.isdigit()])
629         if len(options.sdt_end)==14:
630             options.sdt_end = datetime.strptime(options.sdt_end, '%Y%m%d%H%M%S')
631         elif len(options.sdt_end)==8:
632             options.sdt_end = datetime.strptime(options.sdt_end, '%Y%m%d')
633             options.sdt_end = datetime.combine(options.sdt_end.date(), time(23, 59, 59))
634         else:
635             print >> sys.stderr, "Invalid format for --end option"
636             sys.exit(1)
637     
638     if options.sdt_duration:
639         # remove spaces
640         options.sdt_duration = options.sdt_duration.replace(' ', '')
641         # use upercase
642         options.sdt_duration = options.sdt_duration.upper()
643         if options.sdt_duration[-1] == 'S':
644             options.sdt_duration = options.sdt_duration[:-1]
645             duration_unit = 1
646         elif options.sdt_duration[-1] == 'M':
647             options.sdt_duration = options.sdt_duration[:-1]
648             duration_unit = 60
649         elif options.sdt_duration[-1] == 'H':
650             options.sdt_duration = options.sdt_duration[:-1]
651             duration_unit = 60*60
652         elif options.sdt_duration[-1] == 'D':
653             options.sdt_duration = options.sdt_duration[:-1]
654             duration_unit = 24*60*60
655         elif options.sdt_duration[-1] == 'W':
656             options.sdt_duration = options.sdt_duration[:-1]
657             duration_unit = 7*24*60*60
658         else:
659             duration_unit = 1
660         try:
661             options.sdt_duration = long(options.sdt_duration)
662         except ValueError:
663             print >> sys.stderr, "Can't parse duration"
664             sys.exit(1)
665         options.sdt_duration = timedelta(0, options.sdt_duration * duration_unit)
666
667     if options.sdt_start or options.sdt_duration or options.granularity is not None or options.max_count:
668         # Time period is enabled (note that date_end only defaults to one day archives ending then)
669         if not options.sdt_start and not options.sdt_end and not options.sdt_duration:
670             options.sdt_duration = timedelta(1) # One day
671         # continue without else
672         if not options.sdt_start and not options.sdt_end and options.sdt_duration:
673             dt_end = datetime.utcnow()
674             dt_start = dt_end - options.sdt_duration
675         #elif not options.sdt_start and options.sdt_end and not options.sdt_duration:
676             # never reached
677         elif not options.sdt_start and options.sdt_end and options.sdt_duration:
678             dt_end = options.sdt_end
679             dt_start = dt_end - options.sdt_duration
680         elif options.sdt_start and not options.sdt_end and not options.sdt_duration:
681             dt_start = options.sdt_start
682             dt_end = datetime.utcnow()
683         elif options.sdt_start and not options.sdt_end and options.sdt_duration:
684             dt_start = options.sdt_start
685             dt_end = dt_start + options.sdt_duration
686         elif options.sdt_start and options.sdt_end and not options.sdt_duration:
687             dt_start = options.sdt_start
688             dt_end = options.sdt_end
689         else:
690             assert options.sdt_start and options.sdt_end and options.sdt_duration, 'Internal error'
691             print >> sys.stderr, "You can't have all 3 --start --end and --duration"
692             sys.exit(1)
693         if options.granularity is None:
694             options.granularity = 600
695     else:
696         # Only get one position
697         dt_start = None
698         if options.sdt_end:
699             dt_end = options.sdt_end
700         else:
701             dt_end = datetime.utcnow()
702         options.max_count = 1
703         if options.granularity is None:
704             options.granularity = 600
705             
706     logging.debug('--start is %s', dt_start)
707     logging.debug('--end is %s', dt_end)
708
709     #
710     # Filters
711     #
712
713     filters = []
714     
715     if options.filter_knownposition:
716         filters.append(filter_knownposition)
717
718     if options.speedcheck != 0:
719         maxmps = options.speedcheck / 3600. # from knots to NM per seconds
720         filters.append(lambda nmea: filter_speedcheck(nmea, maxmps))
721
722     if options.area_file:
723         area = load_area_from_kml_polygon(options.area_file)
724         filters.append(lambda nmea: filter_area(nmea, area))
725
726     if options.close_to:
727         try:
728             lat = clean_latitude(unicode(options.close_to[0], 'utf-8'))
729             lon = clean_longitude(unicode(options.close_to[1], 'utf-8'))
730         except LatLonFormatError as err:
731             print >> sys.stderr, err.args
732             sys.exit(1)
733         miles = float(options.close_to[2])
734         filters.append(lambda nmea: filter_close_to(nmea, lat, lon, miles))
735
736     if options.far_from:
737         try:
738             lat = clean_latitude(unicode(options.far_from[0], 'utf-8'))
739             lon = clean_longitude(unicode(options.far_from[1], 'utf-8'))
740         except LatLonFormatError as err:
741             print >> sys.stderr, err.args
742             sys.exit(1)
743         miles = float(options.far_from[2])
744         filters.append(lambda nmea: filter_far_from(nmea, lat, lon, miles))
745     
746     if options.sog_le:
747         filters.append(lambda nmea: filter_sog_le(nmea, float(options.sog_le)))
748
749     if options.type_list:
750         def filter_type(nmea):
751             #print nmea.type, repr(options.type_list), nmea.type in options.type_list
752             #print repr(nmea.get_dump_row())
753             return nmea.type in options.type_list
754         filters.append(filter_type)
755
756     #
757     # Display options
758     #
759
760     if options.style == 'pelagos':
761         STYLE = PelagosStyle()
762
763     kml_displayopt = options.kml_displayopt_noname | options.kml_displayopt_sources
764
765     STYLE.icon_size = options.icon_size
766     STYLE.label_size = options.label_size
767
768     if options.output_innerkml:
769         options.output_kml = True
770     #
771     # Processing
772     #
773
774     if options.format == 'positions':
775         result = u''
776         if not options.output_innerkml:
777             result += KML_HEADER
778             result += STYLE.make_header()
779         for mmsi in target_mmsi_iterator:
780             nmea_generator = NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count)
781             for nmea in nmea_generator:
782                 result += format_boat_data(nmea, None, kml_displayopt|KML_DISPLAYOPT_HISTORICAL)
783         if not options.output_innerkml:
784             result += KML_FOOTER
785
786     elif options.format=='animation':
787         result = u''
788         if not options.output_innerkml:
789             result += KML_HEADER
790             result += STYLE.make_header()
791         for mmsi in target_mmsi_iterator:
792             nmea_generator = NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count)
793             result += '<Folder>\n'
794             result += format_boat_intime_section(nmea_generator, kml_displayopt|KML_DISPLAYOPT_HISTORICAL)
795             result += '</Folder>\n'
796         if not options.output_innerkml:
797             result += KML_FOOTER
798                
799     elif options.format=='track':
800         result = u''
801         if not options.output_innerkml:
802             result += KML_HEADER
803             # don't call STYLE.make_header since there is no icons
804         for mmsi in target_mmsi_iterator:
805             nmea_generator = NmeaFeeder(mmsi, dt_end, dt_start, filters, granularity=options.granularity, max_count=options.max_count)
806             result += '<Folder>\n'
807             result += format_boat_track_section(nmea_generator)
808             result += '</Folder>\n'
809         if not options.output_innerkml:
810             result += KML_FOOTER
811
812     else:
813         print >> sys.stderr, 'Unknown output format'
814         sys.exit(1)
815         
816     result = result.encode('utf-8')
817
818     if not options.output_kml:
819         result = kml_to_kmz(result)
820
821     print result
822
823 if __name__ == '__main__':
824     main()