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