#!/usr/bin/env python
# encoding: utf-8
#
# azimuth-gtk is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as  published
# by the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# cw-kbd is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with azimuth-gtk.  If not, see <http://www.gnu.org/licenses/>.
#
# Copyright © 2011, Vernon Mauery (N7OH)
# Author: Vernon Mauery <vernon@mauery.org>


#
# This program expects a file called azimuth-elevation.log to be present
# in the current directory.  It continuously checks the file for updates.
# This being the case, it is best for the file to be truncated periodically
# so that not too much information is getting plotted.  Up to 12 hours of
# log data is about as much as you will want.  The first line of the log
# file is fixed length (so it can be overwritten) and has information about
# the latest status of the satellites.  All values are hex without leading 0x.
"""
4:c:20:09c12b7a:e5a3a2f6:0000272d:0050 17:8:25:a2 0a:8:1e:a2 10:8:21:a2 03:8:1f:a2 00:0:00:00 00:0:00:00 00:0:00:00 00:0:00:00
"""
# The fields are as follows:
# nr tracked:nr visible:latitude(in # milliarcseconds):longitude(mas):height(cm):DOP
# id(0-32):status(see mode2str below):Sound-Noise-Ratio:Stats
# previous entry repeated 11 more times, 00:0:00:00 if satellite is not visible
#
#
# The remaining lines plot the course of the satellites with azimuth/elevation
# data.  The data should be in ascii format, with each record using a single
# line:
"""
4dd81c0b 17:01f:4e 0d:12e:35 14:0b7:30 10:063:30 20:09e:1e 07:0ef:16 0a:13c:12 04:11e:12 1f:037:07 0
2:145:06 06:070:00 00:000:00
4dd81c29 17:021:4e 0d:12e:35 10:063:30 14:0b7:30 20:09e:1e 07:0ef:16 0a:13c:13 04:11e:12 1f:037:07 0
2:145:06 06:070:00 03:07f:00
"""
# The fields are as follows:
# timestamp (unix timestamp)
# id(0-32):azimuth(0-359):elevation(0-90)
# previous entry repeated 11 more times, 00:000:00 if satellite is not visible

# I have found that the easiest way to get this all working is to have
# a logger program talking to the GPS devices via serial port and then
# dumping the appropriate records to the file, periodically pruning it.
# Since the logging portion is device specific, I leave that to the user
# to implement (unless you also have a Navman/Lucent TU60-D120).

import pygtk
pygtk.require('2.0')
import gtk
import gobject
import math
import time
import struct

class Satellite_Info(object):
    def __init__(self, v):
        if len(v) == 4:
            self.id = int(v[0], 16)
            self.mode = int(v[1], 16)
            self.snr = int(v[2], 16)
            self.stats = int(v[3], 16)
            self.x = 0
            self.y = 0
            self.azimuth = 0
            self.elevation = 0
        elif len(v) == 5:
            self.id = v[0]
            self.azimuth = v[1]
            self.elevation = v[2]
            self.x = v[3]
            self.y = v[4]
            self.mode = 0
            self.snr = 0
            self.stats = 0
    def locked(self):
        return self.mode == 8

    def close_to(self, x, y):
        return math.sqrt((self.x-x)**2 + (self.y-y)**2) < 10

    def set_pos(self, x, y):
        self.x = x
        self.y = y

    def set_location(self, v):
        self.azimuth = int(v[0], 16)
        self.elevation = int(v[1], 16)

    def draw(self, widget):
        if self.locked():
            ftype = 2
        else:
            ftype = 1
        widget.draw_figure(self.x, self.y, widget.colors[self.id], ftype)

    def __eq__(self, other):
        return self.id == other.id and self.mode == other.mode and \
               self.x == other.x and self.y == other.y

    def __str__(self):
        mode2str = {
            0:"Code search",
            1:"Code acquire",
            2:"AGC set",
            3:"Preq acquire",
            4:"Bit sync detect",
            5:"Msg sync detect",
            6:"Time lock",
            7:"Ephemeris acquire",
            8:"Position+Time lock",
        }
        return "Satellite %d: az: %d, el: %d, mode: '%s', C/No=%d dB-Hz, status=0x%x" % (
                self.id, self.azimuth, self.elevation,
                mode2str[self.mode], self.snr, self.stats)

class azimuth(object):
    def __init__(self, box):
        self.azimuth_factor = 1
        self.azimuth = {}
        self.calculate_azimuth_table()
        # Backing pixmap for drawing area
        self.widget = None
        self.pixmap = None
        self.points = {}
        self.last_line = 1
        self.gc = None
        self.colors = {}
        self.sat_info = {}

        # Create the drawing area
        self.widget = gtk.DrawingArea()
        self.widget.set_size_request(600, 600)
        box.pack_start(self.widget, True, True, 0)
        self.info = gtk.Statusbar()
        self.gps_info_id = self.info.get_context_id("GPS info")
        self.info.push(self.gps_info_id, "GPS Satellite Tracker")
        self.sat_info_id = self.info.get_context_id("Satellite info")
        box.pack_end(self.info, False, False, 0)
        self.hoversat = False

        self.widget.show()
        self.info.show()

        # Signals used to handle backing pixmap
        self.widget.connect("expose_event", self.expose_event)
        self.widget.connect("motion_notify_event", self.mouse_motion)
        self.widget.connect("configure_event", self.configure_event)
        self.widget.set_events(gtk.gdk.EXPOSURE_MASK
                             | gtk.gdk.POINTER_MOTION_MASK
                             | gtk.gdk.POINTER_MOTION_HINT_MASK)

        update_id = gobject.timeout_add(1000, self.update_drawing)

    def calculate_azimuth_table(self):
        c=16500
        b=4000
        for el in range(91):
            C = math.pi * (el+90) / 180.0
            bcosC = b * math.cos(C)
            a = bcosC + math.sqrt(bcosC**2 + c**2 - b**2)
            A = (math.pi/2)-math.asin(a*math.sin(C)/c)
            self.azimuth[el] = A
            if self.azimuth_factor == 1:
                self.azimuth_factor = 1.0/math.cos(A)

    def mouse_motion(self, widget, event):
        # find the satellite near event.x,event.y
        for s in self.sat_info:
            if self.sat_info[s].close_to(event.x, event.y):
                #print self.sat_info[s]
                self.info.pop(self.sat_info_id)
                self.info.push(self.sat_info_id, str(self.sat_info[s]))
                self.hoversat = True
                return True
        self.info.pop(self.sat_info_id)
        self.hoversat = False
        return True

    # Draw a rectangle on the screen
    def draw_figure(self, x, y, color, ftype):
        self.gc.set_foreground(color)
        if ftype == 0:
            pts = [(x-8,y+6), (x+8,y+6), (x, y-6)]
            self.pixmap.draw_polygon(self.gc, True, pts)
            return
        elif ftype == 2:
            self.gc.set_foreground(self.colors['white'])
            self.pixmap.draw_arc(self.gc, False, x-9, y-9, 17, 17, 0, 360*64)
            self.gc.set_foreground(color)
        rect = (int(x-5), int(y-5), 10, 10)
        self.pixmap.draw_rectangle(self.gc, True, rect[0], rect[1], rect[2], rect[3])

    def location_update(self, info):
        gps_info = {}
        gps_info['tracked'] = int(info[0], 16)
        gps_info['visible'] = int(info[1], 16)
        gps_info['stats'] = int(info[2], 16)
        gps_info['dop'] = int(info[6], 16)/10
        l = struct.unpack('=l', struct.pack('=L', int(info[3], 16)))[0]
        if l < 0:
            l = -l
            sign='-'
        else:
            sign='+'
        gps_info['latitude'] = "%d %d'%d.%03d\"" % \
                (l/3600000, (l%3600000)/60000, (l%60000)/1000, l%1000)
        l = struct.unpack('=l', struct.pack('=L', int(info[4], 16)))[0]
        if l < 0:
            l = -l
            sign='-'
        else:
            sign='+'
        gps_info['longitude'] = "%s%d %d'%d.%03d\"" % \
                (sign, l/3600000, (l%3600000)/60000, (l%60000)/1000, l%1000)
        gps_info['height'] = "%dm" % (int(info[5], 16)/100)
        if gps_info['stats'] & 0x10:
            gps_info['dfix'] = '2d-fix'
        elif gps_info['stats'] & 0x20:
            gps_info['dfix'] = '3d-fix'
        else:
            gps_info['dfix'] = '0d-fix'
        self.info.pop(self.gps_info_id)
        self.info.push(self.gps_info_id, "Position: %(latitude)s,%(longitude)s,%(height)s "\
            "%(dfix)s, tracking %(tracked)d/%(visible)d satellites, DOP: %(dop)d" % \
            gps_info)

    def draw_contents(self):
        x, y, width, height = self.widget.get_allocation()
        xoff = (width-self.dim)/2
        yoff = (height-self.dim)/2
        hdim = self.dim/2

        sat_update = False
        log = open("azimuth-elevation.log", "r").readlines()
        if self.last_line > len(log):
            self.last_line = 1
            self.points = {}

        # plot the last satellite locations
        locked = {}
        sat_info = {}
        satlock = log[0].split()
        for s in satlock[1:]:
            s = s.split(':')
            ids = int(s[0], 16)
            if ids == 0:
                break
            sat_info[ids] = Satellite_Info(s)
        sats = log[-1]
        sats = sats.split()
        for s in sats[1:]:
            s = s.split(':')
            ids = int(s[0], 16)
            if ids == 0:
                break;
            az = math.pi * int(s[1], 16) / 180.0
            el = hdim * math.cos(self.azimuth[int(s[2], 16)])
            x = xoff + hdim + int(0.5 + math.sin(az) * el *self.azimuth_factor)
            y = yoff + hdim - int(0.5 + math.cos(az) * el *self.azimuth_factor)
            if ids not in sat_info:
                sat_info[ids] = Satellite_Info((ids,int(s[1],16),int(s[2], 16), x, y))
            else:
                sat_info[ids].set_pos(x,y)
                sat_info[ids].set_location(s[1:])

        if sat_info == self.sat_info:
            sat_update = False
        else:
            sat_update = True
            self.sat_info = sat_info
        if not self.hoversat:
            self.location_update(satlock[0].split(':'))

        lcount = 0
        st = time.time()
        for l in log[self.last_line:]:
            self.last_line += 1
            sats = l.split()
            for s in sats[1:]:
                s = s.split(':')
                ids = int(s[0], 16)
                if ids == 0:
                    break;
                if ids not in self.points:
                    self.points[ids] = []
                az = math.pi * int(s[1], 16) / 180.0
                el = hdim * math.cos(self.azimuth[int(s[2], 16)])
                x = xoff + hdim + int(0.5 + math.sin(az) * el * self.azimuth_factor)
                y = yoff + hdim - int(0.5 + math.cos(az) * el * self.azimuth_factor)
                if (x,y) not in self.points[ids]:
                    self.points[ids].append((x,y))
                    self.points[ids].append((x+1,y))
                    self.points[ids].append((x,y+1))
                    self.points[ids].append((x-1,y))
                    self.points[ids].append((x,y-1))
                lcount += 1
        if lcount or sat_update:
            self.pixmap.draw_rectangle(self.widget.get_style().black_gc,
                                  True, 0, 0, width, height)
            self.pixmap.draw_arc(self.widget.get_style().white_gc, False,
                            xoff, yoff, self.dim-1, self.dim-1, 0, 64*360)
            lcount = 0
            for c in self.points:
                lcount += len(self.points[c])
                self.gc.set_foreground(self.colors[c])
                self.pixmap.draw_points(self.gc, self.points[c])

            sats = log[-1]
            sats = sats.split()
            for s in self.sat_info:
                self.sat_info[s].draw(self)
            self.draw_figure(xoff+hdim, yoff+hdim, self.colors['white'], 0)

            # draw colors at top
            #for i in range(33):
            #    self.gc.set_foreground(self.colors[i])
            #    rect = [((i*width/34), 0), (((i+1)*width/34), 0),
            #            (((i+1)*width/34), 15), ((i*width/34), 15)]
            #    self.pixmap.draw_polygon(self.gc, True, rect)
            self.widget.queue_draw_area(0, 0, width, height)

            et = time.time()
            #print "drew %d dots (total: %f seconds)" % (lcount, et-st)

    # Create a new backing pixmap of the appropriate size
    def configure_event(self, widget, event):
        x, y, width, height = self.widget.get_allocation()
        self.dim = min(width,height)
        self.pixmap = gtk.gdk.Pixmap(self.widget.window, width, height)
        self.points = {}
        self.last_line = 1
        self.gc = self.pixmap.new_gc()
        cm = self.gc.get_colormap()
        self.colors[0]  = cm.alloc_color(0x0000, 0x0000, 0x0000)
        self.colors[1]  = cm.alloc_color(0x5555, 0x0000, 0x0000)
        self.colors[2]  = cm.alloc_color(0xaaaa, 0x0000, 0x0000)
        self.colors[3]  = cm.alloc_color(0xffff, 0x0000, 0x0000)
        self.colors[4]  = cm.alloc_color(0x0000, 0x7fff, 0x0000)
        self.colors[5]  = cm.alloc_color(0x5555, 0x7fff, 0x0000)
        self.colors[6]  = cm.alloc_color(0xaaaa, 0x7fff, 0x0000)
        self.colors[7]  = cm.alloc_color(0xffff, 0x7fff, 0x0000)
        self.colors[8]  = cm.alloc_color(0x0000, 0xffff, 0x0000)
        self.colors[9]  = cm.alloc_color(0x5555, 0xffff, 0xffff)
        self.colors[10] = cm.alloc_color(0xaaaa, 0xffff, 0x0000)
        self.colors[11] = cm.alloc_color(0xffff, 0xffff, 0x0000)
        self.colors[12] = cm.alloc_color(0x0000, 0x0000, 0x7fff)
        self.colors[13] = cm.alloc_color(0x5555, 0x0000, 0x7fff)
        self.colors[14] = cm.alloc_color(0xaaaa, 0x0000, 0x7fff)
        self.colors[15] = cm.alloc_color(0xffff, 0x0000, 0x7fff)
        self.colors[16] = cm.alloc_color(0x0000, 0x7fff, 0x7fff)
        self.colors[17] = cm.alloc_color(0xaaaa, 0xffff, 0xffff)
        self.colors[18] = cm.alloc_color(0xaaaa, 0x7fff, 0x7fff)
        self.colors[19] = cm.alloc_color(0xffff, 0x7fff, 0x7fff)
        self.colors[20] = cm.alloc_color(0x0000, 0xffff, 0x7fff)
        self.colors[21] = cm.alloc_color(0x5555, 0xffff, 0x7fff)
        self.colors[22] = cm.alloc_color(0xaaaa, 0xffff, 0x7fff)
        self.colors[23] = cm.alloc_color(0xffff, 0xffff, 0x7fff)
        self.colors[24] = cm.alloc_color(0x0000, 0x0000, 0xffff)
        self.colors[25] = cm.alloc_color(0x5555, 0x0000, 0xffff)
        self.colors[26] = cm.alloc_color(0xaaaa, 0x0000, 0xffff)
        self.colors[27] = cm.alloc_color(0xffff, 0x0000, 0xffff)
        self.colors[28] = cm.alloc_color(0x0000, 0x7fff, 0xffff)
        self.colors[29] = cm.alloc_color(0x5555, 0x7fff, 0xffff)
        self.colors[30] = cm.alloc_color(0xaaaa, 0x7fff, 0xffff)
        self.colors[31] = cm.alloc_color(0xffff, 0x7fff, 0xffff)
        self.colors[32] = cm.alloc_color(0x0000, 0xffff, 0xffff)
        self.colors['white'] = cm.alloc_color('white')
        self.colors['black'] = cm.alloc_color('black')
        self.draw_contents()
        return True

    # Redraw the screen from the backing pixmap
    def expose_event(self, widget, event):
        x , y, width, height = event.area
        self.widget.window.draw_drawable(self.widget.get_style().fg_gc[gtk.STATE_NORMAL],
                                    self.pixmap, x, y, x, y, width, height)
        return False

    def update_drawing(self):
        update_id = gobject.timeout_add(1000, self.update_drawing)
        log_file = open("azimuth-elevation.log", "r")
        self.draw_contents()
        log_file.close()


def main():
    window = gtk.Window(gtk.WINDOW_TOPLEVEL)
    window.set_name ("Test Input")

    vbox = gtk.VBox(False, 0)
    window.add(vbox)
    vbox.show()

    az = azimuth(vbox)

    window.connect("destroy", lambda w: gtk.main_quit())

    window.show()

    try:
        gtk.main()
    except KeyboardInterrupt:
        pass

    return 0

if __name__ == "__main__":
    main()
