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