#!/usr/local/bin/python3.8
#
# by
# Robin Wittler <real@the-real.org> (speedometer mode)
# and
# Chen Wei <weichen302@gmx.com> (nautical mode)
#
# SPDX-License-Identifier: BSD-2-clause

# This code runs compatibly under Python 2 and 3.x for x >= 2.
# Preserve this property!
from __future__ import absolute_import, print_function, division

from math import pi
from math import cos
from math import sin
from math import sqrt
from math import radians
from socket import error as SocketError

import cairo

# Gtk3 imports.  Gtk3 requires the require_version(), which then causes
# pylint to complain about the subsequent "non-top" imports.
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk      # pylint: disable=wrong-import-position
from gi.repository import Gdk      # pylint: disable=wrong-import-position
from gi.repository import GObject  # pylint: disable=wrong-import-position

# pylint wants local modules last
try:
    import gps
except ImportError as e:
    sys.stderr.write(
        "xgpsspeed: can't load Python gps libraries -- check PYTHONPATH.\n")
    sys.stderr.write("%s\n" % e)
    sys.exit(1)

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


class Speedometer(Gtk.DrawingArea):
    "Speedometer class"

    def __init__(self, speed_unit=None):
        "Init Speedometer class"
        Gtk.DrawingArea.__init__(self)
        self.MPH_UNIT_LABEL = 'mph'
        self.KPH_UNIT_LABEL = 'kmh'
        self.KNOTS_UNIT_LABEL = 'knots'
        self.conversions = {
            self.MPH_UNIT_LABEL: gps.MPS_TO_MPH,
            self.KPH_UNIT_LABEL: gps.MPS_TO_KPH,
            self.KNOTS_UNIT_LABEL: gps.MPS_TO_KNOTS
        }
        self.speed_unit = speed_unit or self.MPH_UNIT_LABEL
        if self.speed_unit not in self.conversions:
            raise TypeError(
                '%s is not a valid speed unit'
                % (repr(speed_unit))
            )


class LandSpeedometer(Speedometer):
    "LandSpeedometer class"

    def __init__(self, speed_unit=None):
        "Init LandSpeedometer class"
        Speedometer.__init__(self, speed_unit)
        self.connect('size-allocate', self.on_size_allocate)
        self.width = self.height = 0
        self.connect('draw', self.draw_s)
        self.long_ticks = (2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8)
        self.short_ticks = (0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9)
        self.long_inset = lambda x: 0.1 * x
        self.middle_inset = lambda x: self.long_inset(x) / 1.5
        self.short_inset = lambda x: self.long_inset(x) / 3
        self.res_div = 10.0
        self.res_div_mul = 1
        self.last_speed = 0
        self.nums = {
            -8: 0,
            -7: 10,
            -6: 20,
            -5: 30,
            -4: 40,
            -3: 50,
            -2: 60,
            -1: 70,
            0: 80,
            1: 90,
            2: 100
        }

    def on_size_allocate(self, _unused, allocation):
        self.width = allocation.width
        self.height = allocation.height

    def draw_s(self, _unused, _event, _empty=None):
        self.cr = self.get_window().cairo_create()
        self.cr.rectangle(0, 0, self.width, self.height)
        self.cr.clip()
        x, y = self.get_x_y()
        width, height = self.get_window().get_geometry()[2:4]
        radius = self.get_radius(width, height)
        self.cr.set_line_width(radius / 100)
        self.draw_arc_and_ticks(width, height, radius, x, y)
        self.draw_needle(self.last_speed, radius, x, y)
        self.draw_speed_text(self.last_speed, radius, x, y)

    def draw_arc_and_ticks(self, width, height, radius, x, y):
        self.cr.set_source_rgb(1.0, 1.0, 1.0)
        self.cr.rectangle(0, 0, width, height)
        self.cr.fill()
        self.cr.set_source_rgb(0.0, 0.0, 0.0)

        # draw the speedometer arc
        self.cr.arc_negative(x, y, radius, radians(60), radians(120))
        self.cr.stroke()
        long_inset = self.long_inset(radius)
        middle_inset = self.middle_inset(radius)
        short_inset = self.short_inset(radius)

        # draw the ticks
        for i in self.long_ticks:
            self.cr.move_to(
                x + (radius - long_inset) * cos(i * pi / 6.0),
                y + (radius - long_inset) * sin(i * pi / 6.0)
            )
            self.cr.line_to(
                (x + (radius + (self.cr.get_line_width() / 2)) *
                 cos(i * pi / 6.0)),
                (y + (radius + (self.cr.get_line_width() / 2)) *
                 sin(i * pi / 6.0))
            )
            self.cr.select_font_face(
                'Georgia',
                cairo.FONT_SLANT_NORMAL,
            )
            self.cr.set_font_size(radius / 10)
            self.cr.save()
            _num = str(self.nums.get(i) * self.res_div_mul)
            (
                _x_bearing,
                _y_bearing,
                t_width,
                t_height,
                _x_advance,
                _y_advance
            ) = self.cr.text_extents(_num)

            if i in (-8, -7, -6, -5, -4):
                self.cr.move_to(
                    (x + (radius - long_inset - (t_width / 2)) *
                     cos(i * pi / 6.0)),
                    (y + (radius - long_inset - (t_height * 2)) *
                     sin(i * pi / 6.0))
                )
            elif i in (-2, -1, 0, 2, 1):
                self.cr.move_to(
                    (x + (radius - long_inset - (t_width * 1.5)) *
                     cos(i * pi / 6.0)),
                    (y + (radius - long_inset - (t_height * 2)) *
                     sin(i * pi / 6.0))
                )
            elif i in (-3,):
                self.cr.move_to(
                    (x - t_width / 2),
                    (y - radius + self.long_inset(radius) * 2 + t_height)
                )
            self.cr.show_text(_num)
            self.cr.restore()

            if i != self.long_ticks[0]:
                self.cr.move_to(
                    x + (radius - middle_inset) * cos((i + 0.5) * pi / 6.0),
                    y + (radius - middle_inset) * sin((i + 0.5) * pi / 6.0)
                )
                self.cr.line_to(
                    x + (radius + (self.cr.get_line_width() / 2)) *
                    cos((i + 0.5) * pi / 6.0),
                    y + (radius + (self.cr.get_line_width() / 2)) *
                    sin((i + 0.5) * pi / 6.0)
                )

            for z in self.short_ticks:
                w_half = self.cr.get_line_width() / 2
                if i < 0:
                    self.cr.move_to(
                        x + (radius - short_inset) * cos((i + z) * pi / 6.0),
                        y + (radius - short_inset) * sin((i + z) * pi / 6.0)
                    )
                    self.cr.line_to(
                        x + (radius + w_half) * cos((i + z) * pi / 6.0),
                        y + (radius + w_half) * sin((i + z) * pi / 6.0)
                    )
                else:
                    self.cr.move_to(
                        x + (radius - short_inset) * cos((i - z) * pi / 6.0),
                        y + (radius - short_inset) * sin((i - z) * pi / 6.0)
                    )
                    self.cr.line_to(
                        x + (radius + w_half) * cos((i - z) * pi / 6.0),
                        y + (radius + w_half) * sin((i - z) * pi / 6.0)
                    )
            self.cr.stroke()

    def draw_needle(self, speed, radius, x, y):
        self.cr.save()
        inset = self.long_inset(radius)
        speed = speed * self.conversions.get(self.speed_unit)
        speed = speed / (self.res_div * self.res_div_mul)
        actual = self.long_ticks[-1] + speed
        if actual > self.long_ticks[0]:
            self.res_div_mul += 1
            speed = speed / (self.res_div * self.res_div_mul)
            actual = self.long_ticks[-1] + speed
        self.cr.move_to(x, y)
        self.cr.line_to(
            x + (radius - (2 * inset)) * cos(actual * pi / 6.0),
            y + (radius - (2 * inset)) * sin(actual * pi / 6.0)
        )
        self.cr.stroke()
        self.cr.restore()

    def draw_speed_text(self, speed, radius, x, y):
        self.cr.save()
        speed = '%.2f %s' % (
                speed * self.conversions.get(self.speed_unit),
                self.speed_unit
        )
        self.cr.select_font_face(
            'Georgia',
            cairo.FONT_SLANT_NORMAL,
            # cairo.FONT_WEIGHT_BOLD
        )
        self.cr.set_font_size(radius / 10)
        _x_bearing, _y_bearing, t_width, _t_height = \
            self.cr.text_extents(speed)[:4]
        self.cr.move_to((x - t_width / 2),
                        (y + radius) - self.long_inset(radius))
        self.cr.show_text(speed)
        self.cr.restore()

    def get_x_y(self):
        rect = self.get_allocation()
        x = (rect.x + rect.width / 2.0)
        y = (rect.y + rect.height / 2.0) - 20
        return x, y

    def get_radius(self, width, height):
        return min(width / 2.0, height / 2.0) - 20


class NauticalSpeedometer(Speedometer):
    "NauticalSpeedometer class"
    HEADING_SAT_GAP = 0.8
    SAT_SIZE = 10  # radius of the satellite circle in skyview

    def __init__(self, speed_unit=None, maxspeed=100, rotate=0.0):
        Speedometer.__init__(self, speed_unit)
        self.connect('size-allocate', self.on_size_allocate)
        self.width = self.height = 0
        self.connect('draw', self.draw_s)
        self.long_inset = lambda x: 0.05 * x
        self.mid_inset = lambda x: self.long_inset(x) / 1.5
        self.short_inset = lambda x: self.long_inset(x) / 3
        self.last_speed = 0
        self.satellites = []
        self.last_heading = 0
        self.maxspeed = int(maxspeed)
        self.rotate = radians(rotate)

    def polar2xy(self, radius, angle, polex, poley):
        '''convert Polar coordinate to Cartesian coordinate system
        the y axis in pygtk points downward
        Args:
           radius:
           angle: azimuth from from Polar coordinate system, in radian
           polex and poley are the Cartesian coordinate of the pole
        return a tuple contains (x, y)'''
        return (polex + cos(angle) * radius, poley - sin(angle) * radius)

    def polar2xyr(self, radius, angle, polex, poley):
        '''Version of polar2xy that includes rotation'''
        angle = (angle + self.rotate) % (pi * 2)  # Note reversed sense
        return self.polar2xy(radius, angle, polex, poley)

    def on_size_allocate(self, _unused, allocation):
        self.width = allocation.width
        self.height = allocation.height

    def draw_s(self, _unused, _event, _empty=None):
        self.cr = self.get_window().cairo_create()
        self.cr.rectangle(0, 0, self.width, self.height)
        self.cr.clip()
        x, y = self.get_x_y()
        width, height = self.get_window().get_geometry()[2:4]
        radius = self.get_radius(width, height)
        self.cr.set_line_width(radius / 100)
        self.draw_arc_and_ticks(width, height, radius, x, y)
        self.draw_heading(20, self.last_heading, radius, x, y)
        for sat in self.satellites:
            self.draw_sat(sat, radius * NauticalSpeedometer.HEADING_SAT_GAP,
                          x, y)
        self.draw_speed(radius, x, y)

    def draw_text(self, x, y, text, fontsize=10):
        '''draw text at given location
        Args:
            x, y is the center of textbox'''
        txt = str(text)
        self.cr.new_sub_path()
        self.cr.set_source_rgba(0, 0, 0)
        self.cr.select_font_face('Sans',
                                 cairo.FONT_SLANT_NORMAL,
                                 cairo.FONT_WEIGHT_BOLD)
        self.cr.set_font_size(fontsize)
        (_x_bearing, _y_bearing,
         t_width, t_height) = self.cr.text_extents(txt)[:4]
        # set the center of textbox
        self.cr.move_to(x - t_width / 2, y + t_height / 2)
        self.cr.show_text(txt)

    def draw_arc_and_ticks(self, width, height, radius, x, y):
        '''Draw a serial of circle, with ticks in outmost circle'''

        self.cr.set_source_rgb(1.0, 1.0, 1.0)
        self.cr.rectangle(0, 0, width, height)
        self.cr.fill()
        self.cr.set_source_rgba(0, 0, 0)

        # draw the speedmeter arc
        rspeed = radius + 50
        self.cr.arc(x, y, rspeed, 2 * pi / 3, 7 * pi / 3)
        self.cr.set_source_rgba(0, 0, 0, 1.0)
        self.cr.stroke()
        s_long = self.long_inset(rspeed)
        s_middle = self.mid_inset(radius)
        s_short = self.short_inset(radius)
        for i in range(11):
            # draw the large ticks
            alpha = (8 - i) * pi / 6
            self.cr.move_to(*self.polar2xy(rspeed, alpha, x, y))
            self.cr.set_line_width(radius / 100)
            self.cr.line_to(*self.polar2xy(rspeed - s_long, alpha, x, y))
            self.cr.stroke()
            self.cr.set_line_width(radius / 200)
            xf, yf = self.polar2xy(rspeed + 10, alpha, x, y)
            stxt = (self.maxspeed // 10) * i
            self.draw_text(xf, yf, stxt, fontsize=radius / 15)

        for i in range(1, 11):
            # middle tick
            alpha = (8 - i) * pi / 6
            beta = (17 - 2 * i) * pi / 12
            self.cr.move_to(*self.polar2xy(rspeed, beta, x, y))
            self.cr.line_to(*self.polar2xy(rspeed - s_middle, beta, x, y))

            #  short tick
            for n in range(10):
                gamma = alpha + n * pi / 60
                self.cr.move_to(*self.polar2xy(rspeed, gamma, x, y))
                self.cr.line_to(*self.polar2xy(rspeed - s_short, gamma, x, y))

        # draw the heading arc
        self.cr.new_sub_path()
        self.cr.arc(x, y, radius, 0, 2 * pi)
        self.cr.stroke()
        self.cr.arc(x, y, radius - 20, 0, 2 * pi)
        self.cr.set_source_rgba(0, 0, 0, 0.20)
        self.cr.fill()
        self.cr.set_source_rgba(0, 0, 0)

        # heading label 90/180/270
        for n in range(0, 4):
            label = str(n * 90)
            # self.cr.set_source_rgba(0, 1, 0)
            # radius * (1 + NauticalSpeedometer.HEADING_SAT_GAP),
            tbox_x, tbox_y = self.polar2xyr(
                radius * 0.88,
                (1 - n) * pi / 2,
                x, y)
            self.draw_text(tbox_x, tbox_y,
                           label, fontsize=radius / 20)

        # draw the satellite arcs
        skyradius = radius * NauticalSpeedometer.HEADING_SAT_GAP
        self.cr.set_line_width(radius / 200)
        self.cr.set_source_rgba(0, 0, 0)
        self.cr.arc(x, y, skyradius, 0, 2 * pi)
        self.cr.set_source_rgba(1, 1, 1)
        self.cr.fill()
        self.cr.set_source_rgba(0, 0, 0)

        self.cr.arc(x, y, skyradius * 2 / 3, 0, 2 * pi)
        self.cr.move_to(x + skyradius / 3, y)  # Avoid line connecting circles
        self.cr.arc(x, y, skyradius / 3, 0, 2 * pi)

        # draw the cross hair
        self.cr.move_to(*self.polar2xyr(skyradius, 1.5 * pi, x, y))
        self.cr.line_to(*self.polar2xyr(skyradius, 0.5 * pi, x, y))
        self.cr.move_to(*self.polar2xyr(skyradius, 0.0, x, y))
        self.cr.line_to(*self.polar2xyr(skyradius, pi, x, y))
        self.cr.set_line_width(radius / 200)
        self.cr.stroke()

        long_inset = self.long_inset(radius)
        mid_inset = self.mid_inset(radius)
        short_inset = self.short_inset(radius)

        # draw the large ticks
        for i in range(12):
            agllong = i * pi / 6
            self.cr.move_to(*self.polar2xy(radius - long_inset, agllong, x, y))
            self.cr.line_to(*self.polar2xy(radius, agllong, x, y))
            self.cr.set_line_width(radius / 100)
            self.cr.stroke()
            self.cr.set_line_width(radius / 200)

            # middle tick
            aglmid = (i + 0.5) * pi / 6
            self.cr.move_to(*self.polar2xy(radius - mid_inset, aglmid, x, y))
            self.cr.line_to(*self.polar2xy(radius, aglmid, x, y))

            #  short tick
            for n in range(1, 10):
                aglshrt = agllong + n * pi / 60
                self.cr.move_to(*self.polar2xy(radius - short_inset,
                                aglshrt, x, y))
                self.cr.line_to(*self.polar2xy(radius, aglshrt, x, y))
            self.cr.stroke()

    def draw_heading(self, trig_height, heading, radius, x, y):
        hypo = trig_height * 2 / sqrt(3)
        h = (pi / 2 - radians(heading) + self.rotate) % (pi * 2)  # to xyz
        self.cr.set_line_width(2)
        self.cr.set_source_rgba(0, 0.3, 0.2, 0.8)

        # the triangle pointer
        x0 = x + radius * cos(h)
        y0 = y - radius * sin(h)

        x1 = x0 + hypo * cos(7 * pi / 6 + h)
        y1 = y0 - hypo * sin(7 * pi / 6 + h)

        x2 = x0 + hypo * cos(5 * pi / 6 + h)
        y2 = y0 - hypo * sin(5 * pi / 6 + h)

        self.cr.move_to(x0, y0)
        self.cr.line_to(x1, y1)
        self.cr.line_to(x2, y2)
        self.cr.line_to(x0, y0)
        self.cr.close_path()
        self.cr.fill()
        self.cr.stroke()

        # heading text
        (tbox_x, tbox_y) = self.polar2xy(radius * 1.1, h, x, y)
        self.draw_text(tbox_x, tbox_y, int(heading), fontsize=radius / 15)

        # the ship shape, based on test and try
        shiplen = radius * NauticalSpeedometer.HEADING_SAT_GAP / 4
        xh, yh = self.polar2xy(shiplen * 2.3, h, x, y)
        xa, ya = self.polar2xy(shiplen * 2.2, h + pi - 0.3, x, y)
        xb, yb = self.polar2xy(shiplen * 2.2, h + pi + 0.3, x, y)
        xc, yc = self.polar2xy(shiplen * 1.4, h - pi / 5, x, y)
        xd, yd = self.polar2xy(shiplen * 1.4, h + pi / 5, x, y)

        self.cr.set_source_rgba(0, 0.3, 0.2, 0.5)
        self.cr.move_to(xa, ya)
        self.cr.line_to(xb, yb)
        self.cr.line_to(xc, yc)
        self.cr.line_to(xh, yh)
        self.cr.line_to(xd, yd)
        self.cr.close_path()
        self.cr.fill()
        # self.cr.stroke()

    def set_color(self, spec):
        '''Set foreground color for drawing.'''
        gdkcolor = Gdk.color_parse(spec)
        r = gdkcolor.red / 65535.0
        g = gdkcolor.green / 65535.0
        b = gdkcolor.blue / 65535.0
        self.cr.set_source_rgb(r, g, b)

    def draw_sat(self, satsoup, radius, x, y):
        """Given a sat's elevation, azimuth, SNR, draw it on the skyview
        Arg:
        satsoup: a dictionary {'el': xx, 'az': xx, 'ss': xx}
        """
        el, az = satsoup['el'], satsoup['az']
        if el == 0 and az == 0:
            return  # Skip satellites with unknown position
        h = pi / 2 - radians(az)  # to xy
        self.cr.set_line_width(2)
        self.cr.set_source_rgb(0, 0, 0)

        x0, y0 = self.polar2xyr(radius * (90 - el) // 90, h, x, y)

        self.cr.new_sub_path()
        if gps.is_sbas(satsoup['PRN']):
            self.cr.rectangle(x0 - NauticalSpeedometer.SAT_SIZE,
                              y0 - NauticalSpeedometer.SAT_SIZE,
                              NauticalSpeedometer.SAT_SIZE * 2,
                              NauticalSpeedometer.SAT_SIZE * 2)
        else:
            self.cr.arc(x0, y0, NauticalSpeedometer.SAT_SIZE, 0, pi * 2.0)

        if satsoup['ss'] < 10:
            self.set_color('Gray')
        elif satsoup['ss'] < 30:
            self.set_color('Red')
        elif satsoup['ss'] < 35:
            self.set_color('Yellow')
        elif satsoup['ss'] < 40:
            self.set_color('Green3')
        else:
            self.set_color('Green1')

        if satsoup['used']:
            self.cr.fill()
        else:
            self.cr.stroke()
        self.draw_text(x0, y0, satsoup['PRN'], fontsize=15)

    def draw_speed(self, radius, x, y):
        self.cr.new_sub_path()
        self.cr.set_line_width(20)
        self.cr.set_source_rgba(0, 0, 0, 0.5)
        speed = self.last_speed * self.conversions.get(self.speed_unit)
        # cariol arc angle start at polar 0, going clockwise
        alpha = 4 * pi / 3
        beta = 2 * pi - alpha
        theta = 5 * pi * speed / (self.maxspeed * 3)

        self.cr.arc(x, y, radius + 40, beta, beta + theta)
        self.cr.stroke()
        # self.cr.close_path()
        # self.cr.fill()
        label = '%.2f %s' % (speed, self.speed_unit)
        self.draw_text(x, y + radius + 40, label, fontsize=20)

    def get_x_y(self):
        rect = self.get_allocation()
        x = (rect.x + rect.width / 2.0)
        y = (rect.y + rect.height / 2.0) - 20
        return x, y

    def get_radius(self, width, height):
        return min(width / 2.0, height / 2.0) - 70


class Main(object):
    "Main"

    def __init__(self, host='localhost', port=gps.GPSD_PORT, device=None,
                 debug=0, speed_unit=None, maxspeed=0, nautical=False,
                 rotate=0.0, target=""):
        self.host = host
        self.port = port
        self.device = device
        self.debug = debug
        self.speed_unit = speed_unit
        self.maxspeed = maxspeed
        self.nautical = nautical
        self.rotate = rotate
        self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
        if not self.window.get_display():
            raise Exception("Can't open display")
        if len(target):
            target = " " + target
        self.window.set_title('xgpsspeed' + target)
        if self.nautical:
            self.window.set_size_request(500, 550)
            self.widget = NauticalSpeedometer(
                speed_unit=self.speed_unit,
                maxspeed=self.maxspeed,
                rotate=self.rotate)
        else:
            self.widget = LandSpeedometer(speed_unit=self.speed_unit)
        self.window.connect('delete-event', self.delete_event)
        self.window.connect('destroy', self.destroy)
        self.widget.show()
        vbox = Gtk.VBox(False, 0)
        self.window.add(vbox)

        self.window.present()
        self.uimanager = Gtk.UIManager()
        self.accelgroup = self.uimanager.get_accel_group()
        self.window.add_accel_group(self.accelgroup)
        self.actiongroup = Gtk.ActionGroup('gpsspeed-ng')
        self.actiongroup.add_actions(
            [
                ('Quit', Gtk.STOCK_QUIT, '_Quit', None,
                    'Quit the Program', lambda unused: Gtk.main_quit()),
                ('File', None, '_File'),
                ('Units', None, '_Units')]
        )
        self.actiongroup.add_radio_actions(
            [
                ('Imperial', None, '_Imperial', '<Control>i',
                    'Imperial Units', 0),
                ('Metric', None, '_Metric', '<Control>m',
                    'Metrical Units', 1),
                ('Nautical', None, '_Nautical', '<Control>n',
                    'Nautical Units', 2)
            ],
            0, lambda a, unused: setattr(
                self.widget, 'speed_unit',
                ['mph', 'kmh', 'knots'][a.get_current_value()])
        )

        self.uimanager.insert_action_group(self.actiongroup, 0)
        self.uimanager.add_ui_from_string('''
<ui>
    <menubar name='MenuBar'>
        <menu action='File'>
            <menuitem action='Quit'/>
        </menu>
        <menu action='Units'>
            <menuitem action='Imperial'/>
            <menuitem action='Metric'/>
            <menuitem action='Nautical'/>
        </menu>
    </menubar>
</ui>
''')
        self.active_unit_map = {
            'mph': '/MenuBar/Units/Imperial',
            'kmh': '/MenuBar/Units/Metric',
            'knots': '/MenuBar/Units/Nautical'
        }
        menubar = self.uimanager.get_widget('/MenuBar')
        self.uimanager.get_widget(
            self.active_unit_map.get(self.speed_unit)
        ).set_active(True)
        vbox.pack_start(menubar, False, False, 0)
        vbox.add(self.widget)
        self.window.show_all()

    def watch(self, daemon, device):
        self.daemon = daemon
        self.device = device
        GObject.io_add_watch(daemon.sock, GObject.IO_IN, self.handle_response)
        GObject.io_add_watch(daemon.sock, GObject.IO_ERR, self.handle_hangup)
        GObject.io_add_watch(daemon.sock, GObject.IO_HUP, self.handle_hangup)
        return True

    def handle_response(self, source, condition):
        if self.daemon.read() == -1:
            self.handle_hangup(source, condition)
        if self.daemon.data['class'] == 'VERSION':
            self.update_version(self.daemon.version)
        elif self.daemon.data['class'] == 'TPV':
            self.update_speed(self.daemon.data)
        elif self.nautical and self.daemon.data['class'] == 'SKY':
            self.update_skyview(self.daemon.data)

        return True

    def handle_hangup(self, _dummy, _unused):
        w = Gtk.MessageDialog(
            parent=self.window,
            type=Gtk.MessageType.ERROR,
            flags=Gtk.DialogFlags.DESTROY_WITH_PARENT,
            buttons=Gtk.ButtonsType.OK
        )
        w.connect("destroy", lambda unused: Gtk.main_quit())
        w.set_title('gpsd error')
        w.set_markup("gpsd has stopped sending data.")
        w.run()
        Gtk.main_quit()
        return True

    def update_speed(self, data):
        if hasattr(data, 'speed'):
            self.widget.last_speed = data.speed
            self.widget.queue_draw()
        if self.nautical and hasattr(data, 'track'):
            self.widget.last_heading = data.track
            self.widget.queue_draw()

    # Used for NauticalSpeedometer only
    def update_skyview(self, data):
        "Update the satellite list and skyview."
        if hasattr(data, 'satellites'):
            self.widget.satellites = data.satellites
            self.widget.queue_draw()

    def update_version(self, ver):
        "Update the Version"

        if ver.release != gps_version:
            sys.stderr.write("%s: WARNING gpsd version %s different than "
                             "expected %s\n" %
                             (sys.argv[0], ver.release, gps_version))

        if ((ver.proto_major != gps.api_major_version or
             ver.proto_minor != gps.api_minor_version)):
            sys.stderr.write("%s: WARNING API version %s.%s different than "
                             "expected %s.%s\n" %
                             (sys.argv[0], ver.proto_major, ver.proto_minor,
                              gps.api_major_version, gps.api_minor_version))

    def delete_event(self, _widget, _event, _data=None):
        # Someday, handle all cleanup operations here
        return False

    def destroy(self, _unused, _empty=None):
        Gtk.main_quit()

    def run(self):
        try:
            daemon = gps.gps(
                host=self.host,
                port=self.port,
                mode=gps.WATCH_ENABLE | gps.WATCH_JSON | gps.WATCH_SCALED,
                verbose=self.debug
            )
            self.watch(daemon, self.device)
            Gtk.main()
        except SocketError:
            w = Gtk.MessageDialog(
                parent=self.window,
                type=Gtk.MessageType.ERROR,
                flags=Gtk.DialogFlags.DESTROY_WITH_PARENT,
                buttons=Gtk.ButtonsType.OK
            )
            w.set_title('socket error')
            w.set_markup(
                "could not connect to gpsd socket. make sure gpsd is running."
            )
            w.run()
            w.destroy()
        except KeyboardInterrupt:
            self.window.emit('delete_event', Gdk.Event(Gdk.NOTHING))


if __name__ == '__main__':
    import sys
    from os.path import basename
    from optparse import OptionParser
    prog = basename(sys.argv[0])
    usage = ('%s [OPTIONS] [host [:port [:device]]]') % (prog)
    epilog = 'BSD terms apply: see the file COPYING in the distribution root' \
             ' for details.'

    parser = OptionParser(usage=usage, epilog=epilog)
    parser.add_option(
        '--debug',
        dest='debug',
        default=0,
        action='store',
        type='int',
        help='Set level of debug. Must be integer. [Default 0]'
    )
    parser.add_option(
        '--device',
        dest='device',
        default=None,
        help='The device to connect. [Default None]'
    )
    parser.add_option(
        '--host',
        dest='host',
        default=None,
        help='The host to connect. [Default localhost]'
    )
    parser.add_option(
        '--landspeed',
        dest='nautical',
        default=True,
        action='store_false',
        help='Enable dashboard-style speedometer.'
    )
    parser.add_option(
        '--maxspeed',
        dest='maxspeed',
        default='50',
        help='Max speed of the speedmeter [Default 50]'
    )
    parser.add_option(
        '--nautical',
        dest='nautical',
        default=True,
        action='store_true',
        help='Enable nautical-style speed and track display.'
    )
    parser.add_option(
        '--port',
        dest='port',
        default=None,
        help='The port to connect. [Default %s]' % gps.GPSD_PORT
    )
    parser.add_option(
        '--rotate',
        dest='rotate',
        default=0,
        action='store',
        type='float',
        help='Rotation of skyview ("up" direction) in degrees. [Default 0]'
    )
    parser.add_option(
        '--speedunits',
        dest='speedunits',
        default='mph',
        help='The unit of speed. Possible units are: mph, kmh, knots. '
             '[Default mph]'
    )
    parser.add_option(
        '-V', '--version',
        action='store_true',
        default=False,
        help='Output version to stderr, then exit'
    )
    (options, args) = parser.parse_args()
    if options.version:
        sys.stderr.write("xgpsspeed: Version %s\n" % gps_version)
        sys.exit(0)

    if args:
        arg = args[0].split(':')
        len_arg = len(arg)
        if len_arg == 1:
            (options.host,) = arg
        elif len_arg == 2:
            (options.host, options.port) = arg
        elif len_arg == 3:
            (options.host, options.port, options.device) = arg
        else:
            parser.print_help()
            sys.exit(0)
        target = ':'.join(args[0:])
    elif options.host or options.port or options.device:
        ltarget = [options.host or 'localhost']
        if options.port or options.device:
            ltarget += [options.port or '']
        if options.device:
            ltarget += [options.device]
        target = ':'.join(ltarget)
    else:
        target = ""
    Main(
        host=options.host or 'localhost',
        port=options.port or gps.GPSD_PORT,
        device=options.device,
        speed_unit=options.speedunits,
        maxspeed=options.maxspeed,
        nautical=options.nautical,
        debug=options.debug,
        rotate=options.rotate,
        target=target,
    ).run()
