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