text: #!/usr/bin/env python
  # encoding: utf-8
  
  """webgps.py
  
  This is a Python port of webgps.c from http://www.wireless.org.au/~jhecker/gpsd/
  by Beat Bolli <me+gps@drbeat.li>
  
  It creates a skyview of the currently visible GPS satellites and their tracks
  over a time period.
  
  Usage:
      ./webgps.py [duration]
  
      duration may be
      - a number of seconds
      - a number followed by a time unit ('s' for secinds, 'm' for minutes,
        'h' for hours or 'd' for days, e.g. '4h' for a duration of four hours)
      - the letter 'c' for continuous operation
  
  If duration is missing, the current skyview is generated and webgps.py exits
  immediately. This is the same as giving a duration of 0.
  
  If a duration is given, webgps.py runs for this duration and generates the
  tracks of the GPS satellites in view. If the duration is the letter 'c',
  the script never exits and continuously updates the skyview.
  
  webgps.py generates two files: a HTML5 file that can be browsed, and a
  JavaScript file that contains the drawing commands for the skyview. The HTML5
  file auto-refreshes every five minutes. The generated file names are
  "gpsd-<duration>.html" and "gpsd-<duration>.js".
  
  If webgps.py is interrupted with Ctrl-C before the duration is over, it saves
  the current tracks into the file "tracks.p". This is a Python "pickle" file.
  If this file is present on start of webgps.py, it is loaded. This allows to
  restart webgps.py without losing accumulated satellite tracks.
  """
  
  from __future__ import absolute_import, print_function, division
  
  import time, math, sys, os, pickle
  
  from gps import *
  
  TRACKMAX = 1024
  STALECOUNT = 10
  
  DIAMETER = 200
  
  def polartocart(el, az):
      radius = DIAMETER * (1 - el / 90.0) # * math.cos(Deg2Rad(float(el)))
      theta = Deg2Rad(float(az - 90))
      return (
          # Changed this back to normal orientation - fw
          int(radius * math.cos(theta) + 0.5),
          int(radius * math.sin(theta) + 0.5)
      )
  
  
  class Track:
      '''Store the track of one satellite.'''
  
      def __init__(self, prn):
          self.prn = prn
          self.stale = 0
          self.posn = []          # list of (x, y) tuples
  
      def add(self, x, y):
          pos = (x, y)
          self.stale = STALECOUNT
          if not self.posn or self.posn[-1] != pos:
              self.posn.append(pos)
              if len(self.posn) > TRACKMAX:
                  self.posn = self.posn[-TRACKMAX:]
              return 1
          return 0
  
      def track(self):
          '''Return the track as canvas drawing operations.'''
          return 'M(%d,%d); ' % self.posn[0] + ''.join(['L(%d,%d); ' % p for p in self.posn[1:]])
  
  class SatTracks(gps):
      '''gpsd client writing HTML5 and <canvas> output.'''
  
      def __init__(self):
          super(SatTracks, self).__init__()
          self.sattrack = {}      # maps PRNs to Tracks
          self.state = None
          self.statetimer = time.time()
          self.needsupdate = 0
  
      def html(self, fh, jsfile):
          fh.write("""<!DOCTYPE html>
  
  <html>
  <head>
  \t<meta http-equiv=Refresh content=300>
  \t<meta charset='utf-8'>
  \t<title>GPSD Satellite Positions and Readings</title>
  \t<style type='text/css'>
  \t\t.num td { text-align: right; }
  \t\tth { text-align: left; }
  \t</style>
  \t<script src='%s'></script>
  </head>
  <body>
  \t<table border=1>
  \t\t<tr>
  \t\t\t<td>
  \t\t\t\t<table border=0 class=num>
  \t\t\t\t\t<tr><th>PRN:</th><th>Elev:</th><th>Azim:</th><th>SNR:</th><th>Used:</th></tr>
  """ % jsfile)
  
          sats = self.satellites[:]
          sats.sort(key=lambda x: x.PRN)
          for s in sats:
              fh.write("\t\t\t\t\t<tr><td>%d</td><td>%d</td><td>%d</td><td>%d</td><td>%s</td></tr>\n" % (
                  s.PRN, s.elevation, s.azimuth, s.ss, s.used and 'Y' or 'N'
              ))
  
          fh.write("\t\t\t\t</table>\n\t\t\t\t<table border=0>\n")
  
          def row(l, v):
              fh.write("\t\t\t\t\t<tr><th>%s:</th><td>%s</td></tr>\n" % (l, v))
  
          def deg_to_str(a, hemi):
              return '%.6f %c' % (abs(a), hemi[a < 0])
  
          row('Time', self.utc or 'N/A')
  
          if self.fix.mode >= MODE_2D:
              row('Latitude', deg_to_str(self.fix.latitude, 'SN'))
              row('Longitude', deg_to_str(self.fix.longitude, 'WE'))
              row('Altitude', self.fix.mode == MODE_3D and "%f m" % self.fix.altitude or 'N/A')
              row('Speed', not isnan(self.fix.speed) and "%f m/s" % self.fix.speed or 'N/A')
              row('Course', not isnan(self.fix.track) and "%f°" % self.fix.track or 'N/A')
          else:
              row('Latitude', 'N/A')
              row('Longitude', 'N/A')
              row('Altitude', 'N/A')
              row('Speed', 'N/A')
              row('Course', 'N/A')
  
          row('EPX', not isnan(self.fix.epx) and "%f m" % self.fix.epx or 'N/A')
          row('EPY', not isnan(self.fix.epy) and "%f m" % self.fix.epy or 'N/A')
          row('EPV', not isnan(self.fix.epv) and "%f m" % self.fix.epv or 'N/A')
          row('Climb', self.fix.mode == MODE_3D and not isnan(self.fix.climb) and
              "%f m/s" % self.fix.climb or 'N/A'
          )
  
          if not (self.valid & ONLINE_SET):
              newstate = 0
              state = "OFFLINE"
          else:
              newstate = self.fix.mode
              if newstate == MODE_2D:
                  state = self.status == STATUS_DGPS_FIX and "2D DIFF FIX" or "2D FIX"
              elif newstate == MODE_3D:
                  state = self.status == STATUS_DGPS_FIX and "3D DIFF FIX" or "3D FIX"
              else:
                  state = "NO FIX"
          if newstate != self.state:
              self.statetimer = time.time()
              self.state = newstate
          row('State', state + " (%d secs)" % (time.time() - self.statetimer))
  
          fh.write("""\t\t\t\t</table>
  \t\t\t</td>
  \t\t\t<td>
  \t\t\t\t<canvas id=satview width=425 height=425>
  \t\t\t\t\t<p>Your browser needs HTML5 &lt;canvas> support to display the satellite view correctly.</p>
  \t\t\t\t</canvas>
  \t\t\t\t<script type='text/javascript'>draw_satview();</script>
  \t\t\t</td>
  \t\t</tr>
  \t</table>
  </body>
  </html>
  """)
  
      def js(self, fh):
          fh.write("""// draw the satellite view
  
  function draw_satview() {
      var c = document.getElementById('satview');
      if (!c.getContext) return;
      var ctx = c.getContext('2d');
      if (!ctx) return;
  
      var circle = Math.PI * 2,
          M = function (x, y) { ctx.moveTo(x, y); },
          L = function (x, y) { ctx.lineTo(x, y); };
  
      ctx.save();
      ctx.clearRect(0, 0, c.width, c.height);
      ctx.translate(210, 210);
  
      // grid and labels
      ctx.strokeStyle = 'black';
      ctx.beginPath();
      ctx.arc(0, 0, 200, 0, circle, 0);
      ctx.stroke();
  
      ctx.beginPath();
      ctx.strokeText('N', -4, -202);
      ctx.strokeText('W', -210, 4);
      ctx.strokeText('E', 202, 4);
      ctx.strokeText('S', -4, 210);
  
      ctx.strokeStyle = 'grey';
      ctx.beginPath();
      ctx.arc(0, 0, 100, 0, circle, 0);
      M(2, 0);
      ctx.arc(0, 0,   2, 0, circle, 0);
      ctx.stroke();
  
      ctx.strokeStyle = 'lightgrey';
      ctx.save();
      ctx.beginPath();
      M(0, -200); L(0, 200); ctx.rotate(circle / 8);
      M(0, -200); L(0, 200); ctx.rotate(circle / 8);
      M(0, -200); L(0, 200); ctx.rotate(circle / 8);
      M(0, -200); L(0, 200);
      ctx.stroke();
      ctx.restore();
  
      // tracks
      ctx.lineWidth = 0.6;
      ctx.strokeStyle = 'red';
  """);
  
          # Draw the tracks
          for t in self.sattrack.values():
              if t.posn:
                  fh.write("    ctx.globalAlpha = %s; ctx.beginPath(); %sctx.stroke();\n" % (
                      t.stale == 0 and '0.66' or '1', t.track()
                  ))
  
          fh.write("""
      // satellites
      ctx.lineWidth = 1;
      ctx.strokeStyle = 'black';
  """)
  
          # Draw the satellites
          for s in self.satellites:
              el, az = s.elevation, s.azimuth
              if el == 0 and az == 0:
                  continue  # Skip satellites with unknown position
              x, y = polartocart(el, az)
              fill = not s.used and 'lightgrey' or \
                  s.ss < 30 and 'red' or \
                  s.ss < 35 and 'yellow' or \
                  s.ss < 40 and 'green' or 'lime'
  
              # Center PRNs in the marker
              offset = s.PRN < 10 and 3 or s.PRN >= 100 and -3 or 0
  
              fh.write("    ctx.beginPath(); ctx.fillStyle = '%s'; " % fill)
              if s.PRN > 32:      # Draw a square for SBAS satellites
                  fh.write("ctx.rect(%d, %d, 16, 16); " % (x - 8, y - 8))
              else:
                  fh.write("ctx.arc(%d, %d, 8, 0, circle, 0); " % (x, y))
              fh.write("ctx.fill(); ctx.stroke(); ctx.strokeText('%s', %d, %d);\n" % (s.PRN, x - 6 + offset, y + 4))
  
          fh.write("""
      ctx.restore();
  }
  """)
  
      def make_stale(self):
          for t in self.sattrack.values():
              if t.stale:
                  t.stale -= 1
  
      def delete_stale(self):
          stales = []
          for prn in self.sattrack.keys():
              if self.sattrack[prn].stale == 0:
                  stales.append(prn)
                  self.needsupdate = 1
          for prn in stales:
              del self.sattrack[prn]
  
      def insert_sat(self, prn, x, y):
          try:
              t = self.sattrack[prn]
          except KeyError:
              self.sattrack[prn] = t = Track(prn)
          if t.add(x, y):
              self.needsupdate = 1
  
      def update_tracks(self):
          self.make_stale()
          for s in self.satellites:
              x, y = polartocart(s.elevation, s.azimuth)
              self.insert_sat(s.PRN, x, y)
          self.delete_stale()
  
      def generate_html(self, htmlfile, jsfile):
          fh = open(htmlfile, 'w')
          self.html(fh, jsfile)
          fh.close()
  
      def generate_js(self, jsfile):
          fh = open(jsfile, 'w')
          self.js(fh)
          fh.close()
  
      def run(self, suffix, period):
          jsfile = 'gpsd' + suffix + '.js'
          htmlfile = 'gpsd' + suffix + '.html'
          if period is not None:
              end = time.time() + period
          self.needsupdate = 1
          self.stream(WATCH_ENABLE | WATCH_NEWSTYLE)
          for report in self:
              if report['class'] not in ('TPV', 'SKY'):
                  continue
              self.update_tracks()
              if self.needsupdate:
                  self.generate_js(jsfile)
                  self.needsupdate = 0
              self.generate_html(htmlfile, jsfile)
              if period is not None and (
                  period <= 0 and self.fix.mode >= MODE_2D or
                  period > 0 and time.time() > end
              ):
                  break
  
  def main():
      argv = sys.argv[1:]
  
      factors = {
          's': 1, 'm': 60, 'h': 60 * 60, 'd': 24 * 60 * 60
      }
      arg = argv and argv[0] or '0'
      if arg[-1:] in factors.keys():
          period = int(arg[:-1]) * factors[arg[-1]]
      elif arg == 'c':
          period = None
      else:
          period = int(arg)
      prefix = '-' + arg
  
      sat = SatTracks()
  
      # restore the tracks
      pfile = 'tracks.p'
      if os.path.isfile(pfile):
          p = open(pfile, 'rb')
          try:
              sat.sattrack = pickle.load(p)
          except ValueError:
              print("Ignoring incompatible tracks file.", file=sys.stderr)
          p.close()
  
      try:
          sat.run(prefix, period)
      except KeyboardInterrupt:
          # save the tracks
          p = open(pfile, 'wb')
          # No protocol is backward-compatible from Python 3 to Python 2,
          # so we just use the default and punt at load time if needed.
          pickle.dump(sat.sattrack, p)
          p.close()
  
  if __name__ == '__main__':
      main()
  