#!/usr/bin/env python
#
# Copyright 2012 Nick Foster
# 
# This file is part of gr-air-modes
# 
# gr-air-modes is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
# 
# gr-air-modes 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 General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with gr-air-modes; see the file COPYING.  If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
#

# azimuthal projection widget to plot reception range vs. azimuth

from PyQt4 import QtCore, QtGui
import threading
import math
import air_modes
from air_modes.exceptions import *
import numpy as np

# model has max range vs. azimuth in n-degree increments
# contains separate max range for a variety of altitudes so
# you can determine your altitude dropouts by bearing
# assumes that if you can hear ac at 1000', you can hear at 5000'+.
class az_map_model(QtCore.QObject):
    dataChanged = QtCore.pyqtSignal(name='dataChanged')
    npoints = 360/5
    def __init__(self, parent=None):
        super(az_map_model, self).__init__(parent)
        self._data = []
        self.lock = threading.Lock()
        self._altitudes = [0, 1000, 2000, 5000, 10000, 15000, 20000, 25000, 30000]
        #initialize everything to 0
        for i in range(0,az_map_model.npoints):
            self._data.append([0] * len(self._altitudes))

    def rowCount(self):
        return len(self._data)

    def columnCount(self):
        return len(self._altitudes)

    def data(self, row, col):
        return self._data[row][col]

    def addRecord(self, bearing, altitude, distance):
        with self.lock:
            #round up to nearest altitude in altitudes list
            #there's probably another way to do it
            if altitude >= max(self._altitudes):
                col = self.columnCount()-1
            else:
                col = self._altitudes.index(min([alt for alt in self._altitudes if alt >= altitude]))

            #find which bearing row we sit in
            row = int(int(bearing+(180./az_map_model.npoints)) / (360./az_map_model.npoints)) % az_map_model.npoints
            #set max range for all alts >= the ac alt
            #this expresses the assumption that higher ac can be heard further
            update = False
            for i in range(col, len(self._altitudes)):
                if distance > self._data[row][i]:
                    self._data[row][i] = distance
                    update = True
        if update:
            self.dataChanged.emit()

    def reset(self):
        with self.lock:
            self._data = []
            for i in range(0,az_map_model.npoints):
                self._data.append([0] * len(self._altitudes))
        self.dataChanged.emit()


# the azimuth map widget
class az_map(QtGui.QWidget):
    maxrange = 200
    bgcolor = QtCore.Qt.black
    ringpen =  QtGui.QPen(QtGui.QColor(0,   96,  127, 255), 1.3)

    def __init__(self, parent=None):
        super(az_map, self).__init__(parent)
        self._model = None
        self._paths = []
        self.maxrange = az_map.maxrange

    def minimumSizeHint(self):
        return QtCore.QSize(50, 50)

    def sizeHint(self):
        return QtCore.QSize(300, 300)

    def setModel(self, model):
        self._model = model
        self._model.dataChanged.connect(self.repaint)

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        #TODO: make it not have to redraw paths EVERY repaint
        #drawing paths is VERY SLOW
        #maybe use a QTimer to limit repaints
        self.drawPaths()

        #set background
        painter.fillRect(event.rect(), QtGui.QBrush(az_map.bgcolor))

        #draw the range rings
        self.drawRangeRings(painter)
        for i in range(len(self._paths)):
            alpha = 230 * (i+1) / (len(self._paths)) + 25
            painter.setPen(QtGui.QPen(QtGui.QColor(alpha,alpha,0,255), 1.0))
            painter.drawPath(self._paths[i])

    def drawPaths(self):
        self._paths = []
        if(self._model):
            for alt in range(0, self._model.columnCount()):
                path = QtGui.QPainterPath()
                for i in range(az_map_model.npoints-1,-1,-1):
                    #bearing is to start point of arc (clockwise) 
                    bearing = (i+0.5) * 360./az_map_model.npoints
                    distance = self._model._data[i][alt]
                    radius = min(self.width(), self.height()) / 2.0
                    scale = radius * distance / self.get_range()
                    #convert bearing,distance to x,y
                    xpts = scale * math.sin(bearing * math.pi / 180)
                    ypts = scale * math.cos(bearing * math.pi / 180)
                    #get the bounding rectangle of the arc

                    arcrect = QtCore.QRectF(QtCore.QPointF(0-scale, 0-scale),
                                            QtCore.QPointF(scale, scale))

                    if path.isEmpty():
                        path.moveTo(xpts, 0-ypts) #so we don't get a line from 0,0 to the first point
                    else:
                        path.lineTo(xpts, 0-ypts)
                    path.arcTo(arcrect, 90-bearing, 360./az_map_model.npoints)

                self._paths.append(path)

    #this is just to add a little buffer space for showing the ring & range
    def get_range(self):
        return int(self.maxrange * 1.1)

    def drawRangeRings(self, painter):
        painter.translate(self.width()/2, self.height()/2)
        #choose intelligent range step -- keep it between 3-5 rings
        rangestep = 100
        while self.get_range() / rangestep < 3:
            rangestep /= 2.0
        for i in np.arange(rangestep, self.get_range(), rangestep): 
            diameter = (float(i) / self.get_range()) * min(self.width(), self.height())
            painter.setPen(az_map.ringpen)
            painter.drawEllipse(QtCore.QRectF(-diameter / 2.0,
                                -diameter / 2.0, diameter, diameter))
            painter.setPen(QtGui.QColor(255,127,0,255))

            painter.drawText(0-70/2.0, diameter/2.0, 70, 30, QtCore.Qt.AlignHCenter,
                             "%.1fnm" % i)

    def setMaxRange(self, maxrange):
        maxrange = max(3.25, maxrange)
        maxrange = min(500., maxrange)
        self.maxrange = maxrange
        self.repaint()

    def wheelEvent(self, event):
        self.setMaxRange(self.maxrange + (event.delta()/120.)*self.maxrange/4.)

class az_map_output:
    def __init__(self, cprdec, model, pub):
        self._cpr = cprdec
        self.model = model
        pub.subscribe("type17_dl", self.output)

    def output(self, msg):
        try:
            now = time.time()

            icao = msg.data["aa"]
            subtype = msg.data["ftc"]
            distance, altitude, bearing = [0,0,0]
            if 5 <= subtype <= 8:
                (ground_track, decoded_lat, decoded_lon, distance, bearing) = air_modes.parseBDS06(msg.data, self._cpr)
                altitude = 0
            elif 9 <= subtype <= 18:
                    (altitude, decoded_lat, decoded_lon, distance, bearing) = air_modes.parseBDS05(msg.data, self._cpr)

            self.model.addRecord(bearing, altitude, distance)
        except ADSBError:
            pass


##############################
# Test stuff
##############################
import random, time

class model_updater(threading.Thread):
    def __init__(self, model):
        super(model_updater, self).__init__()
        self.model = model
        self.setDaemon(1)
        self.done = False
        self.start()

    def run(self):
        for i in range(az_map_model.npoints):
            time.sleep(0.005)
            if(self.model):
                for alt in self.model._altitudes:
                    self.model.addRecord(i*360./az_map_model.npoints, alt, random.randint(0,az_map.maxrange)*alt / max(self.model._altitudes))
        self.done = True
        
class Window(QtGui.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        layout = QtGui.QGridLayout()
        self.model = az_map_model()
        mymap = az_map(None)
        mymap.setModel(self.model)
        self.updater = model_updater(self.model)
        layout.addWidget(mymap, 0, 1)
        self.setLayout(layout)

if __name__ == '__main__':

    import sys

    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.show()
    window.update()
    sys.exit(app.exec_())
