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