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