#!/usr/bin/env python3
# encoding: utf-8
# This code is generated by scons.  Do not hand-hack it!
# 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

# Codacy D203 and D211 conflict, I choose D203
# Codacy D212 and D213 conflict, I choose D212

"""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.
"""

from __future__ import absolute_import, print_function, division

import math
import os
import sys
import time

from gps import *

gps_version = '3.22'
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):
    """Polartocart."""
    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):
        """Init class track."""
        self.prn = prn
        self.stale = 0
        self.posn = []          # list of (x, y) tuples

    def add(self, x, y):
        """Add track."""
        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):
        """Init class SatTracks."""
        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):
        """Writh HTML."""
        fh.write("""<!DOCTYPE html>

\t
\t
\tGPSD Satellite Positions and Readings
\t
\t

\t

\t\t PRN: Elev: Azim: SNR: Used:
""" % 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):
            """Fgetnans."""
            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(ln, v):
            """Write raw."""
            fh.write("\t\t<tr><th>%s:</th><td>%s</td></tr>\n" % (ln, v))

        def deg_to_str(a, hemi):
            """deg_to_str."""
            return '%.6f %c' % (abs(a), hemi[a < 0])

        def moderows(mode, table):
            """Moderows."""
            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

\t\t
Your browser needs HTML5 <canvas> support to display
\t\tthe satellite view correctly.
\t
\t
\t\tlight switch
\t

""")

    def js(self, fh):
        """Write the js."""
        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):
        """Nake_stale."""
        for t in self.sattrack.values():
            if t.stale:
                t.stale -= 1

    def delete_stale(self):
        """"Delete_stale."""
        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):
        """Insert_sat."""
        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):
        """update_tracks."""
        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):
        """Run."""
        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():
    """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()
