#!/usr/bin/python
#------------------------------------------------------------------------------
# Localization Variables (unused at the moment)
# - grid size (discretize the map)
# - gaussian size
# - gaussian magnitude (decomposition of signal resolution)
# - grouping function if multiple hotspots (e.g. centroid)
#------------------------------------------------------------------------------
# TODO: z-buffer problems, some placemarks might occlude others
#------------------------------------------------------------------------------
# NMEA Assumptions
# - You are in EST timezone
# - Flips sign of longitude, turns 75 into -75
#------------------------------------------------------------------------------
# Useful stuff
#    for i in *.nmea; do echo $i; tmp=`basename $i .nmea`; ./GoogleEarth.py -n -t $tmp < $i > $tmp.kml; done
#    for i in *.kml; do echo $i; tmp=`basename $i .kml`; zip $tmp.kmz $i; done
#------------------------------------------------------------------------------
# main() function and Usage design cribbed from:
#     http://www.artima.com/weblogs/viewpost.jsp?thread=4829
#------------------------------------------------------------------------------
"""Usage:  GoogleEarth.py -p prune_ap_list.txt
           [-c (kismet-csv input)] [-n (nmea input)] [-g (kismet-gps input)]\n\
           [-C (csv output (nmea input only)] [-t title]\n\
               < file.(csv|gps|nmea)\n\

NMEA -> CSV: ./GoogleEarth.py -n -C < horizontal.nmea > horizontal.csv\n\
NMEA -> KML: ./GoogleEarth.py -n    < horizontal.nmea > horizontal.kml\n\
GPS  -> KML: ./GoogleEarth.py -g    < vertical.gps    > vertical.kml\n\
CSV  -> KML: ./GoogleEarth.py -c    < vertical.csv    > vertical.kml"""
#------------------------------------------------------------------------------
from xml.dom.minidom import parse, parseString      # mini-DOM model
import sys                                          # std[in|out|err], CLI args
import getopt                                       # yay, we're soooooooo cool
#------------------------------------------------------------------------------
def GpsHeader():
    return "Time (EST), Fix, In Bounding Box, Longitude, Latitude, PDOP, HDOP, VDOP, Speed (mph)"
#------------------------------------------------------------------------------
class GpsObservation:
    strTime = ""
    fix     = 0
    bbox    = 0
    long    = 0
    lat     = 0
    pdop    = 0
    hdop    = 0
    vdop    = 0
    speed   = 0
    #--------------------------------------------------------------------------
    def __init__(self, strTime, fix, bbox, long, lat, pdop, hdop, vdop, speed):
        self.strTime = strTime
        self.fix     = fix
        self.bbox    = bbox
        self.long    = long
        self.lat     = lat
        self.pdop    = pdop
        self.hdop    = hdop
        self.vdop    = vdop
        self.speed   = speed
    #--------------------------------------------------------------------------
    def __str__(self):
        # this is used for the CSV file output
        return str(self.strTime) + "," + str(self.fix) + "," + str(self.bbox) + "," + str(self.long) + "," + str(self.lat) + "," + str(self.pdop) + "," + str(self.hdop) + "," + str(self.vdop) + "," + str(self.speed)
#------------------------------------------------------------------------------
class NetObservation:
    long    = 0
    lat     = 0
    signal  = 0
    mac     = ""
    #--------------------------------------------------------------------------
    def __init__(self, long, lat, mac, signal):
        self.long   = long
        self.lat    = lat
        self.mac    = mac
        self.signal = signal
    #--------------------------------------------------------------------------
    # define cmp() function for use by sort, sort by AP, then by signal
    def __cmp__(self, other):
        if (self.mac > other.mac):
            return 1
        elif (self.mac < other.mac):
            return -1
        else:
            return cmp(self.signal, other.signal)
#------------------------------------------------------------------------------
class Usage(Exception):
    def __init__(self, msg):
        self.msg = msg
#------------------------------------------------------------------------------
def InBoundingBox (long, lat):
    # Bounding box of the university city area

    ne_long = -75.17678260803223
    sw_long = -75.23188591003418

    ne_lat  = 39.974226062647006
    sw_lat  = 39.942515191991696

    mylong = float(long)
    mylat  = float(lat)

    # print >>sys.stderr, str(ne_long) + " > " + str(mylong) + " > " + str(sw_long)
    # print >>sys.stderr, str(ne_lat) + " > " + str(mylat) + " > " + str(sw_lat)

    if ne_long > mylong and mylong > sw_long and ne_lat > mylat and mylat > sw_lat:
        return 1
    else:
        return 0
#------------------------------------------------------------------------------
def NmeaParser(fd, observationList):
    lat     = 0
    long    = 0
    fix     = 0
    speed   = 0
    strTime = 0
    bbox    = 0  
    vdop    = 0  
    pdop    = 0  
    hdop    = 0  

    state   = 0  # 0 = waiting for GSA, 1 = ready for RMC

    idLine = 0
    for strLine in fd:
        idLine += 1
        fields = strLine.split(',')

        try:
            if fields[0] == "$GPRMC":

                if state == 0:
                    continue # not ready for RMC line, no GSA yet

                if fields[2] == "A":
                    fix = 1
                else:
                    fix = 0

                strLatitude  = fields[3]

                if strLatitude != "":
                    degLatitude = float(strLatitude[0:2])
                    minLatitude = float(strLatitude[2:])

                    # converted latitude
                    lat = degLatitude + minLatitude / 60.0
                else:
                    lat = 0

                strLongitude = fields[5]
                
                if strLongitude != "":
                    # longitude reads 3 characters for some reason
                    degLongitude = float(strLongitude[0:3])
                    minLongitude = float(strLongitude[3:])

                    # converted longitude
                    long = -1.0 * (degLongitude + minLongitude/60.0)
                else:
                    long = 0

                bbox = InBoundingBox(long, lat)

                if fields[7] != "":
                    speed   = float(fields[7]) / 0.87
                    # print >>sys.stderr, "knots = " + str(fields[7]) + ", mph = " + str(speed)
                else: 
                    speed   = fields[7]

                time    = fields[1]
                hours   = str(int(time[0:2]) - 5) # adjust for EST
                minutes = time[2:4]
                seconds = time[4:6]

                dseconds = 0
                index = time.find(".")
                if index != -1:
                    dseconds = time[index+1:]

                strTime = hours + ':' + minutes + ':' + seconds + '.' + dseconds

                observationList.append(GpsObservation (strTime, fix, bbox, long, lat, pdop, hdop, vdop, speed))


            elif fields[0] == "$GPGSA":

                pdop = fields[15]
                hdop = fields[16]
                vdop = fields[17]

                index = vdop.find("*")
                if index != -1:
                    vdop = vdop[0:index]

                # print >>sys.stderr, "hdop = " + hdop + ", pdop = " + pdop + ", vdop = " + vdop

                state = 1

        except:
            print >>sys.stderr, "Skipping line " + str(idLine) + " due to error:  " + strLine
            continue

#------------------------------------------------------------------------------
def CsvParser(fd, observationList, doPruning, pruneList):
    for strLine in fd:
        fields = strLine.split(',')

        essid  = None
        mac    = None
        signal = None
        long   = None
        lat    = None

        # skip if:
        #     the line does not contain the correct # of fields
        try: 
            essid  = fields[2]
            mac    = fields[3].lower()
            signal = fields[22]
            long   = fields[33]
            lat    = fields[32]
        except:
            continue

        # skip if:
        #     lat/long is zero
        #     lat/long/signal cannot be converted into a float
        #     essid is not dragonfly, or undefined
        #     The -999999 is arbitrary, just want the exception to prune invalid entries
        try:
            if (float(long) == 0.0 and float(lat) == 0.0) or (essid != "dragonfly" and essid != "<no ssid>") or int(signal) < -999999:
                continue
        except:
            continue

        try:
            if doPruning and pruneList.index(mac):
                # print >>sys.stderr, mac + "is in pruneList"
                None
        except:
            # index() throws an exception if 'mac' is not in the list
            continue

        observationList.append(NetObservation(long, lat, mac, signal))
        #print >>sys.stdout, mac + "," + signal + "," + long + "," + lat
#------------------------------------------------------------------------------
def KismetGpsParser(fd, observationList, doPruning, pruneList):
    try:
        dom = parse(fd)
    except:
        print >>sys.stderr, "Error parsing \'" + input + "\' it's not valid xml or your interval is too small"
        return 1

    elementList = dom.getElementsByTagName("gps-point")
    for elementNode in elementList:
        attributeMap = elementNode.attributes

        # skip if:
        #     attributes are missing
        try:
            essid  = None
            mac    = attributeMap.getNamedItem("bssid").value.lower()
            signal = attributeMap.getNamedItem("signal").value
            long   = attributeMap.getNamedItem("lon").value
            lat    = attributeMap.getNamedItem("lat").value
        except:
            continue

        try:
            if doPruning and pruneList.index(mac):
                # print >>sys.stderr, mac + "is in pruneList"
                None
        except:
            # index() throws an exception if 'mac' is not in the list
            continue

        observationList.append(NetObservation(long, lat, mac, signal))
#------------------------------------------------------------------------------
def PrintKmlHeader(strTitle):
    print >>sys.stdout, "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \n\
<kml xmlns=\"http://earth.google.com/kml/2.0\"> \n\
<Document>\n\
<LookAt>\n\
\t<longitude>-75.189812</longitude>\n\
\t<latitude>39.956673</latitude>\n\
\t<range>440</range>\n\
\t<tilt>8</tilt>\n\
\t<heading>3</heading>\n\
</LookAt>\n\
<name>" + strTitle + "</name>\n"
#------------------------------------------------------------------------------
def PrintKmlOpenFolder(name):
    print >>sys.stdout, "<Folder>\n\
\t<name>" + name + "</name>"
#------------------------------------------------------------------------------
def PrintKmlCloseFolder():
    print >>sys.stdout, "</Folder>"
#------------------------------------------------------------------------------
def PrintKmlFooter():
    print >>sys.stdout, "</Document> \n\
</kml>"
#------------------------------------------------------------------------------
def PrintKmlPointAp(long, lat, ap, signal):

    x = None
    isig = int(signal)
    if isig >= -9:
        x = 32
        y = 0
    if isig >= -19:
        x = 0
        y = 224
    elif isig >= -29:
        x = 32
        y = 224
    elif isig >= -39:
        x = 64
        y = 224
    elif isig >= -49:
        x = 96
        y = 224
    elif isig >= -59:
        x = 128
        y = 224
    elif isig >= -69:
        x = 160
        y = 224
    elif isig >= -79:
        x = 192
        y = 224
    elif isig >= -89:
        x = 224
        y = 224
    elif isig >= -99:
        x = 0
        y = 128
    else:
        x = 32
        y = 128

    print >>sys.stdout, "\t<Placemark>\n\
\t\t<Style>\n\
\t\t\t<IconStyle>\n\
\t\t\t\t<Icon>\n\
\t\t\t\t\t<href>root://icons/palette-3.png</href>\n\
\t\t\t\t\t<x>" + str(x) + "</x>\n\
\t\t\t\t\t<y>" + str(y) + "</y>\n\
\t\t\t\t\t<w>32</w>\n\
\t\t\t\t\t<h>32</h>\n\
\t\t\t\t</Icon>\n\
\t\t\t</IconStyle>\n\
\t\t</Style>\n\
\t\t<description>\n\
\t\t\t<![CDATA[ \n\
\t\t\t<ul>\n\
\t\t\t\t<li>AP     : " + ap + "</li>\n\
\t\t\t\t<li>Signal : " + signal + "</li>\n\
\t\t\t</ul>\n\
\t\t\t]]>\n\
\t\t</description>\n\
\t\t<name>" + ap + "</name>\n\
\t\t<Point>\n\
\t\t\t<extrude>1</extrude>\n\
\t\t\t<altitudeMode>relativeToGround</altitudeMode>\n\
\t\t\t<coordinates>" + long + "," + lat + ",0</coordinates>\n\
\t\t</Point>\n\
\t</Placemark>"
#------------------------------------------------------------------------------
def PrintKmlPointGps(long, lat, fix, dop, title, string):

    x = 0
    y = 0

    try:
        fdop = float(dop)
    except:
        # empty string case
        fdop = -1.0 

    if fix == 0:
        # show an 'X' when no fix, but still provides a lat/long
        x = 224
        y = 224
    else:
        if fdop <= 5:
            x = 0
            y = 0
        elif fdop <= 10:
            x = 32
            y = 0
#        elif fdop <= 15:
#            x = 64
#            y = 0
#        elif fdop <= 20:
#            x = 96
#            y = 0
#        elif fdop <= 25:
#            x = 128
#            y = 0
#        elif fdop <= 30:
#            x = 160
#            y = 0
#        elif fdop <= 35:
#            x = 192
#            y = 0
#        elif fdop <= 40:
#            x = 224
#            y = 0
#        elif fdop <= 45:
#            x = 0
#            y = 64
#        elif fdop <= 50:
#            x = 32
#            y = 64
        else:
            # show a '+'
            x = 96
            y = 192

    print >>sys.stdout, "\t<Placemark>\n\
\t\t<Style>\n\
\t\t\t<IconStyle>\n\
\t\t\t\t<Icon>\n\
\t\t\t\t\t<href>root://icons/palette-5.png</href>\n\
\t\t\t\t\t<x>" + str(x) + "</x>\n\
\t\t\t\t\t<y>" + str(y) + "</y>\n\
\t\t\t\t\t<w>32</w>\n\
\t\t\t\t\t<h>32</h>\n\
\t\t\t\t</Icon>\n\
\t\t\t</IconStyle>\n\
\t\t</Style>\n\
\t\t<description>\n\
\t\t\t<![CDATA[ \n\
\t\t\t<ul>\n\
\t\t\t\t<li>" + string + "</li>\n\
\t\t\t</ul>\n\
\t\t\t]]>\n\
\t\t</description>\n\
\t\t<name>" + title + "</name>\n\
\t\t<Point>\n\
\t\t\t<extrude>1</extrude>\n\
\t\t\t<altitudeMode>relativeToGround</altitudeMode>\n\
\t\t\t<coordinates>" + long + "," + lat + ",0</coordinates>\n\
\t\t</Point>\n\
\t</Placemark>"
#------------------------------------------------------------------------------
def main (argv = None):
    if argv is None:
        argv = sys.argv
    try:
        #----------------------------------------------------------------------
        try:
            opts, args = getopt.getopt(argv[1:], "hp:cngCt:", ["help", "prune", "csv", "nmea", "kismet-gps", "CSV", "title"])
        except getopt.error, msg:
            raise Usage(msg);
        #----------------------------------------------------------------------
        # process options
        #----------------------------------------------------------------------

        doPruning    = 0
        strPruneFile = None
        typeInput    = 0   # 0 = gps, 1 = csv, 2 = nmea
        typeOutput   = 0   # 0 = kml, 1 = csv
        strTitle     = "Untitled"
        for o, a in opts:
            if o in ("-h", "--help"):
                print __doc__ # prints the "Module docstring"
                return 0
            elif o in ("-g", "--kismet-gps"):
                typeInput = 0
            elif o in ("-c", "--csv"):
                typeInput = 1
            elif o in ("-n", "--nmea"):
                typeInput = 2
            elif o in ("-C", "--CSV"):
                typeOutput = 1
            elif o in ("-p", "--prune"):
                doPruning = 1
                strPruneFile = a
            elif o in ("-t", "--title"):
                strTitle = a
        #----------------------------------------------------------------------
        # main subprogram
        #----------------------------------------------------------------------

        pruneList = []
        if doPruning == 1:
            print >>sys.stderr, "Pruning APs that are listed in " + strPruneFile
            fd = open(strPruneFile, "r")
            for strLine in fd:
                # remove newline
                if strLine and strLine[-1] == '\n':
                    strLine = strLine[:-1]
                pruneList.append(strLine.lower())

        observationList = []

        print >>sys.stderr, "Reading from stdin ..."

        if typeInput == 0:
            KismetGpsParser(sys.stdin, observationList, doPruning, pruneList)
        elif typeInput == 1:
            CsvParser(sys.stdin, observationList, doPruning, pruneList)
        elif typeInput == 2:
            NmeaParser(sys.stdin, observationList)
        else:
            print >>sys.stderr, "Input type not defined"
            return 0

        if typeInput == 0 or typeInput == 1:
            PrintKmlHeader(strTitle)

            # sort the list
            observationList.sort()

            #------------------------------------------------------------------
            # loop through values and build histogram for each AP
            # more efficient to do in one loop, but this is much simpler
            hist = {}
            for i in observationList:
                if hist.get(i.mac) == None:
                    hist[i.mac] = 1
                else:
                    hist[i.mac] += 1

            #------------------------------------------------------------------
            # print the list of observations
            currentMac = None
            for i in observationList:

                if i.mac != currentMac:
                    if currentMac != None:
                        PrintKmlCloseFolder()
                    PrintKmlOpenFolder(i.mac + "(" + str(hist[i.mac]) + ")")

                PrintKmlPointAp(i.long, i.lat, i.mac, i.signal)

                currentMac = i.mac

            PrintKmlCloseFolder()
            PrintKmlFooter()

        #----------------------------------------------------------------------
        elif typeInput == 2:

            if typeOutput == 0: # kml
                PrintKmlHeader(strTitle)
                for i in observationList:
                    PrintKmlPointGps(str(i.long), str(i.lat), i.fix, i.pdop, str(i.strTime), 
        str("Time = " + i.strTime) + "<li>Fix = " + str(i.fix) + "<li>BoundingBox = " + str(i.bbox) + "<li>Longitude = " + str(i.long) + "<li>Latitude = " + str(i.lat) + "<li>PDOP = " + str(i.pdop) + "<li>HDOP = " + str(i.hdop) + "<li>VDOP = " + str(i.vdop) + "<li>Speed = " + str(i.speed))
                PrintKmlFooter()

            if typeOutput == 1: # csv
                print >>sys.stdout, GpsHeader()
                for i in observationList:
                    print >>sys.stdout, i

    #--------------------------------------------------------------------------
    except Usage, err:
        print >>sys.stderr, err.msg
        print >>sys.stderr, "for help use --help"
        return 1
#------------------------------------------------------------------------------
if __name__ == "__main__":
    sys.exit(main())
#------------------------------------------------------------------------------
