Colin Dietrich avatar Colin Dietrich committed 2b43449

Added .csv save of interpolated acceleration data
Fixed .csv file name inconsistencies
Added .py file identification in log file.
Fixed left padding of time field in non-interpolated .csv files

Comments (0)

Files changed (1)

+"""Parser and Plotter for the Sparkfun KinetaMap, v49
+Copyright (C) 2011  Colin Dietrich
+
+Changes:    Added .csv save of interpolated acceleration data
+            Fixed .csv file name inconsistencies
+            Added .py file identification in log file.
+            Fixed left padding of time field in non-interpolated .csv files
+
+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 or 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 sys
+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 using %s.\n' % (self.record_time, 
+                                         sys.modules['__main__'].__file__))
+        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=('%5d', '%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 accelerometer locations to a .csv file
+        self.interpolated_save(filepath, array_interp)
+        
+        # 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
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.