#!/usr/bin/env python
# encoding: utf-8
# This code is generated by scons.  Do not hand-hack it!

"""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 seconds, '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.j". This is a JSON 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.
"""

# This code runs compatibly under Python 2 and 3.x for x >= 2.
# Preserve this property!
#
# This file is Copyright 2010 by the GPSD project
# SPDX-License-Identifier: BSD-2-clause

from __future__ import absolute_import, print_function, division

import math
import os
import pickle
import sys
import time

from gps import *

gps_version = '3.21'
if gps.__version__ != gps_version:
    sys.stderr.write("webgps.py: ERROR: need gps module version %s, got %s\n" %
                     (gps_version, gps.__version__))
    sys.exit(1)


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.'''
        mid_thing = ');L('
        return('M(%s);' % mid_thing.join(
                        map(lambda x: '%d,%d' % (x[0], x[1]), self.posn)))


class SatTracks(gps):
    '''gpsd client writing HTML5 and <canvas> output.'''

    def __init__(self):
        super(SatTracks, self).__init__()
        self.backing = {}
        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 lang="en"><head>
\t<meta http-equiv="Refresh" content="300">
\t<meta charset='utf-8'>
\t<title>GPSD Satellite Positions and Readings</title>
\t<style>
\t\t.num td { text-align: right; }
\t\tth { text-align: left; }
\t\ttable {
\t\t\tborder: none;
\t\t\tborder-collapse: collapse;
\t\t}
\t\tbody {
\t\t\tcolor: #839496;
\t\t\tbackground: #fdf6e3;
\t\t\tdisplay: grid;
\t\t\tgrid-gap: 10px;
\t\t}
\t\t.dark  {
\t\t\tcolor: #839496;
\t\t\tbackground: #002b36;
\t\t}
\t\t@media only screen and (min-width: 900px) {
\t\t\tbody {
\t\t\t\tgrid-template-areas:
\t\t\t\t\t'tpv satview sky'
\t\t\t\t\t'misc satview sky'
\t\t\t\t\t'light ep dop';
\t\t\t}
\t\t}
\t\t@media only screen and (min-width: 680px) and (max-width: 899px) {
\t\t\tbody {
\t\t\t\tgrid-template-areas:
\t\t\t\t'sky satview'
\t\t\t\t'tpv satview'
\t\t\t\t'dop ep'
\t\t\t\t'light misc';
\t\t\t}
\t\t}
\t\t@media only screen and (min-width: 460px) and (max-width: 679px) {
\t\t\tbody {
\t\t\t\tgrid-template-areas:
\t\t\t\t'satview'
\t\t\t\t'tpv'
\t\t\t\t'sky'
\t\t\t\t'misc'
\t\t\t\t'ep'
\t\t\t\t'dop'
\t\t\t\t'light';
\t\t\t}
\t\t}
\t\t@media only screen and (max-width: 459px) {
\t\t\t#satview {
\t\t\t\tdisplay: none
\t\t\t}
\t\t\tbody {
\t\t\t\tgrid-template-areas:
\t\t\t\t'tpv'
\t\t\t\t'sky'
\t\t\t\t'misc'
\t\t\t\t'ep'
\t\t\t\t'dop'
\t\t\t\t'light';
\t\t\t}
\t\t}
\t\t.wide td:nth-child(odd),
\t\t.tall tr:nth-child(6n),
\t\t.tall tr:nth-child(6n+4),
\t\t.tall tr:nth-child(6n+5) {
\t\t\tbackground: rgba(131,148,150,0.25);
\t\t}
\t</style>
\t<script src='%s'></script>
</head><body onload="draw_satview();">
\t<div style="grid-area: sky;"><table class="num tall">
\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<tr><td>%d</td><td>%s</td><td>%s</td>"
                     "<td>%d</td><td>%s</td></tr>\n" %
                     (s.PRN,
                      (-10 <= s.elevation <= 90) and s.elevation or 'N/A',
                      (0 <= s.azimuth < 360) and s.azimuth or 'N/A',
                      s.ss, s.used and 'Y' or 'N'))
        fh.write('\t</table></div>\n')

        def fgetnans(parent, children):
            result = []
            for child in children:
                k = parent.get(child, float('nan'))
                result.append(isfinite(k) and str(k) or 'N/A')
            return result

        s = dict.get(self.backing, 'SKY', None)
        if s:
            dops = 'x y h v p t g'.split(" ")
            fh.write('\t<div style="grid-area: dop;"><table class="wide">\n'
                     '\t\t<tr><th colspan="7">Dilution Of Precision</th>'
                     '</tr>\n')
            fh.write('\t\t<tr><th>' + '</th><th>'.join(dops) + '</th></tr>\n')

            result = fgetnans(s, map(lambda e: e+"dop", dops))
            fh.write('\t\t<tr class="num"><td>%s</td></tr>\n' %
                     '</td><td>'.join(result))
            fh.write('\t</table></div>\n')

        s = dict.get(self.backing, 'TPV', None)
        if s:
            eps = 'x y v c s d t'.split(" ")
            fh.write('\t<div style="grid-area: ep;"><table class="wide">\n')
            fh.write('\t\t<tr><th colspan="7">Estimated Precision</th></tr>\n')
            fh.write('\t\t<tr><th>' + '</th><th>'.join(eps) + '</th></tr>\n')
            result = fgetnans(s, map(lambda e: "ep"+e, eps))
            fh.write('\t\t<tr class="num"><td>%s m</td><td>%s m</td>'
                     '<td>%s m/s</td><td>%s m/s</td>'
                     '<td>%s m</td><td>%s deg</td><td>%s !s</td></tr>\n' %
                     (result[0], result[1], result[2], result[3], result[4],
                      result[5], result[6]))
            fh.write('\t</table></div>\n')

        fh.write('\t<div style="grid-area: misc;">\n')
        if 'TOFF' in self.backing:
            s = self.backing['TOFF']
            ns = int((int(s.real_sec) - int(s.clock_sec)) * 1000000000
                     + (int(s.real_nsec) - int(s.clock_nsec)))
            fh.write('\t\tTime OFFset: %f ms<br/>\n' % (float(ns)/1.0e6))
        if 'PPS' in self.backing:
            s = self.backing['PPS']
            ns = int((int(s.real_sec) - int(s.clock_sec)) * 1000000000
                     + (int(s.real_nsec) - int(s.clock_nsec)))
            fh.write('\t\tPPS offset: %d us<br/>\n' % (float(ns)/1.0e3))
            fh.write('\t\tPPS precision: %f<br/>\n' % s.precision)
            qerr = s.get('qErr', float('nan'))
            fh.write(isfinite(qerr) and ("\t\tPPS sawtooth %fps<br/>\n"
                     % qerr) or "")
        if 'DEVICES' in self.backing:
            for (index, value) in enumerate(self.backing['DEVICES']['devices']):
                for key in value:
                    fh.write('\t\t[%d]%s: %s<br/>\n' %
                             (index, key, value[key]))
        if 'OSC' in self.backing:
            s = self.backing['OSC']
            fh.write('\t\tOscillator')
            fh.write(': %s running' % (s['running'] and '' or 'not'))
            fh.write(' with%s GPS PPS' % (s['reference'] and '' or 'out'))
            fh.write('and is %s disciplined' %
                     (s['disciplined'] and '' or 'not'))
            fh.write('<br/>\n')
            fh.write('Oscillator delta: %fns<br>\n' % s['delta'])
        fh.write('\t</div>\n')

        def row(l, v):
            fh.write("\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])

        def moderows(mode, table):
            for line in table:
                if line[0] > mode or not isfinite(line[3]):
                    row(line[1], 'N/A')
                    continue
                row(line[1], line[2] % line[3])

        fh.write('\t<div style="grid-area: tpv;"><table class="tall">\n')
        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'))
        else:
            row('Latitude', 'N/A')
            row('Longitude', 'N/A')
        moderows(self.fix.mode, [
                                 [3, 'altHAE', '%f m',    self.fix.altHAE],
                                 [3, 'altMSL', '%f m',    self.fix.altMSL],
                                 [2, 'Speed',  '%f m/s',  self.fix.speed],
                                 [2, 'Course', '%f&deg;', self.fix.track],
                                 [3, 'Climb',  '%f m/s',  self.fix.climb],
                                 ])

        state = "INIT"
        if not (self.valid & ONLINE_SET):
            newstate = 0
            state = "OFFLINE"
        else:
            newstate = self.fix.mode
            if newstate == MODE_2D:
                state = "2D FIX"
            elif newstate == MODE_3D:
                state = "3D FIX"
            else:
                state = "NO FIX"
        if newstate != self.state:
            self.statetimer = time.time()
            self.state = newstate
        row('State', "%s (%d secs)" % (state, time.time() - self.statetimer))

        fh.write("""\t</table></div>
\t<div style="grid-area: satview;"
><canvas id="satview" width="425" height="425">
\t\t<p>Your browser needs HTML5 &lt;canvas&gt; support to display
\t\tthe satellite view correctly.</p>
\t</canvas></div>
\t<div style="grid-area: light">
\t\t<button onclick="document.body.classList.toggle('dark')"
 >light switch</button>
\t</div>
</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 = '#839496';
    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.beginPath();
    ctx.arc(0, 0, 100, 0, circle, 0);
    M(2, 0);
    ctx.arc(0, 0,   2, 0, circle, 0);
    ctx.stroke();

    ctx.save();
    ctx.beginPath();
    M(0, -200); L(0, 200);
    M(-200, 0); L(200, 0); ctx.rotate(circle / 8);
    M(0, -200); L(0, 200);
    M(-200, 0); L(200, 0);
    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 = '#839496';
""")

        # 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 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 | WATCH_PPS)
        for report in self:
            self.backing[report['class']] = report
            if report['class'] not in ('TPV', 'SKY'):
                continue
            self.update_tracks()
            if self.needsupdate:
                with open(jsfile, 'w') as jfh:
                    self.js(jfh)
                    self.needsupdate = 0
            with open(htmlfile, 'w') as hfh:
                self.html(hfh, 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
    jfile = 'tracks.j'
    if os.path.isfile(jfile):
        with open(jfile, 'r') as j:
            try:
                dictionary = json.load(j)
                for t in dictionary.values():
                    prn = t['prn']
                    sat.sattrack[prn] = Track(prn)
                    sat.sattrack[prn].stale = t['stale']
                    sat.sattrack[prn].posn = t['posn']
            except ValueError:
                print("tracker.py WARNING: Ignoring incompatible tracks file.",
                      file=sys.stderr)
    try:
        sat.run(prefix, period)
    except KeyboardInterrupt:
        # save the tracks
        with open(jfile, 'w') as j:
            dictionary = {}
            for t in sat.sattrack.values():
                dictionary[t.prn] = dict(prn=t.prn, stale=t.stale, posn=t.posn)
            json.dump(dictionary, j)
            print("tracker.py INFORMATION: saving state", file=sys.stderr)


if __name__ == '__main__':
    main()
