Commits

cdietrich  committed 59854b4

Variable name changes and code cleaning.

  • Participants
  • Parent commits 36dfd55

Comments (0)

Files changed (1)

File kinetamap_48.py

+"""Parser and Plotter for the Sparkfun KinetaMap, v48
+Copyright (C) 2011  Colin Dietrich
+
+Requirements:
+python 2.6
+numpy 1.4.0+
+matplotlib 0.99.3+
+
+Useage:
+Select KinetaMap log file either by importing the KinetaMapParser class,
+
+test = KinetaMapParse()
+test.parse('D:\\logs\\my_file')
+    
+or run the tkinter class and select the file with the GUI. Launch using the
+command prompt / shell: >python kinetamap.py
+
+The default directory opened in the GUI can be changed by editing 
+self.default_log_dir in the MainWindow class.
+
+Outputs:
+All output files are saved to the same directory as the data file and are
+named 'input file name' + 'data and time' + 'description'
+
+Comma delimited files
+altitude profile = gps altitude values recorded
+locations only = gps locations with a valid fix
+interpolated locations = gps locations and all interpolated locations for 
+    accelerometer readings
+
+png images
+plotted elevation profiles = gps altitude records in feet and meters
+latitude and lonitude records = scatter plot
+
+kml files
+location only clamped = lat/lon records points clamped to the ground
+location only GPS altitude = lat/lon with GPS altitude plotted relative
+    above the ground. Labels ON includes information on each point.
+interpolated - clamped = lat/lon gps records and interpolated locations
+    with altitude records clamped to the ground. Labels ON includes
+    information on each point.
+interpolated - GPS altitude = lat/lon with GPS altitude plotted relative
+    above the ground. Labels ON includes information on each point.
+interpolated - n acceleration = interpolated locations with n = (x,y,z)
+    acceleration plotted as relative altitude to the ground.
+interpolated - n magnitude = interpolated locations with n = (xy,yz,zx,xyz)
+    vector magnitude of acceleration plotted as relative altitude to 
+    the ground.
+
+text files
+log.txt = contains track statistics and any errors encountered.
+
+Notes:
+Tested using Windows XP and KinetaMap firmware version 1.1.
+Sparkfun KinetaMap product page: http://www.sparkfun.com/products/8725
+
+License:
+This program 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 of the License, or 
+any later version.
+
+This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from __future__ import division
+
+import Tkinter
+import tkFileDialog
+import string
+import cStringIO
+import time
+import os
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+class KinetaMapParse():
+    """Functions to parse, clean and analyze a KinetaMap data file.
+    """
+    
+    def __init__(self):
+        
+        self.record_time = time.strftime('20%y_%m_%d - %H_%M_%S')
+        self.statistics = ''
+        self.read_file_error = ''
+        self.parse_error = ''
+        
+        self.converters = {0: lambda s: (s or 0),   # Record number
+                           1: lambda s: (s or 0),   # UTC Time
+                           2: lambda s: (s or 0),   # Acceleration X
+                           3: lambda s: (s or 0),   # Acceleration Y
+                           4: lambda s: (s or 0),   # Acceleration Z
+                           5: lambda s: (s or 0),   # Battery
+                           6: lambda s: (s or 0),   # Fix
+                           7: lambda s: (s or 0),   # Latitude
+                           8: lambda s: (s or 0),   # Latitude Direction
+                           9: lambda s: (s or 0),   # Longitude
+                           10: lambda s: (s or 0),  # Longitude Direction
+                           11: lambda s: (s or 0),  # Altitude
+                           12: lambda s: (s or 0),  # Type
+                           }
+        
+        self.dataType = [('record', np.int32),          # 0
+                          ('time', np.float64),         # 1
+                          ('ax', np.float64),           # 2
+                          ('ay', np.float64),           # 3
+                          ('az', np.float64),           # 4
+                          ('bat', np.float64),          # 5
+                          ('fix', np.int8),             # 6
+                          ('latitude', np.float64),     # 7
+                          ('NS', 'S1'),                 # 8
+                          ('longitude', np.float64),    # 9 
+                          ('EW', 'S1'),                 # 10
+                          ('altitude', np.float64),     # 11
+                          ('type', 'S5'),               # 12
+                          ]
+    
+    def read_file(self, filepath):
+        """Open a KinetaMap data file and return a Numpy Array of data.
+        
+        Before scipy will import the file, the two 'nul' characters the 
+        KinetaMap sometimes outputs at columns [46:48] must be removed.
+        
+        Records are checked for length requirements, GPS fix and then the
+        GPS row records are combined with their immediately previous 
+        accelerometer record.
+        
+        Also, a row number record is inserted at column 0 for later use. 
+        """
+        
+        io_file = cStringIO.StringIO()
+        row_number = -1
+        data_shuffle_check = False
+        
+        with open(filepath) as data_file:
+            for row in data_file:
+                if row_number == -1:
+                    if (row[0:40] != 
+                        'UTC, X, Y, Z, Batt, Fix, Lat., Lat. Dir.'):
+                        return(None)
+                    row_number += 1
+                    continue
+                    
+                row = string.replace(row, '\x00', '')
+                row = row.strip("\n\r")
+                single_row = row.split(",")
+                
+                # Could also to a checksum calc using the GPS string
+                
+                # Skip poorly formed rows of data
+                if len(single_row) != 12:
+                    self.read_file_error += ('Malformed row data: %s\n' %
+                                             single_row)
+                    continue
+                
+                # Skip GPS records that are not correct lengths
+                row_lat = str(single_row[6])
+                if row_lat != '':
+                    row_lat = row_lat.split('.')
+                    if len(str(abs(int(row_lat[0])))) != 4:
+                        self.parse_error += (
+                            'Incorrect GPS data \'%s\' in row: %s\n' % 
+                            (row_lat[0], row))
+                        continue
+                    if len(row_lat[1]) != 4:
+                        self.parse_error += (
+                            'Incorrect GPS data \'%s\' in row: %s\n' % 
+                            (row_lat[0], row))
+                        continue
+                
+                row_lon = str(single_row[8])
+                if row_lon != '':
+                    row_lon = row_lon.split('.')
+                    if len(str(abs(int(row_lon[0])))) != 5:
+                        self.parse_error += (
+                            'Incorrect GPS data \'%s\' in row: %s\n' %
+                            (row_lon[0], row))
+                        continue
+                    if len(row_lon[1]) != 4:
+                        self.parse_error += (
+                            'Incorrect GPS data \'%s\' in row: %s\n' %
+                            (row_lon[0], row))
+                        continue
+                    
+                if single_row[5] == '1':    # 1 or 2 = fix ok from GPS
+                    last_row = single_row
+                    data_shuffle_check = True
+                    continue                
+                
+                if single_row[5] == '2':    # 1 or 2 = fix ok from GPS
+                    last_row = single_row
+                    data_shuffle_check = True
+                    continue
+                
+                if data_shuffle_check == True:
+                    single_row[5:12] = last_row[5:12]
+                    return_row = ",".join(single_row)
+                    return_row = (str(row_number) + 
+                                     ',' + return_row + "\n")
+                    data_shuffle_check = False
+                    
+                else:
+                    return_row = str(row_number) + ',' + row + "\n"
+                    
+                io_file.write(return_row)
+                row_number += 1
+        
+        # Import the stringIO file to a Numpy array
+        np_array = np.genfromtxt(cStringIO.StringIO(io_file.getvalue()),
+                               dtype=self.dataType, delimiter=',',
+                               filling_values={0:0, 1:0, 2:0, 3:0, 4:0, 
+                                               5:0, 6:0, 7:0, 8:0, 9:0, 
+                                               10:0, 11:0, 12:0, })
+        
+        io_file.close()
+        return(np_array)
+    
+    def fix_mask(self, np_array):
+        """Read a Numpy array of KinetaMap data a return a Numpy Array
+        containing only GPS records with a valid fix.
+        
+        For the EM408, any number besides 0 is a type of valid fix.
+        """
+        fm = (np_array['fix'] != 0)
+        return np_array[fm]
+    
+    def decimal_degrees(self, coordinate):
+        """Take a decimal minutes angular measurement and return a decimal
+        degrees angular measurement.
+        
+        The EM408 outputs in latitude ddmm.mmmm and longitude dddmm.mmmm.
+        For calculations, need in latitude dd.dddddd or longitude ddd.dddddd.
+        Input: Latitude in ddmm.mmmm or longitude in dddmm.mmmm
+        Output: Latitude in dd.dddddd or longitude in ddd.dddddd
+        """
+        degree = np.int(coordinate / 100)
+        decimal = degree + ((coordinate - (degree * 100)) / 60)
+        return(decimal)
+    
+    def arc_length(self, lat1, long1, lat2, long2):
+        """Take two points on the globe and return the arc length distance
+        between them.
+        
+        Uses the Spherical Law of Cosines to calculate distances.
+        Input: Two locations in decimal degrees
+        Output: Distance in meters
+        """
+        R = 6371 * 1000    # Radius of Earth in meters
+        distance = R * np.arccos(np.sin(lat1 * (np.pi / 180)) *
+                                np.sin(lat2 * (np.pi / 180)) +
+                                np.cos(lat1 * (np.pi / 180)) *
+                                np.cos(lat2 * (np.pi / 180)) *
+                                np.cos((long2 - long1) * (np.pi / 180))
+                                )
+        return(distance)
+    
+    def clean(self, array):
+        """Read a Numpy array of KinetaMap data and a return a Numpy Array
+        with the location records properly formatted and error checked.
+        
+        Change the latitude and longitude values to decimal degrees, change
+        the notation from NS and EW to negative for S and W, remove GPS 
+        records that have dx/dt > 100 miles / hour = 44 m / s since they are
+        likely incorrect in most applications.
+        """
+        
+        # Vectorize the decimal degree conversion function onto the array
+        vfunc = np.vectorize(self.decimal_degrees)
+        array['latitude'] = vfunc(array['latitude'])
+        array['longitude'] = vfunc(array['longitude'])
+        
+        # If there are S or W entries, multiply by -1
+        for i in array:
+            if i['NS'] == 'S':
+                i['latitude'] = i['latitude'] * -1
+            if i['EW'] == 'W':
+                i['longitude'] = i['longitude'] * -1
+        
+        # Renumber records so array[0] is correct row number
+        for j in range(len(array)):
+            array[j]['record'] = j
+        
+        # dx/dt and dy/dt > 44 m/s, mask it out
+        
+        array_gps = self.fix_mask(array)
+        #array_gps = array
+        dt_r = []
+        
+        for k in range(1, len(array_gps)):
+            
+            kd = self.arc_length(array_gps[k - 1]['latitude'], \
+                                     array_gps[k - 1]['longitude'], \
+                                     array_gps[k]['latitude'], \
+                                     array_gps[k]['longitude'])
+
+            kt = array_gps[k]['time'] - array_gps[k - 1]['time']
+            if (kd != 0) & (kt != 0):
+                kdt = kd / kt
+            else: kdt = 0
+            
+            # If the over land speed is more that 44 m/s (100mph), 
+            # it's probably a data error
+            if kdt > 44:
+                dt_r.append((kd, array_gps[k]['record']))
+                self.parse_error += ('Speed of %s m/s is greater than ' +
+                                    '44m/s from row: %s\n' % 
+                                    (kdt, array_gps[k]))
+                
+            ka = array_gps[k]['altitude'] - array_gps[k - 1]['altitude']
+            if (ka != 0) & (kt != 0) & \
+                np.isfinite(ka) & np.isfinite(kt):
+                kat = ka / kt
+            else: kat = 0
+            
+            # If the rate of elevation change is more that 44 m/s (100mph),
+            # it's probably a data error
+            # Maybe use the GPS checksum?
+            if kat > 100:
+                dt_r.append((ka, array_gps[k - 1]['record']))
+                self.parse_error += ('Elevation change %s is greater than ' +
+                                    '44 m/s in row: %s\n' % 
+                                    (kat, array_gps[k]))
+                
+        dt_mask = np.ones(len(array))
+        for m in dt_r:
+            dt_mask[m[1]] = False
+        
+        dt_mask = np.ma.make_mask(dt_mask)
+        array = array[dt_mask]
+        
+        return(array)
+    
+    def track_stats(self, filepath, npArray):
+        """Read a formatted Numpy array of KinetaMap data and save to disk
+        an comma delimited file with elevation data, two plots of the 
+        elevation profile and a log file with track statistics and 
+        analysis comments.
+        """
+        new_filepath = (filepath[:-4] + ' - ' + self.record_time + 
+                            ' - altitude profile' + '.csv')
+        
+        a_gps = self.fix_mask(npArray)
+        record_time = [a_gps[0]['time'], ]
+        
+        # Save a plot of latitude(t) by longitude(t) at intervals t
+        plt.figure(figsize=(7, 7), dpi=80)
+        plt.clf()
+        plt.axis('equal')
+        plt.title('Latitude and Longitude of Track')
+        plt.xlabel('Longitude')
+        plt.ylabel('Latitude')
+        
+        plt.plot(a_gps['longitude'], a_gps['latitude'], 
+                'ro', label='data points')
+        plt.grid(True)
+        plt.legend(loc='lower right')
+        plt.savefig(filepath[:-4] + 
+                    ' - ' + self.record_time + 
+                    ' - lat lon.png')
+        plt.close()
+        
+        distance = [0, ]
+        distance_total = 0
+        altitude = [a_gps[0]['altitude'], ]
+        altitude_change_final = 0
+        altitude_change_cumulative = 0
+        
+        # Could use a filter to remove drift while stationary...
+        
+        for k in range(1, len(a_gps)):
+            
+            # Distance change
+            k_d = self.arc_length(a_gps[k - 1]['latitude'],
+                                     a_gps[k - 1]['longitude'],
+                                     a_gps[k]['latitude'],
+                                     a_gps[k]['longitude'])
+            
+            if np.isfinite(k_d):
+                distance.append(distance_total + k_d)
+                distance_total += k_d
+            else: 
+                distance.append(distance_total + 0)
+                
+            # Time step (dt)
+            k_t = a_gps[k]['time'] - a_gps[k - 1]['time']
+            
+            # Deal with roll over of 24 hour clock
+            if k_t < 0:
+                k_t = (a_gps[k]['time'] + 
+                           (235960 - a_gps[k - 1]['time']))
+            
+            record_time.append(a_gps[k]['time'])
+            
+            # Altitude change
+            k_a = a_gps[k]['altitude'] - a_gps[k - 1]['altitude']
+            altitude.append(a_gps[k]['altitude'])
+            
+            # Final elevation change
+            altitude_change_final += k_a
+            
+            # Cumulative elevation change
+            if k_a > 0:
+                altitude_change_cumulative += k_a
+            
+        # Save the elevation profile data
+        columns = np.column_stack((record_time, distance, altitude))
+        header = 'Time (UTC), Distance (m), Altitude (m)\n'
+        elevation_profile = open(new_filepath, 'a')
+        elevation_profile.write(header)
+        np.savetxt(elevation_profile, columns, delimiter=',', 
+                   fmt=('%06d', '%1.4f', '%1.1f'))
+        elevation_profile.close()
+        
+        # Save a plot of the elevation profile
+        if os.name == 'nt':
+            filepath_split = filepath.split('\\')
+        elif os.name == 'posix':
+            filepath_split = filepath.split('/')
+        else: filepath_split = filepath
+        
+        distance_array = np.array(distance)
+        altitude_array = np.array(altitude)
+        
+        # Metric plot
+        plt.clf()
+        distance_array_km = distance_array / 1000
+        plt.fill_between(distance_array_km, 
+                         altitude_array, 0, facecolor='green')
+        plt.title('Elevation Profile for:\n %s' % 
+                  filepath_split[-1][:-4])
+        plt.xlabel('Distance (km)')
+        plt.ylabel('Elevation (m)')
+        x_min, x_max = plt.xlim() #@UnusedVariable
+        plt.xlim(x_min, distance_array_km[-1])
+        plot_frame = plt.gcf()
+        plot_frame.set_size_inches(10, 5)
+        plt.savefig(filepath[:-4] + 
+                    ' - ' + self.record_time + 
+                    ' - elevation profile (m).png')
+        
+        # 'Standard' units plot
+        plt.clf()
+        distance_array_miles = distance_array * (3.28 / 5280)
+        plt.fill_between(distance_array_miles, altitude_array * 3.28, 0,
+                         facecolor='green')
+        plt.title('Elevation Profile for:\n %s' % 
+                  filepath_split[-1][:-4])
+        plt.xlabel('Distance (Miles)')
+        plt.ylabel('Elevation (ft)')
+        x_min, x_max = plt.xlim() #@UnusedVariable
+        plt.xlim(x_min, distance_array_miles[-1])
+        plot_frame = plt.gcf()
+        plot_frame.set_size_inches(10, 5)
+        plt.savefig(filepath[:-4] + 
+                    ' - ' + self.record_time + 
+                    ' - elevation profile (ft).png')
+        
+        # Save statistics and a log of the analysis        
+        self.statistics += ('Distance traveled: %s meters (%s feet)\n' %
+                            (distance_total, distance_total * 3.2808399))
+        self.statistics += ('    %s kilometers (%s miles)\n' %
+                            (distance_total / 1000, 
+                             (distance_total * 3.2808399) / 5280))
+        self.statistics += (
+            'Final Elevation Gain/Loss: %s meters (%s feet)\n' %
+            (altitude_change_final, altitude_change_final * 3.2808399))
+        self.statistics += (
+            'Cumulative Elevation Gain: %s meters (%s feet)\n' %
+            (altitude_change_cumulative, 
+             altitude_change_cumulative * 3.2808399))
+        
+        log = open(filepath[:-4] + ' - ' + self.record_time + 
+                      ' - log.txt', 'w')
+        log.write('Working on file: %s\n' % filepath)
+        log.write('Analyzed on %s.\n' % (self.record_time))
+        log.write('--- Statistics ---\n')
+        log.write(self.statistics)
+        if self.read_file_error != '':
+            log.write('--- Source Data Reading ---\n')
+            log.write(self.read_file_error)
+        if self.parse_error != '':
+            log.write('--- Parsing ---\n')
+            log.write(self.parse_error)
+        log.write('--- End of Log ---\n')
+        log.close()
+        
+    def location_save(self, filepath, array, interpolated):
+        """Read a formatted Numpy array of KinetaMap data and save to disk
+        a comma delimited file containing just the GPS location records.
+        """
+        if interpolated == True:
+            suffix = ' - interpolated locations.csv'
+        else: 
+            suffix = ' - location only.csv'
+            array = self.fix_mask(array)
+            
+        new_filepath = (filepath[:-4] + ' - ' + 
+                            self.record_time + suffix)
+        
+        j = np.column_stack((array['time'],
+                                  array['latitude'],
+                                  array['longitude'],
+                                  array['altitude']))
+        header = 'UTC Time,Latitude,Longitude,Altitude (m)\n'
+        f = open(new_filepath, 'a')
+        f.write(header)
+        np.savetxt(f, j, delimiter=',',
+                   fmt=('%6d', '%6.6f', '%6.6f', '%1.3f'))
+        f.close()
+        
+    def KML_points(self, filepath, np_array, type, mag, name_bool):
+        """Read a formatted Numpy array of KinetaMap data and save to disk
+        a KML file containing just the GPS location records.
+        
+        Altitude records are replaced by the magnitude of the vector sum of
+        of the three axis acceleration records.
+        """
+        if type == 'locations':
+            np_array = self.fix_mask(np_array)
+            rec_type = 'locations only'
+            style_color = 'ff00ff00'
+        elif type == 'interpolated':
+            rec_type = 'interpolated'
+            style_color = 'ffffff00'
+        else:
+            rec_type = ''
+            
+        alt_mode = 'relativeToGround'
+        
+        if mag == 'clamp':
+            mag_label = 'clamped'
+            alt_mode = 'clampToGround'
+        elif mag == 'gps':
+            mag_label = 'GPS altitude'
+        elif mag == 'x':
+            mag_label = 'x acceleration'
+        elif mag == 'y':
+            mag_label = 'y acceleration'
+        elif mag == 'z':
+            mag_label = 'z acceleration'
+        elif mag == 'xy':
+            mag_label = 'xy magnitude'
+        elif mag == 'xz':
+            mag_label = 'xz magnitude'
+        elif mag == 'yz':
+            mag_label = 'yz magnitude'
+        elif mag == 'xyz':
+            mag_label = 'xyz magnitude'
+        else:
+            mag_label = 'zero magnitude'
+        
+        if name_bool == True:
+            labels = '[labels ON]'
+        else:
+            labels = '[labels OFF]'
+        
+        new_filepath = (filepath[:-4] + ' - ' + 
+                            self.record_time + ' - ' + rec_type +
+                            ' - ' + mag_label +
+                            ' - ' + labels + '.kml')
+        
+        g = open(new_filepath, "w")
+        
+        # Start building the kml file with header information
+        kml_header = "\
+<?xml version='1.0' encoding='UTF-8'?>\n\
+<kml xmlns='http://www.opengis.net/kml/2.2'>\n\
+    <Document>\n\
+        <name>Path points from " + filepath[:-4] + ' - ' + mag_label + \
+        ' - ' + labels + "</name>\n\
+        <Style id='dot'>\n\
+            <IconStyle>\n\
+                <Icon>\n\
+                    " + \
+"<href>http://maps.google.com/mapfiles/kml/pal2/icon18.png</href>\n\
+                </Icon>\n\
+                <scale>0.25</scale>\n\
+                <color>" + style_color + "</color>\n\
+            </IconStyle>\n\
+        </Style>\n"
+        
+        kml_footer = "\
+    </Document>\n\
+</kml>"
+        # Start out the KML file with the header
+        g.write(kml_header)
+        
+        for element in np_array:
+            kml_place = "\
+        <Placemark>\n"
+        
+            if mag == 'gps':
+                KML_alt = element['altitude']
+            elif mag == 'x':
+                KML_alt = element['ax']
+            elif mag == 'y':
+                KML_alt = element['ay']
+            elif mag == 'z':
+                KML_alt = element['az']
+            elif mag == 'xy':
+                KML_alt = np.power(
+                            np.power(element['ax'], 2) +
+                            np.power(element['ay'], 2), 0.5)
+            elif mag == 'xz':
+                KML_alt = np.power(
+                            np.power(element['ax'], 2) +
+                            np.power(element['az'], 2), 0.5)
+            elif mag == 'yz':
+                KML_alt = np.power(
+                            np.power(element['ay'], 2) +
+                            np.power(element['az'], 2), 0.5)
+            elif mag == 'xyz':
+                KML_alt = np.power(
+                            np.power(element['ax'], 2) +
+                            np.power(element['ay'], 2) +
+                            np.power(element['az'], 2), 0.5)
+            else:
+                KML_alt = 0
+            
+            # Magnitude can only be plotted (and seen) positively in KML maps
+            KML_alt = abs(KML_alt)
+            
+            if name_bool == True:
+                kml_place = (kml_place + "\
+            <name>" + str(element['longitude']) +
+                                    "," + str(element['latitude']) +
+                                    "," + str(KML_alt) + "</name>\n")
+                
+            kml_place = kml_place + "\
+            <styleUrl>#dot</styleUrl>\n\
+            <Point>\n\
+                <altitudeMode>" + alt_mode + "</altitudeMode>\n\
+                <extrude>1</extrude>\n\
+                <coordinates>" + (str(element['longitude']) + ',' + 
+                                  str(element['latitude']) + ',' + 
+                                  str(KML_alt)) + "</coordinates>\n\
+            </Point>\n\
+        </Placemark>\n"
+            g.write(kml_place)
+        
+        # Done adding placemarks, append the KML footer
+        g.write(kml_footer)
+        
+        g.close()
+        return(new_filepath)
+         
+    def interpolate(self, np_array):
+        """Read a formatted Numpy array of KinetaMap data and return a Numpy
+        array with linearly interpolated location records for all 
+        acceleration records.
+        """
+        a_i = np_array
+        a_gps = self.fix_mask(np_array)
+        
+        a_gps_records = a_gps['record']
+
+        for i in range(0, len(a_gps_records)):
+            dr = a_gps_records[i] - a_gps_records[i - 1]
+            
+            lat_dt = (a_i[a_gps_records[i]]['latitude'] - 
+                          a_i[a_gps_records[i - 1]]['latitude'])
+
+            lon_dt = (a_i[a_gps_records[i]]['longitude'] - 
+                          a_i[a_gps_records[i - 1]]['longitude'])
+            alt_dt = (a_i[a_gps_records[i]]['altitude'] - 
+                          a_i[a_gps_records[i - 1]]['altitude'])
+            
+            lat_dt_dr = lat_dt / dr
+            lon_dt_dr = lon_dt / dr
+            alt_dt_dr = alt_dt / dr
+            
+            lat_increment = 0
+            lon_increment = 0
+            alt_increment = 0
+            
+            a_i_records = a_gps_records[i - 1]
+            for j in a_i[ a_gps_records[i - 1]:a_gps_records[i] + 1 ]:
+                if a_i_records == a_gps_records[i - 1]:
+                    lat_increment = a_i[a_i_records]['latitude']
+                    lon_increment = a_i[a_i_records]['longitude']
+                    alt_increment = a_i[a_i_records]['altitude']
+                    a_i_records += 1
+                else:
+                    lat_increment = lat_increment + lat_dt_dr
+                    lon_increment = lon_increment + lon_dt_dr
+                    alt_increment = alt_increment + alt_dt_dr
+                    a_i[a_i_records]['latitude'] = lat_increment
+                    a_i[a_i_records]['longitude'] = lon_increment
+                    a_i[a_i_records]['altitude'] = alt_increment
+                    a_i[a_i_records]['NS'] = \
+                                        a_i[a_gps_records[i - 1]]['NS']
+                    a_i[a_i_records]['EW'] = \
+                                        a_i[a_gps_records[i - 1]]['EW']
+                    a_i_records += 1    
+    
+        # Remove records not bound by GPS data
+        gps_mask = ((a_gps['record'].min() <= a_i['record']) &
+                        (a_i['record'] <= a_gps['record'].max()))
+        
+        a_i = a_i[gps_mask]
+        return(a_i)
+    
+    def interpolated_save(self, filepath, np_array):
+        """Read a formatted Numpy array of KinetaMap data and save to disk
+        a comma delimited file containing GPS records and acceleration 
+        records that include interpolated locations.
+        """
+        new_filepath = (filepath[:-4] + ' ' + self.record_time + 
+                            ' - Interpolated.csv')
+        j = np.column_stack((np_array['time'],
+                                  np_array['ax'],
+                                  np_array['ay'],
+                                  np_array['az'],
+                                  np_array['latitude'],
+                                  np_array['longitude'],
+                                  np_array['altitude']))
+        header = 'UTC Time,x,y,z,Latitude,Longitude,Altitude (m)\n'
+        f = open(new_filepath, 'a')
+        f.write(header)
+        np.savetxt(f, j, delimiter=',',
+                   fmt=('%1.2f', '%1.3f', '%1.3f', 
+                        '%1.3f', '%6.6f', '%6.6f', '%1.3f'))
+        f.close()
+    
+    def parse(self, filepath):
+        """Open a KinetaMap data file and save to disk cleaned data files,
+        KML records and elevation profiles images.
+        
+        This is the main analysis function.
+        """
+        array = self.read_file(filepath)
+        
+        # Check that there are gps locations in the file
+        check = self.fix_mask(array)
+        if len(check) == 0:
+            return(False)
+        
+        # Reformat the latitude and longitude
+        array = self.clean(array)
+        
+        # Calculate track stats, log errors
+        self.track_stats(filepath, array)
+        
+        # Write GPS fixed track points to a .csv file
+        self.location_save(filepath, array, False)
+        
+        # Write the GPS track data to KML files
+        self.KML_points(filepath, array, 'locations', 'gps', True)
+        self.KML_points(filepath, array, 'locations', 'gps', False)
+        self.KML_points(filepath, array, 'locations', 'clamp', False)
+        
+        # Interpolate accelerometer locations between GPS records
+        array_interp = self.interpolate(array)
+        
+        # Write interpolated track data to a .csv file
+        self.location_save(filepath, array_interp, True)
+        
+        # Write interpolated data to KML files ('interpolated' in file name)
+        self.KML_points(filepath, array_interp, 'interpolated', 'clamp', True)
+        self.KML_points(filepath, array_interp, 'interpolated', 'clamp', False)
+        self.KML_points(filepath, array_interp, 'interpolated', 'gps', False)
+        self.KML_points(filepath, array_interp, 'interpolated', 'x', False)
+        self.KML_points(filepath, array_interp, 'interpolated', 'y', False)
+        self.KML_points(filepath, array_interp, 'interpolated', 'z', False)
+        self.KML_points(filepath, array_interp, 'interpolated', 'xy', False)
+        self.KML_points(filepath, array_interp, 'interpolated', 'xz', False)
+        self.KML_points(filepath, array_interp, 'interpolated', 'yz', False)
+        self.KML_points(filepath, array_interp, 'interpolated', 'xyz', False)
+        return(True)
+    
+class MainWindow(Tkinter.Tk):
+    """GUI to select KinetaMap files, plot location records and save
+    analyzed data to disk.
+    """
+    
+    def __init__(self, parent):
+        
+        # Instance the KinetaMapParse class
+        self.kmp = KinetaMapParse()
+        
+        # Tkinter setup
+        Tkinter.Tk.__init__(self, parent)
+        
+        self.main_file_name = Tkinter.StringVar()        
+        self.main_status = Tkinter.StringVar()
+        
+        self.file_selected = False
+        self.file_name = ''
+        
+        self.protocol("WM_DELETE_WINDOW", self.quit)
+        
+        # Configuration information
+        self.default_log_dir = 'D:\logs'
+        self.main_status.set('Default directory: %s' % (self.default_log_dir))
+        
+        # Start the windows!
+        self.window()
+            
+    def ask_file(self):
+        try:
+            self.file_name = tkFileDialog.askopenfilename(
+                                title='Select file to parse...',
+                                initialdir=self.default_log_dir)
+            if self.file_name != '':
+                self.main_file_name.set('Working with: %s' % (self.file_name))
+                self.main_status.set('Data file opened successfully.')
+                self.file_selected = True
+        except:
+            self.main_status.set('Error opening file')
+        
+    def parse(self):
+        if self.file_selected:
+            self.main_status.set('Working on : %s' % (self.file_name))
+            parse_result = self.kmp.parse(self.file_name)
+            if parse_result == None:
+                self.main_status.set('Problem opening: %s' % (self.file_name))
+            elif parse_result == False:
+                self.main_status.set('No GPS fixes in: %s' % (self.file_name))
+            else:
+                self.main_status.set('Successfully parsed and saved!')
+        else:
+            self.main_status.set('No data file selected.')
+        
+    def quit(self):
+        self.destroy()
+
+    def window(self):
+        
+        # Window format options
+        font_1 = ('Arial', 8, 'bold')
+        self.title('KinetaMap Data Parser')
+        
+        # Window position and layout
+        w_width, w_height = 540, 110
+        s_width = self.winfo_screenwidth()
+        s_height = self.winfo_screenheight()
+        x = (s_width / 2) - (w_width / 2)
+        y = (s_height / 2) - (w_height / 2)
+        self.geometry('%dx%d+%d+%d' % (w_width, w_height, x, y))
+
+        # Window Grid Elements
+        tk_row = 0    # Header row labels
+        Tkinter.Label(self, text='Data File:', font=font_1).\
+            grid(row=tk_row, column=0, sticky='W')
+        
+        tk_row = 1
+        Tkinter.Label(self, textvariable=self.main_file_name).\
+            grid(row=tk_row, column=0, columnspan=3, sticky='W')
+        
+        tk_row = 2
+        Tkinter.Label(self, text='Status:', font=font_1).\
+            grid(row=tk_row, column=0, sticky='W')
+        
+        tk_row = 3
+        Tkinter.Label(self, textvariable=self.main_status).\
+            grid(row=tk_row, column=0, columnspan=3, sticky='W')
+             
+        tk_row = 4
+        Tkinter.Button(self, text="Select File", fg="blue", font=font_1,
+                       command=self.ask_file).grid(
+                                                  row=tk_row, 
+                                                  column=0, 
+                                                  padx='10', 
+                                                  sticky='W')
+        
+        Tkinter.Button(self, text="Parse", fg="green", font=font_1,
+                       command=self.parse).grid(
+                                                row=tk_row, 
+                                                column=1, 
+                                                padx='10')
+        
+        Tkinter.Button(self, text="Quit", fg="red", font=font_1,
+                       command=self.quit).grid(
+                                               row=tk_row, 
+                                               column=2, 
+                                               padx='10')
+        
+if __name__ == '__main__':
+    go = MainWindow(None)
+    go.mainloop()
+    #test = KinetaMapParse()
+    #test.parse('D:\\logs\\my_file')
+# eof